You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by cc...@apache.org on 2018/06/21 21:42:07 UTC
[incubator-superset] branch dashboard-builder updated: [dashboard
v2] ui + ux fixes (#5208)
This is an automated email from the ASF dual-hosted git repository.
ccwilliams pushed a commit to branch dashboard-builder
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/dashboard-builder by this push:
new e979d6d [dashboard v2] ui + ux fixes (#5208)
e979d6d is described below
commit e979d6df6bb76c880aaca84f980fa3cb1db4f48b
Author: Chris Williams <wi...@users.noreply.github.com>
AuthorDate: Thu Jun 21 14:42:00 2018 -0700
[dashboard v2] ui + ux fixes (#5208)
* [dashboard v2] use <Loading /> throughout, small loading gif, improve row/column visual hierarchy, add cached data pop
* [dashboard v2] lots of polish
* [dashboard v2] remove markdown padding on edit, more opaque slice drag preview, unsavedChanges=true upon moving a component, fix initial load logging.
* [dashboard v2] gray loading.gif, sticky header, undo/redo keyboard shortcuts, fix move component saved changes update, v0 double scrollbar fix
* [dashboard v2] move UndoRedoKeylisteners into Header, render only in edit mode, show visual feedback for keyboard shortcut, hide hover menu in top-level tabs
* [dashboard v2] fix grid + sidepane height issues
* [dashboard v2] add auto-resize functionality, update tests. cache findParentId results.
* [dashboard v2][tests] add getDetailedComponentWidth_spec.js
* [dashboard v2] fix lint
---
superset/assets/images/loading.gif | Bin 1945878 -> 79023 bytes
.../dashboard/actions/dashboardLayout_spec.js | 71 ++++---
.../dashboard/components/DashboardGrid_spec.jsx | 10 +-
.../dashboard/util/dropOverflowsParent_spec.js | 118 ++++++++++-
.../util/getDetailedComponentWidth_spec.js | 223 +++++++++++++++++++++
.../dashboard/util/newEntitiesFromDrop_spec.js | 3 +
.../assets/src/SqlLab/components/QuerySearch.jsx | 79 ++++----
.../assets/src/SqlLab/components/ResultSet.jsx | 3 +-
superset/assets/src/components/Loading.jsx | 9 +-
.../src/dashboard/actions/dashboardLayout.js | 41 +---
.../dashboard/components/BuilderComponentPane.jsx | 119 ++++++-----
.../assets/src/dashboard/components/Dashboard.jsx | 22 +-
.../src/dashboard/components/DashboardBuilder.jsx | 93 ++++-----
.../src/dashboard/components/DashboardGrid.jsx | 38 +++-
.../assets/src/dashboard/components/Header.jsx | 214 ++++++++++++--------
.../assets/src/dashboard/components/SliceAdder.jsx | 9 +-
.../dashboard/components/UndoRedoKeylisteners.jsx | 47 +++++
.../components/dnd/AddSliceDragPreview.jsx | 4 +-
.../src/dashboard/components/dnd/handleHover.js | 2 +-
.../components/gridComponents/Markdown.jsx | 13 +-
.../dashboard/components/gridComponents/Tab.jsx | 45 ++++-
.../dashboard/components/gridComponents/Tabs.jsx | 20 +-
.../dashboard/containers/DashboardComponent.jsx | 21 +-
.../deprecated/v1/components/SliceAdder.jsx | 7 +-
.../src/dashboard/reducers/dashboardLayout.js | 20 ++
.../dashboard/stylesheets/builder-sidepane.less | 4 +-
.../dashboard/stylesheets/components/chart.less | 40 +++-
.../dashboard/stylesheets/components/column.less | 8 +-
.../dashboard/stylesheets/components/header.less | 26 ++-
.../dashboard/stylesheets/components/markdown.less | 9 +
.../src/dashboard/stylesheets/components/row.less | 9 +-
.../src/dashboard/stylesheets/components/tabs.less | 163 ++++++++-------
.../src/dashboard/stylesheets/dashboard.less | 49 ++---
superset/assets/src/dashboard/stylesheets/dnd.less | 35 +++-
.../assets/src/dashboard/stylesheets/grid.less | 17 +-
.../src/dashboard/stylesheets/popover-menu.less | 5 +-
.../src/dashboard/util/dropOverflowsParent.js | 45 +----
superset/assets/src/dashboard/util/findParentId.js | 14 +-
.../assets/src/dashboard/util/getChildWidth.js | 13 --
.../dashboard/util/getComponentWidthFromDrop.js | 57 ++++++
.../dashboard/util/getDetailedComponentWidth.js | 76 +++++++
.../assets/src/dashboard/util/getDropPosition.js | 2 +-
.../src/dashboard/util/headerStyleOptions.js | 18 +-
.../src/dashboard/util/newEntitiesFromDrop.js | 8 +
.../src/explore/components/DisplayQueryButton.jsx | 14 +-
.../components/controls/DatasourceControl.jsx | 58 +++---
.../assets/src/profile/components/TableLoader.jsx | 25 ++-
superset/assets/src/welcome/DashboardTable.jsx | 8 +-
superset/assets/yarn.lock | 57 +++++-
49 files changed, 1377 insertions(+), 614 deletions(-)
diff --git a/superset/assets/images/loading.gif b/superset/assets/images/loading.gif
index ae5cbdd..d82fc5d 100644
Binary files a/superset/assets/images/loading.gif and b/superset/assets/images/loading.gif differ
diff --git a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
index 84f0856..4b28480 100644
--- a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js
@@ -43,7 +43,9 @@ import {
describe('dashboardLayout actions', () => {
const mockState = {
- dashboardState: {},
+ dashboardState: {
+ hasUnsavedChanges: true, // don't dispatch setUnsavedChanges() after every action
+ },
dashboardInfo: {},
dashboardLayout: {
past: [],
@@ -62,9 +64,7 @@ describe('dashboardLayout actions', () => {
describe('updateComponents', () => {
it('should dispatch an updateLayout action', () => {
- const { getState, dispatch } = setup({
- dashboardState: { hasUnsavedChanges: true },
- });
+ const { getState, dispatch } = setup();
const nextComponents = { 1: {} };
const thunk = updateComponents(nextComponents);
thunk(dispatch, getState);
@@ -91,9 +91,7 @@ describe('dashboardLayout actions', () => {
describe('deleteComponents', () => {
it('should dispatch an deleteComponent action', () => {
- const { getState, dispatch } = setup({
- dashboardState: { hasUnsavedChanges: true },
- });
+ const { getState, dispatch } = setup();
const thunk = deleteComponent('id', 'parentId');
thunk(dispatch, getState);
expect(dispatch.callCount).to.equal(1);
@@ -135,14 +133,14 @@ describe('dashboardLayout actions', () => {
},
},
});
+
+ expect(dispatch.callCount).to.equal(2);
});
});
describe('createTopLevelTabs', () => {
it('should dispatch a createTopLevelTabs action', () => {
- const { getState, dispatch } = setup({
- dashboardState: { hasUnsavedChanges: true },
- });
+ const { getState, dispatch } = setup();
const dropResult = {};
const thunk = createTopLevelTabs(dropResult);
thunk(dispatch, getState);
@@ -169,9 +167,7 @@ describe('dashboardLayout actions', () => {
describe('deleteTopLevelTabs', () => {
it('should dispatch a deleteTopLevelTabs action', () => {
- const { getState, dispatch } = setup({
- dashboardState: { hasUnsavedChanges: true },
- });
+ const { getState, dispatch } = setup();
const dropResult = {};
const thunk = deleteTopLevelTabs(dropResult);
thunk(dispatch, getState);
@@ -213,7 +209,6 @@ describe('dashboardLayout actions', () => {
it('should update the size of the component', () => {
const { getState, dispatch } = setup({
- dashboardState: { hasUnsavedChanges: true },
dashboardLayout,
});
@@ -239,6 +234,8 @@ describe('dashboardLayout actions', () => {
},
},
});
+
+ expect(dispatch.callCount).to.equal(2);
});
it('should dispatch a setUnsavedChanges action if hasUnsavedChanges=false', () => {
@@ -265,11 +262,11 @@ describe('dashboardLayout actions', () => {
dragging: { id: NEW_ROW_ID, type: ROW_TYPE },
};
- const thunk1 = handleComponentDrop(dropResult);
- thunk1(dispatch, getState);
+ const handleComponentDropThunk = handleComponentDrop(dropResult);
+ handleComponentDropThunk(dispatch, getState);
- const thunk2 = dispatch.getCall(0).args[0];
- thunk2(dispatch, getState);
+ const createComponentThunk = dispatch.getCall(0).args[0];
+ createComponentThunk(dispatch, getState);
expect(dispatch.getCall(1).args[0]).to.deep.equal({
type: CREATE_COMPONENT,
@@ -277,36 +274,47 @@ describe('dashboardLayout actions', () => {
dropResult,
},
});
+
+ expect(dispatch.callCount).to.equal(2);
});
it('should move a component if the component is not new', () => {
const { getState, dispatch } = setup({
- dashboardLayout: { present: { id: { type: ROW_TYPE, children: [] } } },
+ dashboardLayout: {
+ // if 'dragging' is not only child will dispatch deleteComponent thunk
+ present: { id: { type: ROW_TYPE, children: ['_'] } },
+ },
});
const dropResult = {
source: { id: 'id', index: 0, type: ROW_TYPE },
destination: { id: DASHBOARD_GRID_ID, type: DASHBOARD_GRID_TYPE },
- dragging: { id: NEW_ROW_ID, type: ROW_TYPE },
+ dragging: { id: 'dragging', type: ROW_TYPE },
};
- const thunk = handleComponentDrop(dropResult);
- thunk(dispatch, getState);
+ const handleComponentDropThunk = handleComponentDrop(dropResult);
+ handleComponentDropThunk(dispatch, getState);
- expect(dispatch.getCall(0).args[0]).to.deep.equal({
+ const moveComponentThunk = dispatch.getCall(0).args[0];
+ moveComponentThunk(dispatch, getState);
+
+ expect(dispatch.getCall(1).args[0]).to.deep.equal({
type: MOVE_COMPONENT,
payload: {
dropResult,
},
});
+
+ expect(dispatch.callCount).to.equal(2);
});
it('should dispatch a toast if the drop overflows the destination', () => {
const { getState, dispatch } = setup({
dashboardLayout: {
present: {
- source: { type: ROW_TYPE, meta: { width: 0 } },
- destination: { type: ROW_TYPE, meta: { width: 0 } },
- dragging: { type: CHART_TYPE, meta: { width: 100 } },
+ source: { type: ROW_TYPE },
+ destination: { type: ROW_TYPE, children: ['rowChild'] },
+ dragging: { type: CHART_TYPE, meta: { width: 1 } },
+ rowChild: { type: CHART_TYPE, meta: { width: 12 } },
},
},
});
@@ -321,6 +329,8 @@ describe('dashboardLayout actions', () => {
expect(dispatch.getCall(0).args[0].type).to.deep.equal(
addInfoToast('').type,
);
+
+ expect(dispatch.callCount).to.equal(1);
});
it('should delete a parent Row or Tabs if the moved child was the only child', () => {
@@ -358,6 +368,9 @@ describe('dashboardLayout actions', () => {
parentId: 'parentId',
},
});
+
+ // move thunk, delete thunk, delete result actions
+ expect(dispatch.callCount).to.equal(3);
});
it('should create top-level tabs if dropped on root', () => {
@@ -380,6 +393,8 @@ describe('dashboardLayout actions', () => {
dropResult,
},
});
+
+ expect(dispatch.callCount).to.equal(2);
});
});
@@ -413,9 +428,7 @@ describe('dashboardLayout actions', () => {
describe('redoLayoutAction', () => {
it('should dispatch a redux-undo .redo() action ', () => {
- const { getState, dispatch } = setup({
- dashboardState: { hasUnsavedChanges: true },
- });
+ const { getState, dispatch } = setup();
const thunk = redoLayoutAction();
thunk(dispatch, getState);
diff --git a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
index 1160d65..d11c37f 100644
--- a/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/DashboardGrid_spec.jsx
@@ -42,11 +42,11 @@ describe('DashboardGrid', () => {
expect(wrapper.find(DashboardComponent)).to.have.length(2);
});
- it('should render an empty DragDroppables in editMode to increase the drop target zone', () => {
- const withChildren = setup({ editMode: false });
- const withoutChildren = setup({ editMode: true });
- expect(withChildren.find(DragDroppable)).to.have.length(0);
- expect(withoutChildren.find(DragDroppable)).to.have.length(1);
+ it('should render two empty DragDroppables in editMode to increase the drop target zone', () => {
+ const viewMode = setup({ editMode: false });
+ const editMode = setup({ editMode: true });
+ expect(viewMode.find(DragDroppable)).to.have.length(0);
+ expect(editMode.find(DragDroppable)).to.have.length(2);
});
it('should render grid column guides when resizing', () => {
diff --git a/superset/assets/spec/javascripts/dashboard/util/dropOverflowsParent_spec.js b/superset/assets/spec/javascripts/dashboard/util/dropOverflowsParent_spec.js
index b153e1e..8e6f889 100644
--- a/superset/assets/spec/javascripts/dashboard/util/dropOverflowsParent_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/util/dropOverflowsParent_spec.js
@@ -7,6 +7,8 @@ import {
CHART_TYPE,
COLUMN_TYPE,
ROW_TYPE,
+ HEADER_TYPE,
+ TAB_TYPE,
} from '../../../../src/dashboard/util/componentTypes';
describe('dropOverflowsParent', () => {
@@ -42,7 +44,7 @@ describe('dropOverflowsParent', () => {
expect(dropOverflowsParent(dropResult, layout)).to.equal(true);
});
- it('returns false if a parent DOES not have adequate width for child', () => {
+ it('returns false if a parent DOES have adequate width for child', () => {
const dropResult = {
source: { id: '_' },
destination: { id: 'a' },
@@ -74,9 +76,41 @@ describe('dropOverflowsParent', () => {
expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
});
- it('it should base result off of column width (instead of its children) if dropped on column', () => {
+ it('returns false if a child CAN shrink to available parent space', () => {
const dropResult = {
- source: { id: 'z' },
+ source: { id: '_' },
+ destination: { id: 'a' },
+ dragging: { id: 'z' },
+ };
+
+ const layout = {
+ a: {
+ id: 'a',
+ type: ROW_TYPE,
+ children: ['b', 'b'], // 2x b = 10
+ },
+ b: {
+ id: 'b',
+ type: CHART_TYPE,
+ meta: {
+ width: 5,
+ },
+ },
+ z: {
+ id: 'z',
+ type: CHART_TYPE,
+ meta: {
+ width: 10,
+ },
+ },
+ };
+
+ expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
+ });
+
+ it('returns true if a child CANNOT shrink to available parent space', () => {
+ const dropResult = {
+ source: { id: '_' },
destination: { id: 'a' },
dragging: { id: 'b' },
};
@@ -85,24 +119,71 @@ describe('dropOverflowsParent', () => {
a: {
id: 'a',
type: COLUMN_TYPE,
- meta: { width: 10 },
+ meta: {
+ width: 6,
+ },
},
+ // rows with children cannot shrink
b: {
id: 'b',
+ type: ROW_TYPE,
+ children: ['bChild', 'bChild', 'bChild'],
+ },
+ bChild: {
+ id: 'bChild',
type: CHART_TYPE,
meta: {
- width: 2,
+ width: 3,
},
},
};
- expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
+ expect(dropOverflowsParent(dropResult, layout)).to.equal(true);
+ });
+
+ it('returns true if a column has children that CANNOT shrink to available parent space', () => {
+ const dropResult = {
+ source: { id: '_' },
+ destination: { id: 'destination' },
+ dragging: { id: 'dragging' },
+ };
+
+ const layout = {
+ destination: {
+ id: 'destination',
+ type: ROW_TYPE,
+ children: ['b', 'b'], // 2x b = 10, 2 available
+ },
+ b: {
+ id: 'b',
+ type: CHART_TYPE,
+ meta: {
+ width: 5,
+ },
+ },
+ dragging: {
+ id: 'dragging',
+ type: COLUMN_TYPE,
+ meta: {
+ width: 10,
+ },
+ children: ['rowWithChildren'], // 2x b = width 10
+ },
+ rowWithChildren: {
+ id: 'rowWithChildren',
+ type: ROW_TYPE,
+ children: ['b', 'b'],
+ },
+ };
+
+ expect(dropOverflowsParent(dropResult, layout)).to.equal(true);
+ // remove children
expect(
dropOverflowsParent(dropResult, {
...layout,
- a: { ...layout.a, meta: { width: 1 } },
+ dragging: { ...layout.dragging, children: [] },
}),
- ).to.equal(true);
+ ).to.equal(false);
});
it('should work with new components that are not in the layout', () => {
@@ -122,4 +203,25 @@ describe('dropOverflowsParent', () => {
expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
});
+
+ it('source/destination without widths should not overflow parent', () => {
+ const dropResult = {
+ source: { id: '_' },
+ destination: { id: 'tab' },
+ dragging: { id: 'header' },
+ };
+
+ const layout = {
+ tab: {
+ id: 'tab',
+ type: TAB_TYPE,
+ },
+ header: {
+ id: 'header',
+ type: HEADER_TYPE,
+ },
+ };
+
+ expect(dropOverflowsParent(dropResult, layout)).to.equal(false);
+ });
});
diff --git a/superset/assets/spec/javascripts/dashboard/util/getDetailedComponentWidth_spec.js b/superset/assets/spec/javascripts/dashboard/util/getDetailedComponentWidth_spec.js
new file mode 100644
index 0000000..99e2282
--- /dev/null
+++ b/superset/assets/spec/javascripts/dashboard/util/getDetailedComponentWidth_spec.js
@@ -0,0 +1,223 @@
+import { describe, it } from 'mocha';
+import { expect } from 'chai';
+
+import getDetailedComponentWidth from '../../../../src/dashboard/util/getDetailedComponentWidth';
+import * as types from '../../../../src/dashboard/util/componentTypes';
+import {
+ GRID_COLUMN_COUNT,
+ GRID_MIN_COLUMN_COUNT,
+} from '../../../../src/dashboard/util/constants';
+
+describe('getDetailedComponentWidth', () => {
+ it('should return an object with width, minimumWidth, and occupiedWidth', () => {
+ expect(
+ getDetailedComponentWidth({ id: '_', components: {} }),
+ ).to.have.all.keys(['minimumWidth', 'occupiedWidth', 'width']);
+ });
+
+ describe('width', () => {
+ it('should be undefined if the component is not resizable and has no defined width', () => {
+ const empty = {
+ width: undefined,
+ occupiedWidth: undefined,
+ minimumWidth: undefined,
+ };
+
+ expect(
+ getDetailedComponentWidth({
+ component: { id: '', type: types.HEADER_TYPE },
+ }),
+ ).to.deep.equal(empty);
+
+ expect(
+ getDetailedComponentWidth({
+ component: { id: '', type: types.DIVIDER_TYPE },
+ }),
+ ).to.deep.equal(empty);
+
+ expect(
+ getDetailedComponentWidth({
+ component: { id: '', type: types.TAB_TYPE },
+ }),
+ ).to.deep.equal(empty);
+ });
+
+ it('should match component meta width for resizeable components', () => {
+ expect(
+ getDetailedComponentWidth({
+ component: { id: '', type: types.CHART_TYPE, meta: { width: 1 } },
+ }),
+ ).to.deep.equal({ width: 1, occupiedWidth: 1, minimumWidth: 1 });
+
+ expect(
+ getDetailedComponentWidth({
+ component: { id: '', type: types.MARKDOWN_TYPE, meta: { width: 2 } },
+ }),
+ ).to.deep.equal({ width: 2, occupiedWidth: 2, minimumWidth: 1 });
+
+ expect(
+ getDetailedComponentWidth({
+ component: { id: '', type: types.COLUMN_TYPE, meta: { width: 3 } },
+ }),
+ // note: occupiedWidth is zero for colunns/see test below
+ ).to.deep.equal({ width: 3, occupiedWidth: 0, minimumWidth: 1 });
+ });
+
+ it('should be GRID_COLUMN_COUNT for row components WITHOUT parents', () => {
+ expect(
+ getDetailedComponentWidth({
+ id: 'row',
+ components: { row: { id: 'row', type: types.ROW_TYPE } },
+ }),
+ ).to.deep.equal({
+ width: GRID_COLUMN_COUNT,
+ occupiedWidth: 0,
+ minimumWidth: GRID_MIN_COLUMN_COUNT,
+ });
+ });
+
+ it('should match parent width for row components WITH parents', () => {
+ expect(
+ getDetailedComponentWidth({
+ id: 'row',
+ components: {
+ row: { id: 'row', type: types.ROW_TYPE },
+ parent: {
+ id: 'parent',
+ type: types.COLUMN_TYPE,
+ children: ['row'],
+ meta: { width: 7 },
+ },
+ },
+ }),
+ ).to.deep.equal({
+ width: 7,
+ occupiedWidth: 0,
+ minimumWidth: GRID_MIN_COLUMN_COUNT,
+ });
+ });
+
+ it('should use either id or component (to support new components)', () => {
+ expect(
+ getDetailedComponentWidth({
+ id: 'id',
+ components: {
+ id: { id: 'id', type: types.CHART_TYPE, meta: { width: 6 } },
+ },
+ }).width,
+ ).to.equal(6);
+
+ expect(
+ getDetailedComponentWidth({
+ component: { id: 'id', type: types.CHART_TYPE, meta: { width: 6 } },
+ }).width,
+ ).to.equal(6);
+ });
+ });
+
+ describe('occupiedWidth', () => {
+ it('should reflect the sum of child widths for row components', () => {
+ expect(
+ getDetailedComponentWidth({
+ id: 'row',
+ components: {
+ row: {
+ id: 'row',
+ type: types.ROW_TYPE,
+ children: ['child', 'child'],
+ },
+ child: { id: 'child', meta: { width: 3.5 } },
+ },
+ }),
+ ).to.deep.equal({ width: 12, occupiedWidth: 7, minimumWidth: 7 });
+ });
+
+ it('should always be zero for column components', () => {
+ expect(
+ getDetailedComponentWidth({
+ component: { id: '', type: types.COLUMN_TYPE, meta: { width: 2 } },
+ }),
+ ).to.deep.equal({ width: 2, occupiedWidth: 0, minimumWidth: 1 });
+ });
+ });
+
+ describe('minimumWidth', () => {
+ it('should equal GRID_MIN_COLUMN_COUNT for resizable components', () => {
+ expect(
+ getDetailedComponentWidth({
+ component: { id: '', type: types.CHART_TYPE, meta: { width: 1 } },
+ }),
+ ).to.deep.equal({
+ width: 1,
+ minimumWidth: GRID_MIN_COLUMN_COUNT,
+ occupiedWidth: 1,
+ });
+
+ expect(
+ getDetailedComponentWidth({
+ component: { id: '', type: types.MARKDOWN_TYPE, meta: { width: 2 } },
+ }),
+ ).to.deep.equal({
+ width: 2,
+ minimumWidth: GRID_MIN_COLUMN_COUNT,
+ occupiedWidth: 2,
+ });
+
+ expect(
+ getDetailedComponentWidth({
+ component: { id: '', type: types.COLUMN_TYPE, meta: { width: 3 } },
+ }),
+ ).to.deep.equal({
+ width: 3,
+ minimumWidth: GRID_MIN_COLUMN_COUNT,
+ occupiedWidth: 0,
+ });
+ });
+
+ it('should equal the width of row children for column components with row children', () => {
+ expect(
+ getDetailedComponentWidth({
+ id: 'column',
+ components: {
+ column: {
+ id: 'column',
+ type: types.COLUMN_TYPE,
+ children: ['rowChild', 'ignoredChartChild'],
+ meta: { width: 12 },
+ },
+ rowChild: {
+ id: 'rowChild',
+ type: types.ROW_TYPE,
+ children: ['rowChildChild', 'rowChildChild'],
+ },
+ rowChildChild: {
+ id: 'rowChildChild',
+ meta: { width: 3.5 },
+ },
+ ignoredChartChild: {
+ id: 'ignoredChartChild',
+ meta: { width: 100 },
+ },
+ },
+ }),
+ // occupiedWidth is zero for colunns/see test below
+ ).to.deep.equal({ width: 12, occupiedWidth: 0, minimumWidth: 7 });
+ });
+
+ it('should equal occupiedWidth for row components', () => {
+ expect(
+ getDetailedComponentWidth({
+ id: 'row',
+ components: {
+ row: {
+ id: 'row',
+ type: types.ROW_TYPE,
+ children: ['child', 'child'],
+ },
+ child: { id: 'child', meta: { width: 3.5 } },
+ },
+ }),
+ ).to.deep.equal({ width: 12, occupiedWidth: 7, minimumWidth: 7 });
+ });
+ });
+});
diff --git a/superset/assets/spec/javascripts/dashboard/util/newEntitiesFromDrop_spec.js b/superset/assets/spec/javascripts/dashboard/util/newEntitiesFromDrop_spec.js
index 677c329..8d00c18 100644
--- a/superset/assets/spec/javascripts/dashboard/util/newEntitiesFromDrop_spec.js
+++ b/superset/assets/spec/javascripts/dashboard/util/newEntitiesFromDrop_spec.js
@@ -16,6 +16,7 @@ describe('newEntitiesFromDrop', () => {
dropResult: {
destination: { id: 'a', index: 0 },
dragging: { type: CHART_TYPE },
+ source: { id: 'b', index: 0 },
},
layout: {
a: {
@@ -37,6 +38,7 @@ describe('newEntitiesFromDrop', () => {
dropResult: {
destination: { id: 'a', index: 0 },
dragging: { type: TABS_TYPE },
+ source: { id: 'b', index: 0 },
},
layout: {
a: {
@@ -61,6 +63,7 @@ describe('newEntitiesFromDrop', () => {
dropResult: {
destination: { id: 'a', index: 0 },
dragging: { type: CHART_TYPE },
+ source: { id: 'b', index: 0 },
},
layout: {
a: {
diff --git a/superset/assets/src/SqlLab/components/QuerySearch.jsx b/superset/assets/src/SqlLab/components/QuerySearch.jsx
index 9d36d85..45924e3 100644
--- a/superset/assets/src/SqlLab/components/QuerySearch.jsx
+++ b/superset/assets/src/SqlLab/components/QuerySearch.jsx
@@ -2,14 +2,19 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'react-bootstrap';
import Select from 'react-select';
+import Loading from '../../components/Loading';
import QueryTable from './QueryTable';
-import { now, epochTimeXHoursAgo,
- epochTimeXDaysAgo, epochTimeXYearsAgo } from '../../modules/dates';
+import {
+ now,
+ epochTimeXHoursAgo,
+ epochTimeXDaysAgo,
+ epochTimeXYearsAgo,
+} from '../../modules/dates';
import { STATUS_OPTIONS, TIME_OPTIONS } from '../constants';
import AsyncSelect from '../../components/AsyncSelect';
import { t } from '../../locales';
-const $ = window.$ = require('jquery');
+const $ = (window.$ = require('jquery'));
const propTypes = {
actions: PropTypes.object.isRequired,
@@ -47,13 +52,17 @@ class QuerySearch extends React.PureComponent {
this.refreshQueries();
}
onUserClicked(userId) {
- this.setState({ userId }, () => { this.refreshQueries(); });
+ this.setState({ userId }, () => {
+ this.refreshQueries();
+ });
}
onDbClicked(dbId) {
- this.setState({ databaseId: dbId }, () => { this.refreshQueries(); });
+ this.setState({ databaseId: dbId }, () => {
+ this.refreshQueries();
+ });
}
onChange(db) {
- const val = (db) ? db.value : null;
+ const val = db ? db.value : null;
this.setState({ databaseId: val });
}
getTimeFromSelection(selection) {
@@ -77,25 +86,25 @@ class QuerySearch extends React.PureComponent {
}
}
changeFrom(user) {
- const val = (user) ? user.value : null;
+ const val = user ? user.value : null;
this.setState({ from: val });
}
changeTo(status) {
- const val = (status) ? status.value : null;
+ const val = status ? status.value : null;
this.setState({ to: val });
}
changeUser(user) {
- const val = (user) ? user.value : null;
+ const val = user ? user.value : null;
this.setState({ userId: val });
}
insertParams(baseUrl, params) {
- const validParams = params.filter(
- function (p) { return p !== ''; },
- );
+ const validParams = params.filter(function (p) {
+ return p !== '';
+ });
return baseUrl + '?' + validParams.join('&');
}
changeStatus(status) {
- const val = (status) ? status.value : null;
+ const val = status ? status.value : null;
this.setState({ status: val });
}
changeSearch(event) {
@@ -120,7 +129,7 @@ class QuerySearch extends React.PureComponent {
if (data.result.length === 0) {
this.props.actions.addAlert({
bsStyle: 'danger',
- msg: t('It seems you don\'t have access to any database'),
+ msg: t("It seems you don't have access to any database"),
});
}
return options;
@@ -175,8 +184,10 @@ class QuerySearch extends React.PureComponent {
<Select
name="select-from"
placeholder={t('[From]-')}
- options={TIME_OPTIONS
- .slice(1, TIME_OPTIONS.length).map(xt => ({ value: xt, label: xt }))}
+ options={TIME_OPTIONS.slice(1, TIME_OPTIONS.length).map(xt => ({
+ value: xt,
+ label: xt,
+ }))}
value={this.state.from}
autosize={false}
onChange={this.changeFrom}
@@ -206,29 +217,21 @@ class QuerySearch extends React.PureComponent {
</Button>
</div>
</div>
- {this.state.queriesLoading ?
- (<img className="loading" alt="Loading..." src="/static/assets/images/loading.gif" />)
- :
- (
- <div className="scrollbar-container">
- <div
- className="scrollbar-content"
- style={{ height: this.props.height }}
- >
- <QueryTable
- columns={[
- 'state', 'db', 'user', 'time',
- 'progress', 'rows', 'sql', 'querylink',
- ]}
- onUserClicked={this.onUserClicked}
- onDbClicked={this.onDbClicked}
- queries={this.state.queriesArray}
- actions={this.props.actions}
- />
- </div>
+ {this.state.queriesLoading ? (
+ <Loading />
+ ) : (
+ <div className="scrollbar-container">
+ <div className="scrollbar-content" style={{ height: this.props.height }}>
+ <QueryTable
+ columns={['state', 'db', 'user', 'time', 'progress', 'rows', 'sql', 'querylink']}
+ onUserClicked={this.onUserClicked}
+ onDbClicked={this.onDbClicked}
+ queries={this.state.queriesArray}
+ actions={this.props.actions}
+ />
</div>
- )
- }
+ </div>
+ )}
</div>
);
}
diff --git a/superset/assets/src/SqlLab/components/ResultSet.jsx b/superset/assets/src/SqlLab/components/ResultSet.jsx
index f36a164..bd446bb 100644
--- a/superset/assets/src/SqlLab/components/ResultSet.jsx
+++ b/superset/assets/src/SqlLab/components/ResultSet.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { Alert, Button, ButtonGroup, ProgressBar } from 'react-bootstrap';
import shortid from 'shortid';
+import Loading from '../../components/Loading';
import VisualizeModal from './VisualizeModal';
import HighlightedSql from './HighlightedSql';
import FilterableTable from '../../components/FilterableTable/FilterableTable';
@@ -234,7 +235,7 @@ export default class ResultSet extends React.PureComponent {
}
return (
<div>
- <img className="loading" alt={t('Loading...')} src="/static/assets/images/loading.gif" />
+ <Loading />
<QueryStateLabel query={query} />
{progressBar}
<div>
diff --git a/superset/assets/src/components/Loading.jsx b/superset/assets/src/components/Loading.jsx
index 810c581..953e702 100644
--- a/superset/assets/src/components/Loading.jsx
+++ b/superset/assets/src/components/Loading.jsx
@@ -5,7 +5,7 @@ const propTypes = {
size: PropTypes.number,
};
const defaultProps = {
- size: 25,
+ size: 50,
};
export default function Loading(props) {
@@ -15,17 +15,18 @@ export default function Loading(props) {
alt="Loading..."
src="/static/assets/images/loading.gif"
style={{
- width: props.size,
- height: props.size,
+ width: Math.min(props.size, 50),
+ // height is auto
padding: 0,
margin: 0,
position: 'absolute',
left: '50%',
top: '50%',
- transform: 'translate(-50%, -60%)',
+ transform: 'translate(-50%, -50%)',
}}
/>
);
}
+
Loading.propTypes = propTypes;
Loading.defaultProps = defaultProps;
diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js
index c4908b0..bd01146 100644
--- a/superset/assets/src/dashboard/actions/dashboardLayout.js
+++ b/superset/assets/src/dashboard/actions/dashboardLayout.js
@@ -2,16 +2,10 @@ import { ActionCreators as UndoActionCreators } from 'redux-undo';
import { addInfoToast } from './messageToasts';
import { setUnsavedChanges } from './dashboardState';
-import {
- CHART_TYPE,
- MARKDOWN_TYPE,
- TABS_TYPE,
- ROW_TYPE,
-} from '../util/componentTypes';
+import { TABS_TYPE, ROW_TYPE } from '../util/componentTypes';
import {
DASHBOARD_ROOT_ID,
NEW_COMPONENTS_SOURCE_ID,
- GRID_MIN_COLUMN_COUNT,
DASHBOARD_HEADER_ID,
} from '../util/constants';
import dropOverflowsParent from '../util/dropOverflowsParent';
@@ -117,22 +111,6 @@ export function resizeComponent({ id, width, height }) {
},
};
- // set any resizable children to have a minimum width so that
- // the chances that they are validly movable to future containers is maximized
- component.children.forEach(childId => {
- const child = dashboard[childId];
- if ([CHART_TYPE, MARKDOWN_TYPE].includes(child.type)) {
- updatedComponents[childId] = {
- ...child,
- meta: {
- ...child.meta,
- width: GRID_MIN_COLUMN_COUNT,
- height: height || child.meta.height,
- },
- };
- }
- });
-
dispatch(updateComponents(updatedComponents));
}
};
@@ -140,14 +118,12 @@ export function resizeComponent({ id, width, height }) {
// Drag and drop --------------------------------------------------------------
export const MOVE_COMPONENT = 'MOVE_COMPONENT';
-function moveComponent(dropResult) {
- return {
- type: MOVE_COMPONENT,
- payload: {
- dropResult,
- },
- };
-}
+const moveComponent = setUnsavedChangesAfterAction(dropResult => ({
+ type: MOVE_COMPONENT,
+ payload: {
+ dropResult,
+ },
+}));
export const HANDLE_COMPONENT_DROP = 'HANDLE_COMPONENT_DROP';
export function handleComponentDrop(dropResult) {
@@ -160,7 +136,7 @@ export function handleComponentDrop(dropResult) {
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.`,
+ `There is not enough space for this component. Try decreasing its width, or increasing the destination width.`,
),
);
}
@@ -191,6 +167,7 @@ export function handleComponentDrop(dropResult) {
if (
(sourceComponent.type === TABS_TYPE ||
sourceComponent.type === ROW_TYPE) &&
+ sourceComponent.children &&
sourceComponent.children.length === 0
) {
const parentId = findParentId({
diff --git a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
index c35a637..aafee5d 100644
--- a/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
+++ b/superset/assets/src/dashboard/components/BuilderComponentPane.jsx
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import cx from 'classnames';
import { StickyContainer, Sticky } from 'react-sticky';
+import ParentSize from '@vx/responsive/build/components/ParentSize';
import NewColumn from './gridComponents/new/NewColumn';
import NewDivider from './gridComponents/new/NewDivider';
@@ -13,6 +14,8 @@ import NewMarkdown from './gridComponents/new/NewMarkdown';
import SliceAdder from '../containers/SliceAdder';
import { t } from '../../locales';
+const SUPERSET_HEADER_HEIGHT = 59;
+
const propTypes = {
topOffset: PropTypes.number,
toggleBuilderPane: PropTypes.func.isRequired,
@@ -42,62 +45,80 @@ class BuilderComponentPane extends React.PureComponent {
render() {
const { topOffset } = this.props;
return (
- <StickyContainer className="dashboard-builder-sidepane">
- <Sticky topOffset={-topOffset}>
- {({ style, calculatedHeight, isSticky }) => (
- <div
- className="viewport"
- style={isSticky ? { ...style, top: topOffset } : null}
- >
- <div
- className={cx('slider-container', this.state.slideDirection)}
- >
- <div className="component-layer slide-content">
- <div className="dashboard-builder-sidepane-header">
- <span>{t('Insert')}</span>
- <i
- className="fa fa-times trigger"
- onClick={this.props.toggleBuilderPane}
- role="none"
- />
- </div>
+ <div
+ className="dashboard-builder-sidepane"
+ style={{
+ height: `calc(100vh - ${topOffset + SUPERSET_HEADER_HEIGHT}px)`,
+ }}
+ >
+ <ParentSize>
+ {({ height }) => (
+ <StickyContainer>
+ <Sticky topOffset={-topOffset} bottomOffset={Infinity}>
+ {({ style, isSticky }) => (
<div
- className="new-component static"
- role="none"
- onClick={this.openSlicesPane}
+ className="viewport"
+ style={isSticky ? { ...style, top: topOffset } : null}
>
- <div className="new-component-placeholder fa fa-area-chart" />
- <div className="new-component-label">
- {t('Charts & filters')}
- </div>
+ <div
+ className={cx(
+ 'slider-container',
+ this.state.slideDirection,
+ )}
+ >
+ <div className="component-layer slide-content">
+ <div className="dashboard-builder-sidepane-header">
+ <span>{t('Insert')}</span>
+ <i
+ className="fa fa-times trigger"
+ onClick={this.props.toggleBuilderPane}
+ role="none"
+ />
+ </div>
+ <div
+ className="new-component static"
+ role="none"
+ onClick={this.openSlicesPane}
+ >
+ <div className="new-component-placeholder fa fa-area-chart" />
+ <div className="new-component-label">
+ {t('Your charts & filters')}
+ </div>
- <i className="fa fa-arrow-right trigger" />
- </div>
+ <i className="fa fa-arrow-right trigger" />
+ </div>
- <NewTabs />
- <NewRow />
- <NewColumn />
+ <NewTabs />
+ <NewRow />
+ <NewColumn />
- <NewHeader />
- <NewMarkdown />
- <NewDivider />
- </div>
- <div className="slices-layer slide-content">
- <div
- className="dashboard-builder-sidepane-header"
- onClick={this.closeSlicesPane}
- role="none"
- >
- <i className="fa fa-arrow-left trigger" />
- <span>{t('All components')}</span>
+ <NewHeader />
+ <NewMarkdown />
+ <NewDivider />
+ </div>
+ <div className="slices-layer slide-content">
+ <div
+ className="dashboard-builder-sidepane-header"
+ onClick={this.closeSlicesPane}
+ role="none"
+ >
+ <i className="fa fa-arrow-left trigger" />
+ <span>{t('All components')}</span>
+ </div>
+ <SliceAdder
+ height={
+ height + (isSticky ? SUPERSET_HEADER_HEIGHT : 0)
+ }
+ />
+ </div>
+ </div>
</div>
- <SliceAdder height={calculatedHeight} />
- </div>
- </div>
- </div>
+ )}
+ </Sticky>
+ </StickyContainer>
)}
- </Sticky>
- </StickyContainer>
+ </ParentSize>
+ </div>
);
}
}
diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx
index 99e93aa..f069cd1 100644
--- a/superset/assets/src/dashboard/components/Dashboard.jsx
+++ b/superset/assets/src/dashboard/components/Dashboard.jsx
@@ -77,10 +77,10 @@ class Dashboard extends React.PureComponent {
eventNames: DASHBOARD_EVENT_NAMES,
});
Logger.start(this.actionLog);
+ this.initTs = new Date().getTime();
}
componentDidMount() {
- this.ts_mount = new Date().getTime();
Logger.append(LOG_ACTIONS_MOUNT_DASHBOARD);
}
@@ -91,19 +91,22 @@ class Dashboard extends React.PureComponent {
: 'v2';
// log pane loads
const loadedPaneIds = [];
- const allPanesDidLoad = Object.entries(nextProps.loadStats).every(
+ let minQueryStartTime = Infinity;
+ const allVisiblePanesDidLoad = Object.entries(nextProps.loadStats).every(
([paneId, stats]) => {
- const { didLoad, minQueryStartTime, ...restStats } = stats;
-
+ const {
+ didLoad,
+ minQueryStartTime: paneMinQueryStart,
+ ...restStats
+ } = stats;
if (
didLoad &&
this.props.loadStats[paneId] &&
!this.props.loadStats[paneId].didLoad
) {
- const duration = new Date().getTime() - minQueryStartTime;
Logger.append(LOG_ACTIONS_LOAD_DASHBOARD_PANE, {
...restStats,
- duration,
+ duration: new Date().getTime() - paneMinQueryStart,
version,
});
@@ -113,15 +116,18 @@ class Dashboard extends React.PureComponent {
}
if (this.isFirstLoad && didLoad && stats.slice_ids.length > 0) {
loadedPaneIds.push(paneId);
+ minQueryStartTime = Math.min(minQueryStartTime, paneMinQueryStart);
}
+
+ // return true if it is loaded, or it's index is not 0
return didLoad || stats.index !== 0;
},
);
- if (allPanesDidLoad && this.isFirstLoad) {
+ if (allVisiblePanesDidLoad && this.isFirstLoad) {
Logger.append(LOG_ACTIONS_FIRST_DASHBOARD_LOAD, {
pane_ids: loadedPaneIds,
- duration: new Date().getTime() - this.ts_mount,
+ duration: new Date().getTime() - minQueryStartTime,
version,
});
Logger.send(this.actionLog);
diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
index 59a9152..9621a49 100644
--- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx
+++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx
@@ -26,6 +26,7 @@ import {
} from '../util/constants';
const TABS_HEIGHT = 47;
+const HEADER_HEIGHT = 67;
const propTypes = {
// redux
@@ -96,52 +97,52 @@ class DashboardBuilder extends React.Component {
<StickyContainer
className={cx('dashboard', editMode && 'dashboard--editing')}
>
- <DragDroppable
- component={dashboardRoot}
- parentComponent={null}
- depth={DASHBOARD_ROOT_DEPTH}
- index={0}
- orientation="column"
- onDrop={handleComponentDrop}
- editMode
- // you cannot drop on/displace tabs if they already exist
- disableDragdrop={!editMode || topLevelTabs}
- >
- {({ dropIndicatorProps }) => (
- <div>
- <DashboardHeader />
- {dropIndicatorProps && <div {...dropIndicatorProps} />}
- </div>
+ <Sticky>
+ {({ style }) => (
+ <DragDroppable
+ component={dashboardRoot}
+ parentComponent={null}
+ depth={DASHBOARD_ROOT_DEPTH}
+ index={0}
+ orientation="column"
+ onDrop={handleComponentDrop}
+ editMode={editMode}
+ // you cannot drop on/displace tabs if they already exist
+ disableDragdrop={!!topLevelTabs}
+ style={{ zIndex: 100, ...style }}
+ >
+ {({ dropIndicatorProps }) => (
+ <div>
+ <DashboardHeader />
+ {dropIndicatorProps && <div {...dropIndicatorProps} />}
+ {topLevelTabs && (
+ <WithPopoverMenu
+ shouldFocus={DashboardBuilder.shouldFocusTabs}
+ menuItems={[
+ <IconButton
+ className="fa fa-level-down"
+ label="Collapse tab content"
+ onClick={this.handleDeleteTopLevelTabs}
+ />,
+ ]}
+ editMode={editMode}
+ >
+ <DashboardComponent
+ id={topLevelTabs.id}
+ parentId={DASHBOARD_ROOT_ID}
+ depth={DASHBOARD_ROOT_DEPTH + 1}
+ index={0}
+ renderTabContent={false}
+ renderHoverMenu={false}
+ onChangeTab={this.handleChangeTab}
+ />
+ </WithPopoverMenu>
+ )}
+ </div>
+ )}
+ </DragDroppable>
)}
- </DragDroppable>
-
- {topLevelTabs && (
- <Sticky topOffset={50}>
- {({ style }) => (
- <WithPopoverMenu
- shouldFocus={DashboardBuilder.shouldFocusTabs}
- menuItems={[
- <IconButton
- className="fa fa-level-down"
- label="Collapse tab content"
- onClick={this.handleDeleteTopLevelTabs}
- />,
- ]}
- editMode={editMode}
- style={{ zIndex: 100, ...style }}
- >
- <DashboardComponent
- id={topLevelTabs.id}
- parentId={DASHBOARD_ROOT_ID}
- depth={DASHBOARD_ROOT_DEPTH + 1}
- index={0}
- renderTabContent={false}
- onChangeTab={this.handleChangeTab}
- />
- </WithPopoverMenu>
- )}
- </Sticky>
- )}
+ </Sticky>
<div className="dashboard-content">
<div className="grid-container">
@@ -187,7 +188,7 @@ class DashboardBuilder extends React.Component {
{this.props.editMode &&
this.props.showBuilderPane && (
<BuilderComponentPane
- topOffset={topLevelTabs ? TABS_HEIGHT : 0}
+ topOffset={HEADER_HEIGHT + (topLevelTabs ? TABS_HEIGHT : 0)}
toggleBuilderPane={this.props.toggleBuilderPane}
/>
)}
diff --git a/superset/assets/src/dashboard/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx
index f5ca6e5..d85015e 100644
--- a/superset/assets/src/dashboard/components/DashboardGrid.jsx
+++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx
@@ -29,6 +29,7 @@ class DashboardGrid extends React.PureComponent {
this.handleResizeStart = this.handleResizeStart.bind(this);
this.handleResize = this.handleResize.bind(this);
this.handleResizeStop = this.handleResizeStop.bind(this);
+ this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
this.getRowGuidePosition = this.getRowGuidePosition.bind(this);
this.setGridRef = this.setGridRef.bind(this);
}
@@ -38,7 +39,7 @@ class DashboardGrid extends React.PureComponent {
return (
resizeRef.getBoundingClientRect().bottom -
this.grid.getBoundingClientRect().top -
- 1
+ 2
);
}
return null;
@@ -75,6 +76,19 @@ class DashboardGrid extends React.PureComponent {
}));
}
+ handleTopDropTargetDrop(dropResult) {
+ if (dropResult) {
+ this.props.handleComponentDrop({
+ ...dropResult,
+ destination: {
+ ...dropResult.destination,
+ // force appending as the first child if top drop target
+ index: 0,
+ },
+ });
+ }
+ }
+
render() {
const {
gridComponent,
@@ -93,6 +107,26 @@ class DashboardGrid extends React.PureComponent {
return width < 100 ? null : (
<div className="dashboard-grid" ref={this.setGridRef}>
<div className="grid-content">
+ {/* make the area above components droppable */}
+ {editMode && (
+ <DragDroppable
+ component={gridComponent}
+ depth={depth}
+ parentComponent={null}
+ index={0}
+ orientation="column"
+ onDrop={this.handleTopDropTargetDrop}
+ className="empty-droptarget"
+ editMode
+ >
+ {({ dropIndicatorProps }) =>
+ dropIndicatorProps && (
+ <div className="drop-indicator drop-indicator--bottom" />
+ )
+ }
+ </DragDroppable>
+ )}
+
{gridComponent.children.map((id, index) => (
<DashboardComponent
key={id}
@@ -117,7 +151,7 @@ class DashboardGrid extends React.PureComponent {
index={gridComponent.children.length}
orientation="column"
onDrop={handleComponentDrop}
- className="empty-grid-droptarget--bottom"
+ className="empty-droptarget"
editMode
>
{({ dropIndicatorProps }) =>
diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx
index 5fa4afe..3b1b6b1 100644
--- a/superset/assets/src/dashboard/components/Header.jsx
+++ b/superset/assets/src/dashboard/components/Header.jsx
@@ -1,12 +1,12 @@
/* eslint-env browser */
import React from 'react';
import PropTypes from 'prop-types';
-import { ButtonGroup, ButtonToolbar } from 'react-bootstrap';
import HeaderActionsDropdown from './HeaderActionsDropdown';
import EditableTitle from '../../components/EditableTitle';
import Button from '../../components/Button';
import FaveStar from '../../components/FaveStar';
+import UndoRedoKeylisteners from './UndoRedoKeylisteners';
import V2PreviewModal from '../deprecated/V2PreviewModal';
import { chartPropShape } from '../util/propShapes';
@@ -58,10 +58,14 @@ class Header extends React.PureComponent {
super(props);
this.state = {
didNotifyMaxUndoHistoryToast: false,
+ emphasizeUndo: false,
+ hightlightRedo: false,
showV2PreviewModal: props.isV2Preview,
};
this.handleChangeText = this.handleChangeText.bind(this);
+ this.handleCtrlZ = this.handleCtrlZ.bind(this);
+ this.handleCtrlY = this.handleCtrlY.bind(this);
this.toggleEditMode = this.toggleEditMode.bind(this);
this.forceRefresh = this.forceRefresh.bind(this);
this.overwriteDashboard = this.overwriteDashboard.bind(this);
@@ -84,6 +88,11 @@ class Header extends React.PureComponent {
}
}
+ componentWillUnmount() {
+ clearTimeout(this.ctrlYTimeout);
+ clearTimeout(this.ctrlZTimeout);
+ }
+
forceRefresh() {
return this.props.fetchCharts(Object.values(this.props.charts), true);
}
@@ -96,6 +105,26 @@ class Header extends React.PureComponent {
}
}
+ handleCtrlY() {
+ this.props.onRedo();
+ this.setState({ emphasizeRedo: true }, () => {
+ if (this.ctrlYTimeout) clearTimeout(this.ctrlYTimeout);
+ this.ctrlYTimeout = setTimeout(() => {
+ this.setState({ emphasizeRedo: false });
+ }, 100);
+ });
+ }
+
+ handleCtrlZ() {
+ this.props.onUndo();
+ this.setState({ emphasizeUndo: true }, () => {
+ if (this.ctrlZTimeout) clearTimeout(this.ctrlZTimeout);
+ this.ctrlZTimeout = setTimeout(() => {
+ this.setState({ emphasizeUndo: false });
+ }, 100);
+ });
+ }
+
toggleEditMode() {
this.props.setEditMode(!this.props.editMode);
}
@@ -183,110 +212,117 @@ class Header extends React.PureComponent {
)}
</div>
- <ButtonToolbar>
- {userCanSaveAs && (
- <ButtonGroup>
- {editMode && (
+ {userCanSaveAs && (
+ <div className="button-container">
+ {editMode && (
+ <Button
+ bsSize="small"
+ onClick={onUndo}
+ disabled={undoLength < 1}
+ bsStyle={this.state.emphasizeUndo ? 'primary' : undefined}
+ >
+ <div title="Undo" className="undo-action fa fa-reply" />
+ </Button>
+ )}
+
+ {editMode && (
+ <Button
+ bsSize="small"
+ onClick={onRedo}
+ disabled={redoLength < 1}
+ bsStyle={this.state.emphasizeRedo ? 'primary' : undefined}
+ >
+ <div title="Redo" className="redo-action fa fa-share" />
+ </Button>
+ )}
+
+ {editMode && (
+ <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
+ {showBuilderPane
+ ? t('Hide components')
+ : t('Insert components')}
+ </Button>
+ )}
+
+ {editMode &&
+ (hasUnsavedChanges || isV2Preview) && (
<Button
bsSize="small"
- onClick={onUndo}
- disabled={undoLength < 1}
+ bsStyle={popButton ? 'primary' : undefined}
+ onClick={this.overwriteDashboard}
>
- <div title="Undo" className="undo-action fa fa-reply" />
+ {isV2Preview
+ ? t('Persist as Dashboard v2')
+ : t('Save changes')}
</Button>
)}
- {editMode && (
+ {!editMode &&
+ isV2Preview && (
<Button
bsSize="small"
- onClick={onRedo}
- disabled={redoLength < 1}
+ onClick={this.toggleEditMode}
+ bsStyle={popButton ? 'primary' : undefined}
+ disabled={!userCanEdit}
>
- <div title="Redo" className="redo-action fa fa-share" />
+ {t('Edit to persist Dashboard v2')}
</Button>
)}
- {editMode && (
- <Button bsSize="small" onClick={this.props.toggleBuilderPane}>
- {showBuilderPane
- ? t('Hide components')
- : t('Insert components')}
+ {!editMode &&
+ !isV2Preview &&
+ !hasUnsavedChanges && (
+ <Button
+ bsSize="small"
+ onClick={this.toggleEditMode}
+ bsStyle={popButton ? 'primary' : undefined}
+ disabled={!userCanEdit}
+ >
+ {t('Edit dashboard')}
+ </Button>
+ )}
+
+ {editMode &&
+ !isV2Preview &&
+ !hasUnsavedChanges && (
+ <Button
+ bsSize="small"
+ onClick={this.toggleEditMode}
+ bsStyle={undefined}
+ disabled={!userCanEdit}
+ >
+ {t('Switch to view mode')}
</Button>
)}
- {editMode &&
- (hasUnsavedChanges || isV2Preview) && (
- <Button
- bsSize="small"
- bsStyle={popButton ? 'primary' : undefined}
- onClick={this.overwriteDashboard}
- >
- {isV2Preview
- ? t('Persist as Dashboard v2')
- : t('Save changes')}
- </Button>
- )}
-
- {!editMode &&
- isV2Preview && (
- <Button
- bsSize="small"
- onClick={this.toggleEditMode}
- bsStyle={popButton ? 'primary' : undefined}
- disabled={!userCanEdit}
- >
- {t('Edit to persist Dashboard v2')}
- </Button>
- )}
-
- {!editMode &&
- !isV2Preview &&
- !hasUnsavedChanges && (
- <Button
- bsSize="small"
- onClick={this.toggleEditMode}
- bsStyle={popButton ? 'primary' : undefined}
- disabled={!userCanEdit}
- >
- {t('Edit dashboard')}
- </Button>
- )}
-
- {editMode &&
- !isV2Preview &&
- !hasUnsavedChanges && (
- <Button
- bsSize="small"
- onClick={this.toggleEditMode}
- bsStyle={undefined}
- disabled={!userCanEdit}
- >
- {t('Switch to view mode')}
- </Button>
- )}
-
- <HeaderActionsDropdown
- addSuccessToast={this.props.addSuccessToast}
- addDangerToast={this.props.addDangerToast}
- dashboardId={dashboardInfo.id}
- dashboardTitle={dashboardTitle}
- layout={layout}
- filters={filters}
- expandedSlices={expandedSlices}
- css={css}
- onSave={onSave}
- onChange={onChange}
- forceRefreshAllCharts={this.forceRefresh}
- startPeriodicRender={this.props.startPeriodicRender}
- updateCss={updateCss}
- editMode={editMode}
- hasUnsavedChanges={hasUnsavedChanges}
- userCanEdit={userCanEdit}
- isV2Preview={isV2Preview}
+ <HeaderActionsDropdown
+ addSuccessToast={this.props.addSuccessToast}
+ addDangerToast={this.props.addDangerToast}
+ dashboardId={dashboardInfo.id}
+ dashboardTitle={dashboardTitle}
+ layout={layout}
+ filters={filters}
+ expandedSlices={expandedSlices}
+ css={css}
+ onSave={onSave}
+ onChange={onChange}
+ forceRefreshAllCharts={this.forceRefresh}
+ startPeriodicRender={this.props.startPeriodicRender}
+ updateCss={updateCss}
+ editMode={editMode}
+ hasUnsavedChanges={hasUnsavedChanges}
+ userCanEdit={userCanEdit}
+ isV2Preview={isV2Preview}
+ />
+
+ {editMode && (
+ <UndoRedoKeylisteners
+ onUndo={this.handleCtrlZ}
+ onRedo={this.handleCtrlY}
/>
- </ButtonGroup>
- )}
- </ButtonToolbar>
+ )}
+ </div>
+ )}
</div>
);
}
diff --git a/superset/assets/src/dashboard/components/SliceAdder.jsx b/superset/assets/src/dashboard/components/SliceAdder.jsx
index ed652c0..f237a06 100644
--- a/superset/assets/src/dashboard/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/components/SliceAdder.jsx
@@ -8,6 +8,7 @@ import SearchInput, { createFilter } from 'react-search-input';
import AddSliceCard from './AddSliceCard';
import AddSliceDragPreview from './dnd/AddSliceDragPreview';
import DragDroppable from './dnd/DragDroppable';
+import Loading from '../../components/Loading';
import { CHART_TYPE, NEW_COMPONENT_SOURCE_TYPE } from '../util/componentTypes';
import { NEW_CHART_ID, NEW_COMPONENTS_SOURCE_ID } from '../util/constants';
import { slicePropShape } from '../util/propShapes';
@@ -222,13 +223,7 @@ class SliceAdder extends React.Component {
</DropdownButton>
</div>
- {this.props.isLoading && (
- <img
- src="/static/assets/images/loading.gif"
- className="loading"
- alt="loading"
- />
- )}
+ {this.props.isLoading && <Loading />}
{this.props.errorMessage && <div>{this.props.errorMessage}</div>}
diff --git a/superset/assets/src/dashboard/components/UndoRedoKeylisteners.jsx b/superset/assets/src/dashboard/components/UndoRedoKeylisteners.jsx
new file mode 100644
index 0000000..5af0934
--- /dev/null
+++ b/superset/assets/src/dashboard/components/UndoRedoKeylisteners.jsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ onUndo: PropTypes.func.isRequired,
+ onRedo: PropTypes.func.isRequired,
+};
+
+class UndoRedoKeylisteners extends React.PureComponent {
+ constructor(props) {
+ super(props);
+ this.handleKeydown = this.handleKeydown.bind(this);
+ }
+
+ componentDidMount() {
+ document.addEventListener('keydown', this.handleKeydown);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('keydown', this.handleKeydown);
+ }
+
+ handleKeydown(event) {
+ const controlOrCommand = event.keyCode === 90 || event.metaKey;
+ if (controlOrCommand) {
+ const isZChar = event.key === 'z' || event.keyCode === 90;
+ const isYChar = event.key === 'y' || event.keyCode === 89;
+ const isEditingMarkdown = document.querySelector(
+ '.dashboard-markdown--editing',
+ );
+
+ if (!isEditingMarkdown && (isZChar || isYChar)) {
+ event.preventDefault();
+ const func = isZChar ? this.props.onUndo : this.props.onRedo;
+ func();
+ }
+ }
+ }
+
+ render() {
+ return null;
+ }
+}
+
+UndoRedoKeylisteners.propTypes = propTypes;
+
+export default UndoRedoKeylisteners;
diff --git a/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx b/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
index 91fc055..2c1128e 100644
--- a/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
+++ b/superset/assets/src/dashboard/components/dnd/AddSliceDragPreview.jsx
@@ -11,11 +11,11 @@ import {
const staticCardStyles = {
position: 'fixed',
- background: 'white',
+ background: 'rgba(255, 255, 255, 0.7)',
pointerEvents: 'none',
top: 0,
left: 0,
- zIndex: 100,
+ zIndex: 101, // this should be higher than top-level tabs
width: 376 - 2 * 16,
};
diff --git a/superset/assets/src/dashboard/components/dnd/handleHover.js b/superset/assets/src/dashboard/components/dnd/handleHover.js
index cb98a6f..a3b16aa 100644
--- a/superset/assets/src/dashboard/components/dnd/handleHover.js
+++ b/superset/assets/src/dashboard/components/dnd/handleHover.js
@@ -1,7 +1,7 @@
import throttle from 'lodash.throttle';
import getDropPosition from '../../util/getDropPosition';
-const HOVER_THROTTLE_MS = 150;
+const HOVER_THROTTLE_MS = 100;
function handleHover(props, monitor, Component) {
// this may happen due to throttling
diff --git a/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx b/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
index a49a893..bd639e7 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Markdown.jsx
@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactMarkdown from 'react-markdown';
+import cx from 'classnames';
import AceEditor from 'react-ace';
import 'brace/mode/markdown';
import 'brace/theme/textmate';
@@ -138,6 +139,7 @@ class Markdown extends React.PureComponent {
onChange={this.handleMarkdownChange}
width="100%"
height="100%"
+ showGutter={false}
editorProps={{ $blockScrolling: true }}
value={
// thisl allows "select all => delete" to give an empty editor
@@ -183,6 +185,8 @@ class Markdown extends React.PureComponent {
? parentComponent.meta.width || GRID_MIN_COLUMN_COUNT
: component.meta.width || GRID_MIN_COLUMN_COUNT;
+ const isEditing = this.state.editorMode === 'edit';
+
return (
<DragDroppable
component={component}
@@ -207,7 +211,12 @@ class Markdown extends React.PureComponent {
]}
editMode={editMode}
>
- <div className="dashboard-markdown">
+ <div
+ className={cx(
+ 'dashboard-markdown',
+ isEditing && 'dashboard-markdown--editing',
+ )}
+ >
<ResizableContainer
id={component.id}
adjustableWidth={parentComponent.type === ROW_TYPE}
@@ -230,7 +239,7 @@ class Markdown extends React.PureComponent {
ref={dragSourceRef}
className="dashboard-component dashboard-component-chart-holder"
>
- {editMode && this.state.editorMode === 'edit'
+ {editMode && isEditing
? this.renderEditMode()
: this.renderPreviewMode()}
</div>
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
index 4cba2e6..b91d808 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
@@ -57,6 +57,7 @@ export default class Tab extends React.PureComponent {
this.handleChangeText = this.handleChangeText.bind(this);
this.handleDeleteComponent = this.handleDeleteComponent.bind(this);
this.handleDrop = this.handleDrop.bind(this);
+ this.handleTopDropTargetDrop = this.handleTopDropTargetDrop.bind(this);
}
handleChangeFocus(nextFocus) {
@@ -89,21 +90,53 @@ export default class Tab extends React.PureComponent {
this.props.onDropOnTab(dropResult);
}
+ handleTopDropTargetDrop(dropResult) {
+ if (dropResult) {
+ this.props.handleComponentDrop({
+ ...dropResult,
+ destination: {
+ ...dropResult.destination,
+ // force appending as the first child if top drop target
+ index: 0,
+ },
+ });
+ }
+ }
+
renderTabContent() {
const {
component: tabComponent,
parentComponent: tabParentComponent,
- index,
depth,
availableColumnCount,
columnWidth,
onResizeStart,
onResize,
onResizeStop,
+ editMode,
} = this.props;
return (
<div className="dashboard-component-tabs-content">
+ {/* Make top of tab droppable */}
+ {editMode && (
+ <DragDroppable
+ component={tabComponent}
+ parentComponent={tabParentComponent}
+ orientation="column"
+ index={0}
+ depth={depth}
+ onDrop={this.handleTopDropTargetDrop}
+ editMode
+ className="empty-droptarget"
+ >
+ {({ dropIndicatorProps }) =>
+ dropIndicatorProps && (
+ <div className="drop-indicator drop-indicator--top" />
+ )
+ }
+ </DragDroppable>
+ )}
{tabComponent.children.map((componentId, componentIndex) => (
<DashboardComponent
key={componentId}
@@ -119,21 +152,21 @@ export default class Tab extends React.PureComponent {
onResizeStop={onResizeStop}
/>
))}
- {/* Make the content of the tab component droppable in the case that there are no children */}
- {tabComponent.children.length === 0 && (
+ {/* Make bottom of tab droppable */}
+ {editMode && (
<DragDroppable
component={tabComponent}
parentComponent={tabParentComponent}
orientation="column"
- index={index}
+ index={tabComponent.children.length}
depth={depth}
onDrop={this.handleDrop}
editMode
- className="empty-tab-droptarget"
+ className="empty-droptarget"
>
{({ dropIndicatorProps }) =>
dropIndicatorProps && (
- <div className="drop-indicator drop-indicator--top" />
+ <div className="drop-indicator drop-indicator--bottom" />
)
}
</DragDroppable>
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
index 813961d..01c0e60 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx
@@ -24,6 +24,7 @@ const propTypes = {
depth: PropTypes.number.isRequired,
renderTabContent: PropTypes.bool, // whether to render tabs + content or just tabs
editMode: PropTypes.bool.isRequired,
+ renderHoverMenu: PropTypes.bool,
// grid related
availableColumnCount: PropTypes.number,
@@ -43,6 +44,7 @@ const propTypes = {
const defaultProps = {
children: null,
renderTabContent: true,
+ renderHoverMenu: true,
availableColumnCount: 0,
columnWidth: 0,
onChangeTab() {},
@@ -132,6 +134,7 @@ class Tabs extends React.PureComponent {
onResizeStop,
handleComponentDrop,
renderTabContent,
+ renderHoverMenu,
editMode,
} = this.props;
@@ -153,19 +156,20 @@ class Tabs extends React.PureComponent {
dragSourceRef: tabsDragSourceRef,
}) => (
<div className="dashboard-component dashboard-component-tabs">
- {editMode && (
- <HoverMenu innerRef={tabsDragSourceRef} position="left">
- <DragHandle position="left" />
- <DeleteComponentButton onDelete={this.handleDeleteComponent} />
- </HoverMenu>
- )}
+ {editMode &&
+ renderHoverMenu && (
+ <HoverMenu innerRef={tabsDragSourceRef} position="left">
+ <DragHandle position="left" />
+ <DeleteComponentButton
+ onDelete={this.handleDeleteComponent}
+ />
+ </HoverMenu>
+ )}
<BootstrapTabs
id={tabsComponent.id}
activeKey={selectedTabIndex}
onSelect={this.handleClickTab}
- // these are important for performant loading of tabs. also, there is a
- // react-bootstrap bug where mountOnEnter has no effect unless animation=true
animation
mountOnEnter
unmountOnExit={false}
diff --git a/superset/assets/src/dashboard/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
index 29071cb..6f5b2e0 100644
--- a/superset/assets/src/dashboard/containers/DashboardComponent.jsx
+++ b/superset/assets/src/dashboard/containers/DashboardComponent.jsx
@@ -4,10 +4,9 @@ import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import ComponentLookup from '../components/gridComponents';
-import getTotalChildWidth from '../util/getChildWidth';
+import getDetailedComponentWidth from '../util/getDetailedComponentWidth';
import { componentShape } from '../util/propShapes';
import { COLUMN_TYPE, ROW_TYPE } from '../util/componentTypes';
-import { GRID_MIN_COLUMN_COUNT } from '../util/constants';
import {
createComponent,
@@ -40,23 +39,15 @@ function mapStateToProps(
// rows and columns need more data about their child dimensions
// doing this allows us to not pass the entire component lookup to all Components
- if (props.component.type === ROW_TYPE) {
- props.occupiedColumnCount = getTotalChildWidth({
+ const componentType = component.type;
+ if (componentType === ROW_TYPE || componentType === COLUMN_TYPE) {
+ const { occupiedWidth, minimumWidth } = getDetailedComponentWidth({
id,
components: dashboardLayout,
});
- } else if (props.component.type === COLUMN_TYPE) {
- props.minColumnWidth = GRID_MIN_COLUMN_COUNT;
- component.children.forEach(childId => {
- // rows don't have widths, so find the width of its children
- if (dashboardLayout[childId].type === ROW_TYPE) {
- props.minColumnWidth = Math.max(
- props.minColumnWidth,
- getTotalChildWidth({ id: childId, components: dashboardLayout }),
- );
- }
- });
+ if (componentType === ROW_TYPE) props.occupiedColumnCount = occupiedWidth;
+ if (componentType === COLUMN_TYPE) props.minColumnWidth = minimumWidth;
}
return props;
diff --git a/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx b/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx
index 6c2f624..3d3e468 100644
--- a/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx
+++ b/superset/assets/src/dashboard/deprecated/v1/components/SliceAdder.jsx
@@ -3,6 +3,7 @@ import $ from 'jquery';
import PropTypes from 'prop-types';
import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
+import Loading from '../../../../components/Loading';
import ModalTrigger from '../../../../components/ModalTrigger';
import { t } from '../../../../locales';
@@ -130,11 +131,7 @@ class SliceAdder extends React.Component {
}
const modalContent = (
<div>
- <img
- src="/static/assets/images/loading.gif"
- className={'loading ' + (hideLoad ? 'hidden' : '')}
- alt={hideLoad ? '' : 'loading'}
- />
+ {!hideLoad && <Loading />}
<div className={this.errored ? '' : 'hidden'}>
{this.state.errorMsg}
</div>
diff --git a/superset/assets/src/dashboard/reducers/dashboardLayout.js b/superset/assets/src/dashboard/reducers/dashboardLayout.js
index 396a56c..51cd02a 100644
--- a/superset/assets/src/dashboard/reducers/dashboardLayout.js
+++ b/superset/assets/src/dashboard/reducers/dashboardLayout.js
@@ -3,7 +3,9 @@ import {
DASHBOARD_GRID_ID,
NEW_COMPONENTS_SOURCE_ID,
} from '../util/constants';
+import componentIsResizable from '../util/componentIsResizable';
import findParentId from '../util/findParentId';
+import getComponentWidthFromDrop from '../util/getComponentWidthFromDrop';
import newComponentFactory from '../util/newComponentFactory';
import newEntitiesFromDrop from '../util/newEntitiesFromDrop';
import reorderItem from '../util/dnd-reorder';
@@ -104,6 +106,24 @@ const actionHandlers = {
destination,
});
+ if (componentIsResizable(nextEntities[dragging.id])) {
+ // update component width if it changed
+ const nextWidth =
+ getComponentWidthFromDrop({
+ dropResult,
+ layout: state,
+ }) || undefined; // don't set a 0 width
+ if ((nextEntities[dragging.id].meta || {}).width !== nextWidth) {
+ nextEntities[dragging.id] = {
+ ...nextEntities[dragging.id],
+ meta: {
+ ...nextEntities[dragging.id].meta,
+ width: nextWidth,
+ },
+ };
+ }
+ }
+
// wrap the dragged component in a row depending on destination type
const wrapInRow = shouldWrapChildInRow({
parentType: destination.type,
diff --git a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
index 6250243..e807bcc 100644
--- a/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
+++ b/superset/assets/src/dashboard/stylesheets/builder-sidepane.less
@@ -2,7 +2,6 @@
flex: 0 0 @builder-pane-width;
z-index: 10;
position: relative;
- box-shadow: -4px 0 4px 0 rgba(0, 0, 0, 0.1);
.dashboard-builder-sidepane-header {
font-size: 15px;
@@ -34,13 +33,14 @@
overflow: hidden;
width: @builder-pane-width;
height: 100%;
+ box-shadow: -4px 0 4px 0 rgba(0, 0, 0, 0.1);
}
.slider-container {
position: absolute;
background: white;
width: @builder-pane-width * 2;
- height: 100%;
+ height: 100vh;
display: flex;
transition: all 0.5s ease;
diff --git a/superset/assets/src/dashboard/stylesheets/components/chart.less b/superset/assets/src/dashboard/stylesheets/components/chart.less
index 73914fb..bc0005d 100644
--- a/superset/assets/src/dashboard/stylesheets/components/chart.less
+++ b/superset/assets/src/dashboard/stylesheets/components/chart.less
@@ -31,13 +31,14 @@
.resizable-container:hover
> .dashboard-component-chart-holder:after,
.dashboard--editing .dashboard-component-chart-holder:hover:after {
- border: 1px solid @gray-light;
+ border: 1px dashed @indicator-color;
+ z-index: 2;
}
.dashboard--editing
.resizable-container.resizable-container--resizing:hover
> .dashboard-component-chart-holder:after {
- border: 1px solid @indicator-color;
+ border: 1px dashed @indicator-color;
}
.dashboard--editing
@@ -62,3 +63,38 @@
/* disable chart interactions in edit mode */
pointer-events: none;
}
+
+.slice-header-controls-trigger {
+ padding: 0 16px;
+ position: absolute;
+ top: 0;
+ right: -16px; //increase the click-able area for the button
+
+ &:hover {
+ cursor: pointer;
+ }
+}
+
+.dot {
+ height: 4px;
+ width: 4px;
+ background-color: @gray;
+ border-radius: 50%;
+ margin: 2px 0;
+ display: inline-block;
+
+ .is-cached & {
+ background-color: @pink;
+ box-shadow: 0 0 5px 1.5px rgba(255, 0, 0, 0.3);
+ }
+
+ .vertical-dots-container & {
+ display: block;
+ }
+
+ a[role='menuitem'] & {
+ width: 8px;
+ height: 8px;
+ margin-right: 8px;
+ }
+}
diff --git a/superset/assets/src/dashboard/stylesheets/components/column.less b/superset/assets/src/dashboard/stylesheets/components/column.less
index 2f26d95..e923f8c 100644
--- a/superset/assets/src/dashboard/stylesheets/components/column.less
+++ b/superset/assets/src/dashboard/stylesheets/components/column.less
@@ -24,12 +24,16 @@
.resizable-container.resizable-container--resizing:hover
> .grid-column:after,
.dashboard--editing .hover-menu:hover + .grid-column:after {
+ border: 1px dashed @indicator-color;
+ z-index: 2;
+}
+
+.dashboard--editing .grid-column:after {
border: 1px dashed @gray-light;
- box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
}
.grid-column--empty {
- min-height: 72px;
+ min-height: 100px;
}
.grid-column--empty:before {
diff --git a/superset/assets/src/dashboard/stylesheets/components/header.less b/superset/assets/src/dashboard/stylesheets/components/header.less
index 9403103..71b2176 100644
--- a/superset/assets/src/dashboard/stylesheets/components/header.less
+++ b/superset/assets/src/dashboard/stylesheets/components/header.less
@@ -6,15 +6,28 @@
color: @almost-black;
}
+.dashboard--editing .dashboard-grid .dashboard-component-header:after {
+ border: 1px dashed transparent;
+ content: '';
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ pointer-events: none;
+}
+
+.dashboard--editing .dashboard-grid .dashboard-component-header:hover:after {
+ border: 1px dashed @indicator-color;
+ z-index: 2;
+}
+
.dashboard-header .dashboard-component-header {
font-weight: 300;
width: auto;
}
-.dashboard-header .btn-group button {
- margin-right: 8px;
-}
-
.dashboard-header .undo-action,
.dashboard-header .redo-action {
line-height: 18px;
@@ -25,6 +38,11 @@
cursor: move;
}
+.header-style-option {
+ font-weight: 700;
+ color: @almost-black;
+}
+
/* note: sizes should be a multiple of the 8px grid unit so that rows in the grid align */
.header-small {
font-size: 16px;
diff --git a/superset/assets/src/dashboard/stylesheets/components/markdown.less b/superset/assets/src/dashboard/stylesheets/components/markdown.less
index 2cfd929..87974d9 100644
--- a/superset/assets/src/dashboard/stylesheets/components/markdown.less
+++ b/superset/assets/src/dashboard/stylesheets/components/markdown.less
@@ -14,3 +14,12 @@
border: none;
}
}
+
+/* maximize editing space */
+.dashboard-markdown--editing {
+ .dashboard-component-chart-holder {
+ .with-popover-menu--focused & {
+ padding: 1px;
+ }
+ }
+}
diff --git a/superset/assets/src/dashboard/stylesheets/components/row.less b/superset/assets/src/dashboard/stylesheets/components/row.less
index 382417e..06d98e6 100644
--- a/superset/assets/src/dashboard/stylesheets/components/row.less
+++ b/superset/assets/src/dashboard/stylesheets/components/row.less
@@ -32,13 +32,18 @@
> .grid-row:after,
.dashboard--editing .hover-menu:hover + .grid-row:after,
.dashboard--editing .dashboard-component-tabs > .hover-menu:hover + div:after {
+ border: 1px dashed @indicator-color;
+ z-index: 2;
+}
+
+.dashboard--editing .grid-row:after,
+.dashboard--editing .dashboard-component-tabs > .hover-menu + div:after {
border: 1px dashed @gray-light;
- box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.1);
}
.grid-row.grid-row--empty {
align-items: center; /* this centers the empty note content */
- height: 80px;
+ height: 100px;
}
.grid-row--empty:before {
diff --git a/superset/assets/src/dashboard/stylesheets/components/tabs.less b/superset/assets/src/dashboard/stylesheets/components/tabs.less
index b1124da..cee524c 100644
--- a/superset/assets/src/dashboard/stylesheets/components/tabs.less
+++ b/superset/assets/src/dashboard/stylesheets/components/tabs.less
@@ -1,89 +1,84 @@
.dashboard-component-tabs {
width: 100%;
background-color: white;
-}
-
-.dashboard-component-tabs .dashboard-component-tabs-content {
- min-height: 48px;
- margin-top: 1px;
-}
-
-.dashboard-component-tabs-content .empty-tab-droptarget {
- min-height: 24px;
-}
-
-.dashboard-component-tabs .nav-tabs {
- border-bottom: none;
-}
-
-/* by moving padding from <a/> to <li/> we can restrict the selected tab indicator to text width */
-.dashboard-component-tabs .nav-tabs > li {
- margin: 0 16px;
-}
-
-.dashboard-component-tabs .nav-tabs > li > a {
- color: @almost-black;
- border: none;
- padding: 12px 0 14px 0;
- font-size: 15px;
- margin-right: 0;
-}
-
-.dashboard-component-tabs .nav-tabs > li.active > a {
- border: none;
-}
-
-.dashboard-component-tabs .nav-tabs > li.active > a:after {
- content: '';
- position: absolute;
- height: 3px;
- width: 100%;
- bottom: 0;
- background: linear-gradient(to right, #e32464, #2c2261);
-}
-
-.dashboard-component-tabs .nav-tabs > li > a:hover {
- border: none;
- background: inherit;
- color: @almost-black;
-}
-
-.dashboard-component-tabs .nav-tabs > li > a:focus {
- outline: none;
- background: #fff;
-}
-
-.dashboard-component-tabs .nav-tabs > li .dragdroppable-tab {
- cursor: move;
-}
-
-/* These expande the outline border + drop indicator for tabs */
-.dashboard-component-tabs .nav-tabs > li .drop-indicator {
- top: -12px !important;
- height: ~'calc(100% + 24px)' !important;
-}
-
-.dashboard-component-tabs .nav-tabs > li .drop-indicator--left {
- left: -12px !important;
-}
-
-.dashboard-component-tabs .nav-tabs > li .drop-indicator--right {
- right: -12px !important;
-}
-
-.dashboard-component-tabs .nav-tabs > li .drop-indicator--top,
-.dashboard-component-tabs .nav-tabs > li .drop-indicator--bottom {
- left: -12px !important;
- width: ~'calc(100% + 24px)' !important; /* escape for .less */
- opacity: 0.4;
-}
-
-.dashboard-component-tabs li .fa-plus {
- color: @gray-dark;
- font-size: 14px;
- margin-top: 3px;
-}
-.dashboard-component-tabs li .editable-title input[type='button'] {
- cursor: pointer;
+ & .nav-tabs {
+ border-bottom: none;
+
+ /* by moving padding from <a/> to <li/> we can restrict the selected tab indicator to text width */
+ & > li {
+ margin: 0 16px;
+
+ & > a {
+ color: @almost-black;
+ border: none;
+ padding: 12px 0 14px 0;
+ font-size: 15px;
+ margin-right: 0;
+ }
+
+ & > a:hover {
+ border: none;
+ background: inherit;
+ color: @almost-black;
+ }
+
+ & > a:focus {
+ outline: none;
+ background: #fff;
+ }
+
+ & .dragdroppable-tab {
+ cursor: move;
+ }
+
+ & .drop-indicator {
+ top: -12px !important;
+ height: ~'calc(100% + 24px)' !important;
+ }
+
+ & .drop-indicator--left {
+ left: -12px !important;
+ }
+ & .drop-indicator--right {
+ right: -12px !important;
+ }
+
+ & .drop-indicator--bottom,
+ & .drop-indicator--top {
+ left: -12px !important;
+ width: ~'calc(100% + 24px)' !important; /* escape for .less */
+ opacity: 0.4;
+ }
+
+ & .fa-plus {
+ color: @gray-dark;
+ font-size: 14px;
+ margin-top: 3px;
+ }
+
+ & .editable-title input[type='button'] {
+ cursor: pointer;
+ }
+ }
+
+ & li.active > a {
+ border: none;
+ }
+
+ & li.active > a:after {
+ content: '';
+ position: absolute;
+ height: 3px;
+ width: 100%;
+ bottom: 0;
+ background: linear-gradient(to right, #e32464, #2c2261);
+ }
+ }
+
+ & .dashboard-component-tabs-content {
+ min-height: 48px;
+ margin-top: 1px;
+ position: relative;
+ }
}
diff --git a/superset/assets/src/dashboard/stylesheets/dashboard.less b/superset/assets/src/dashboard/stylesheets/dashboard.less
index 3db5cdc..92f1ff1 100644
--- a/superset/assets/src/dashboard/stylesheets/dashboard.less
+++ b/superset/assets/src/dashboard/stylesheets/dashboard.less
@@ -78,7 +78,7 @@ body {
.dashboard .dashboard-header {
#save-dash-split-button {
border-radius: 0;
- margin-left: -8px;
+ margin-left: -9px;
height: 30px;
width: 30px;
z-index: 10;
@@ -97,6 +97,15 @@ body {
min-width: unset;
}
}
+
+ .button-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ & > :not(:last-child) {
+ margin-right: 8px;
+ }
+ }
}
.dashboard .chart-header,
@@ -120,40 +129,6 @@ body {
}
}
-.slice-header-controls-trigger {
- padding: 0 16px;
- position: absolute;
- top: 0;
- right: -16px; //increase the click-able area for the button
-
- &:hover {
- cursor: pointer;
- }
-}
-
-.dot {
- height: 4px;
- width: 4px;
- background-color: @gray;
- border-radius: 50%;
- margin: 2px 0;
- display: inline-block;
-
- .is-cached & {
- background-color: @pink;
- }
-
- .vertical-dots-container & {
- display: block;
- }
-
- a[role='menuitem'] & {
- width: 8px;
- height: 8px;
- margin-right: 8px;
- }
-}
-
.modal img.loading {
width: 50px;
margin: 0;
@@ -205,6 +180,10 @@ body {
line-height: 1em;
cursor: pointer;
opacity: 0.9;
+ flex-wrap: nowrap;
+ display: flex;
+ align-items: center;
+ white-space: nowrap;
&:hover {
opacity: 1;
diff --git a/superset/assets/src/dashboard/stylesheets/dnd.less b/superset/assets/src/dashboard/stylesheets/dnd.less
index 0a10c61..9b5ea89 100644
--- a/superset/assets/src/dashboard/stylesheets/dnd.less
+++ b/superset/assets/src/dashboard/stylesheets/dnd.less
@@ -3,7 +3,7 @@
}
.dragdroppable--dragging {
- opacity: 0.15;
+ opacity: 0.2;
}
.dragdroppable-row {
@@ -76,3 +76,36 @@
margin: -1px;
width: 2px;
}
+
+/* empty drop targets */
+.dashboard-component-tabs-content {
+ & > .empty-droptarget {
+ position: absolute;
+ width: 100%;
+ }
+
+ & > .empty-droptarget:first-child {
+ height: 16px;
+ top: -8px;
+ z-index: 10;
+ }
+
+ & > .empty-droptarget:last-child {
+ height: 12px;
+ bottom: 0px;
+ }
+}
+
+.grid-content {
+ /* note we don't do a :last-child selection because
+ assuming bottom empty-droptarget is last child is fragile */
+ & > .empty-droptarget {
+ width: 100%;
+ height: 100%;
+ }
+
+ & > .empty-droptarget:first-child {
+ height: 24px;
+ margin-top: -24px;
+ }
+}
diff --git a/superset/assets/src/dashboard/stylesheets/grid.less b/superset/assets/src/dashboard/stylesheets/grid.less
index 9d09ac7..1e22e1d 100644
--- a/superset/assets/src/dashboard/stylesheets/grid.less
+++ b/superset/assets/src/dashboard/stylesheets/grid.less
@@ -1,5 +1,4 @@
.grid-container {
- min-height: 100%;
position: relative;
margin: 24px;
/* without this, the grid will not get smaller upon toggling the builder panel on */
@@ -7,33 +6,21 @@
width: 100%;
}
-/* this is the ParentSize wrapper */
+/* this is the ParentSize wrapper */
.grid-container > div:first-child {
height: inherit !important;
}
.grid-content {
- min-height: 100%;
display: flex;
flex-direction: column;
- margin-bottom: 100px;
}
/* gutters between rows */
-.grid-content
- > div:not(:only-child):not(:last-child):not(.empty-grid-droptarget--bottom):not(.empty-grid-droptarget--top) {
+.grid-content > div:not(:only-child):not(:last-child):not(.empty-droptarget) {
margin-bottom: 16px;
}
-.grid-content > .empty-grid-droptarget--top {
- height: 24px;
- margin-top: -24px;
-}
-.empty-grid-droptarget--bottom {
- width: 100%;
- height: 100%;
-}
-
/* Editing guides */
.grid-column-guide {
position: absolute;
diff --git a/superset/assets/src/dashboard/stylesheets/popover-menu.less b/superset/assets/src/dashboard/stylesheets/popover-menu.less
index 0c70f58..3c790e4 100644
--- a/superset/assets/src/dashboard/stylesheets/popover-menu.less
+++ b/superset/assets/src/dashboard/stylesheets/popover-menu.less
@@ -85,7 +85,7 @@
.hover-dropdown li.dropdown-item:hover a,
.popover-menu li.dropdown-item:hover a {
- background: @gray-light;
+ background: @menu-hover;
}
.popover-dropdown .caret {
@@ -96,7 +96,7 @@
.hover-dropdown li.dropdown-item.active a,
.popover-menu li.dropdown-item.active a {
- background: white;
+ background: @gray-light;
font-weight: bold;
color: @almost-black;
}
@@ -125,6 +125,7 @@
border: 1px solid @gray-light;
}
+/* Create the transparent rect icon */
.background-style-option.background--transparent:before {
background-image: linear-gradient(45deg, @gray 25%, transparent 25%),
linear-gradient(-45deg, @gray 25%, transparent 25%),
diff --git a/superset/assets/src/dashboard/util/dropOverflowsParent.js b/superset/assets/src/dashboard/util/dropOverflowsParent.js
index 328d8e3..a5f99f3 100644
--- a/superset/assets/src/dashboard/util/dropOverflowsParent.js
+++ b/superset/assets/src/dashboard/util/dropOverflowsParent.js
@@ -1,45 +1,6 @@
-import { COLUMN_TYPE } from '../util/componentTypes';
-import { GRID_COLUMN_COUNT, NEW_COMPONENTS_SOURCE_ID } from './constants';
-import findParentId from './findParentId';
-import getChildWidth from './getChildWidth';
-import newComponentFactory from './newComponentFactory';
+import getComponentWidthFromDrop from './getComponentWidthFromDrop';
export default function doesChildOverflowParent(dropResult, layout) {
- const { source, destination, dragging } = dropResult;
-
- // moving a component within a container should never overflow
- if (source.id === destination.id) {
- return false;
- }
-
- const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
- const grandparentId = findParentId({
- childId: destination.id,
- layout,
- });
-
- const child = isNewComponent
- ? newComponentFactory(dragging.type)
- : layout[dragging.id] || {};
- const parent = layout[destination.id] || {};
- const grandparent = layout[grandparentId] || {};
-
- const childWidth = (child.meta && child.meta.width) || 0;
-
- const grandparentCapacity =
- grandparent.meta && typeof grandparent.meta.width === 'number'
- ? grandparent.meta.width
- : GRID_COLUMN_COUNT;
-
- const parentCapacity =
- parent.meta && typeof parent.meta.width === 'number'
- ? parent.meta.width
- : grandparentCapacity;
-
- const occupiedParentWidth =
- parent.type === COLUMN_TYPE
- ? 0
- : getChildWidth({ id: destination.id, components: layout });
-
- return parentCapacity < occupiedParentWidth + childWidth;
+ const childWidth = getComponentWidthFromDrop({ dropResult, layout });
+ return typeof childWidth === 'number' && childWidth < 0;
}
diff --git a/superset/assets/src/dashboard/util/findParentId.js b/superset/assets/src/dashboard/util/findParentId.js
index 9e47bf2..bf26d54 100644
--- a/superset/assets/src/dashboard/util/findParentId.js
+++ b/superset/assets/src/dashboard/util/findParentId.js
@@ -1,4 +1,4 @@
-export default function findParentId({ childId, layout = {} }) {
+function findParentId({ childId, layout = {} }) {
let parentId = null;
const ids = Object.keys(layout);
@@ -17,3 +17,15 @@ export default function findParentId({ childId, layout = {} }) {
return parentId;
}
+
+const cache = {};
+export default function findParentIdWithCache({ childId, layout = {} }) {
+ if (cache[childId]) {
+ const lastParent = layout[cache[childId]] || {};
+ if (lastParent.children && lastParent.children.includes(childId)) {
+ return lastParent.id;
+ }
+ }
+ cache[childId] = findParentId({ childId, layout });
+ return cache[childId];
+}
diff --git a/superset/assets/src/dashboard/util/getChildWidth.js b/superset/assets/src/dashboard/util/getChildWidth.js
deleted file mode 100644
index 69d2792..0000000
--- a/superset/assets/src/dashboard/util/getChildWidth.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export default function getTotalChildWidth({ id, components }) {
- const component = components[id];
- if (!component) return 0;
-
- let width = 0;
-
- (component.children || []).forEach(childId => {
- const child = components[childId] || {};
- width += (child.meta || {}).width || 0;
- });
-
- return width;
-}
diff --git a/superset/assets/src/dashboard/util/getComponentWidthFromDrop.js b/superset/assets/src/dashboard/util/getComponentWidthFromDrop.js
new file mode 100644
index 0000000..38c7c5a
--- /dev/null
+++ b/superset/assets/src/dashboard/util/getComponentWidthFromDrop.js
@@ -0,0 +1,57 @@
+import { NEW_COMPONENTS_SOURCE_ID } from './constants';
+import findParentId from './findParentId';
+import getDetailedComponentWidth from './getDetailedComponentWidth';
+import newComponentFactory from './newComponentFactory';
+
+export default function getComponentWidthFromDrop({
+ dropResult,
+ layout: components,
+}) {
+ const { source, destination, dragging } = dropResult;
+
+ const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID;
+ const component = isNewComponent
+ ? newComponentFactory(dragging.type)
+ : components[dragging.id] || {};
+
+ // moving a component within the same container shouldn't change its width
+ if (source.id === destination.id) {
+ return component.meta.width;
+ }
+
+ const draggingWidth = getDetailedComponentWidth({
+ component,
+ components,
+ });
+
+ const destinationWidth = getDetailedComponentWidth({
+ id: destination.id,
+ components,
+ });
+
+ let destinationCapacity =
+ destinationWidth.width - destinationWidth.occupiedWidth;
+
+ if (isNaN(destinationCapacity)) {
+ const grandparentWidth = getDetailedComponentWidth({
+ id: findParentId({
+ childId: destination.id,
+ layout: components,
+ }),
+ components,
+ });
+
+ destinationCapacity =
+ grandparentWidth.width - grandparentWidth.occupiedWidth;
+ }
+
+ if (isNaN(destinationCapacity) || isNaN(draggingWidth.width)) {
+ return draggingWidth.width;
+ } else if (destinationCapacity >= draggingWidth.width) {
+ return draggingWidth.width;
+ } else if (destinationCapacity >= draggingWidth.minimumWidth) {
+ return destinationCapacity;
+ }
+
+ return -1;
+}
diff --git a/superset/assets/src/dashboard/util/getDetailedComponentWidth.js b/superset/assets/src/dashboard/util/getDetailedComponentWidth.js
new file mode 100644
index 0000000..ee3096d
--- /dev/null
+++ b/superset/assets/src/dashboard/util/getDetailedComponentWidth.js
@@ -0,0 +1,76 @@
+import findParentId from './findParentId';
+import { GRID_MIN_COLUMN_COUNT, GRID_COLUMN_COUNT } from './constants';
+import {
+ ROW_TYPE,
+ COLUMN_TYPE,
+ MARKDOWN_TYPE,
+ CHART_TYPE,
+} from './componentTypes';
+
+function getTotalChildWidth({ id, components }) {
+ const component = components[id];
+ if (!component) return 0;
+
+ let width = 0;
+
+ (component.children || []).forEach(childId => {
+ const child = components[childId] || {};
+ width += (child.meta || {}).width || 0;
+ });
+
+ return width;
+}
+
+export default function getDetailedComponentWidth({
+ // pass either an id, or a component
+ id,
+ component: passedComponent,
+ components = {},
+}) {
+ const result = {
+ width: undefined,
+ occupiedWidth: undefined,
+ minimumWidth: undefined,
+ };
+
+ const component = passedComponent || components[id];
+ if (!component) return result;
+
+ // note these remain as undefined if the component has no defined width
+ result.width = (component.meta || {}).width;
+ result.occupiedWidth = result.width;
+
+ if (component.type === ROW_TYPE) {
+ // not all rows have width 12, e
+ result.width =
+ getDetailedComponentWidth({
+ id: findParentId({
+ childId: component.id,
+ layout: components,
+ }),
+ components,
+ }).width || GRID_COLUMN_COUNT;
+ result.occupiedWidth = getTotalChildWidth({ id: component.id, components });
+ result.minimumWidth = result.occupiedWidth || GRID_MIN_COLUMN_COUNT;
+ } else if (component.type === COLUMN_TYPE) {
+ // find the width of the largest child, only rows count
+ result.minimumWidth = GRID_MIN_COLUMN_COUNT;
+ result.occupiedWidth = 0;
+ (component.children || []).forEach(childId => {
+ // rows don't have widths, so find the width of its children
+ if (components[childId].type === ROW_TYPE) {
+ result.minimumWidth = Math.max(
+ result.minimumWidth,
+ getTotalChildWidth({ id: childId, components }),
+ );
+ }
+ });
+ } else if (
+ component.type === MARKDOWN_TYPE ||
+ component.type === CHART_TYPE
+ ) {
+ result.minimumWidth = GRID_MIN_COLUMN_COUNT;
+ }
+
+ return result;
+}
diff --git a/superset/assets/src/dashboard/util/getDropPosition.js b/superset/assets/src/dashboard/util/getDropPosition.js
index 74dfcaa..dd4add9 100644
--- a/superset/assets/src/dashboard/util/getDropPosition.js
+++ b/superset/assets/src/dashboard/util/getDropPosition.js
@@ -72,7 +72,7 @@ export default function getDropPosition(monitor, Component) {
const siblingDropOrientation =
orientation === 'row' ? 'horizontal' : 'vertical';
- if (validChild && !validSibling) {
+ if (isDraggingOverShallow && validChild && !validSibling) {
// easiest case, insert as child
if (childDropOrientation === 'vertical') {
return hasChildren ? DROP_RIGHT : DROP_LEFT;
diff --git a/superset/assets/src/dashboard/util/headerStyleOptions.js b/superset/assets/src/dashboard/util/headerStyleOptions.js
index 7efa040..a37bd5f 100644
--- a/superset/assets/src/dashboard/util/headerStyleOptions.js
+++ b/superset/assets/src/dashboard/util/headerStyleOptions.js
@@ -2,7 +2,19 @@ import { t } from '../../locales';
import { SMALL_HEADER, MEDIUM_HEADER, LARGE_HEADER } from './constants';
export default [
- { value: SMALL_HEADER, label: t('Small'), className: 'header-small' },
- { value: MEDIUM_HEADER, label: t('Medium'), className: 'header-medium' },
- { value: LARGE_HEADER, label: t('Large'), className: 'header-large' },
+ {
+ value: SMALL_HEADER,
+ label: t('Small'),
+ className: 'header-style-option header-small',
+ },
+ {
+ value: MEDIUM_HEADER,
+ label: t('Medium'),
+ className: 'header-style-option header-medium',
+ },
+ {
+ value: LARGE_HEADER,
+ label: t('Large'),
+ className: 'header-style-option header-large',
+ },
];
diff --git a/superset/assets/src/dashboard/util/newEntitiesFromDrop.js b/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
index 8abc9b9..9054d44 100644
--- a/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
+++ b/superset/assets/src/dashboard/util/newEntitiesFromDrop.js
@@ -1,5 +1,7 @@
+import componentIsResizable from './componentIsResizable';
import shouldWrapChildInRow from './shouldWrapChildInRow';
import newComponentFactory from './newComponentFactory';
+import getComponentWidthFromDrop from './getComponentWidthFromDrop';
import { ROW_TYPE, TABS_TYPE, TAB_TYPE } from './componentTypes';
@@ -10,6 +12,12 @@ export default function newEntitiesFromDrop({ dropResult, layout }) {
const dropEntity = layout[destination.id];
const dropType = dropEntity.type;
let newDropChild = newComponentFactory(dragType, dragging.meta);
+
+ if (componentIsResizable(dragging)) {
+ newDropChild.meta.width = // don't set a 0 width
+ getComponentWidthFromDrop({ dropResult, layout }) || undefined;
+ }
+
const wrapChildInRow = shouldWrapChildInRow({
parentType: dropType,
childType: dragType,
diff --git a/superset/assets/src/explore/components/DisplayQueryButton.jsx b/superset/assets/src/explore/components/DisplayQueryButton.jsx
index d098d2a..4aa3f96 100644
--- a/superset/assets/src/explore/components/DisplayQueryButton.jsx
+++ b/superset/assets/src/explore/components/DisplayQueryButton.jsx
@@ -9,6 +9,7 @@ import github from 'react-syntax-highlighter/dist/styles/github';
import CopyToClipboard from './../../components/CopyToClipboard';
import { getExploreUrlAndPayload } from '../exploreUtils';
+import Loading from '../../components/Loading';
import ModalTrigger from './../../components/ModalTrigger';
import Button from '../../components/Button';
import { t } from '../../locales';
@@ -18,7 +19,7 @@ registerLanguage('html', html);
registerLanguage('sql', sql);
registerLanguage('json', json);
-const $ = window.$ = require('jquery');
+const $ = (window.$ = require('jquery'));
const propTypes = {
animation: PropTypes.bool,
@@ -80,8 +81,9 @@ export default class DisplayQueryButton extends React.PureComponent {
}
beforeOpen() {
if (
- ['loading', null].indexOf(this.props.chartStatus) >= 0
- || !this.props.queryResponse || !this.props.queryResponse.query
+ ['loading', null].indexOf(this.props.chartStatus) >= 0 ||
+ !this.props.queryResponse ||
+ !this.props.queryResponse.query
) {
this.fetchQuery();
} else {
@@ -90,11 +92,7 @@ export default class DisplayQueryButton extends React.PureComponent {
}
renderModalBody() {
if (this.state.isLoading) {
- return (<img
- className="loading"
- alt="Loading..."
- src="/static/assets/images/loading.gif"
- />);
+ return <Loading />;
} else if (this.state.error) {
return <pre>{this.state.error}</pre>;
} else if (this.state.query) {
diff --git a/superset/assets/src/explore/components/controls/DatasourceControl.jsx b/superset/assets/src/explore/components/controls/DatasourceControl.jsx
index 404ba5e..d63d6fe 100644
--- a/superset/assets/src/explore/components/controls/DatasourceControl.jsx
+++ b/superset/assets/src/explore/components/controls/DatasourceControl.jsx
@@ -3,11 +3,19 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Table } from 'reactable';
import {
- Row, Col, Collapse, Label, FormControl, Modal,
- OverlayTrigger, Tooltip, Well,
+ Row,
+ Col,
+ Collapse,
+ Label,
+ FormControl,
+ Modal,
+ OverlayTrigger,
+ Tooltip,
+ Well,
} from 'react-bootstrap';
import ControlHeader from '../ControlHeader';
+import Loading from '../../../components/Loading';
import { t } from '../../../locales';
import ColumnOption from '../../../components/ColumnOption';
import MetricOption from '../../../components/MetricOption';
@@ -68,7 +76,8 @@ export default class DatasourceControl extends React.PureComponent {
className="datasource-link"
>
{ds.name}
- </a>),
+ </a>
+ ),
type: ds.type,
}));
@@ -113,7 +122,9 @@ export default class DatasourceControl extends React.PureComponent {
<div>
<FormControl
id="formControlsText"
- inputRef={(ref) => { this.setSearchRef(ref); }}
+ inputRef={(ref) => {
+ this.setSearchRef(ref);
+ }}
type="text"
bsSize="sm"
value={this.state.filter}
@@ -121,14 +132,8 @@ export default class DatasourceControl extends React.PureComponent {
onChange={this.changeSearch}
/>
</div>
- {this.state.loading &&
- <img
- className="loading"
- alt="Loading..."
- src="/static/assets/images/loading.gif"
- />
- }
- {this.state.datasources &&
+ {this.state.loading && <Loading />}
+ {this.state.datasources && (
<Table
columns={['name', 'type', 'schema', 'connection', 'creator']}
className="table table-condensed"
@@ -138,9 +143,10 @@ export default class DatasourceControl extends React.PureComponent {
filterBy={this.state.filter}
hideFilterInput
/>
- }
+ )}
</Modal.Body>
- </Modal>);
+ </Modal>
+ );
}
renderDatasource() {
const datasource = this.props.datasource;
@@ -157,18 +163,23 @@ export default class DatasourceControl extends React.PureComponent {
<Col md={6}>
<strong>Columns</strong>
{datasource.columns.map(col => (
- <div key={col.column_name}><ColumnOption showType column={col} /></div>
+ <div key={col.column_name}>
+ <ColumnOption showType column={col} />
+ </div>
))}
</Col>
<Col md={6}>
<strong>Metrics</strong>
{datasource.metrics.map(m => (
- <div key={m.metric_name}><MetricOption metric={m} showType /></div>
+ <div key={m.metric_name}>
+ <MetricOption metric={m} showType />
+ </div>
))}
</Col>
</Row>
</Well>
- </div>);
+ </div>
+ );
}
render() {
return (
@@ -188,7 +199,7 @@ export default class DatasourceControl extends React.PureComponent {
placement="right"
overlay={
<Tooltip id={'edit-datasource-tooltip'}>
- {t('Edit the datasource\'s configuration')}
+ {t("Edit the datasource's configuration")}
</Tooltip>
}
>
@@ -199,9 +210,7 @@ export default class DatasourceControl extends React.PureComponent {
<OverlayTrigger
placement="right"
overlay={
- <Tooltip id={'toggle-datasource-tooltip'}>
- {t('Show datasource configuration')}
- </Tooltip>
+ <Tooltip id={'toggle-datasource-tooltip'}>{t('Show datasource configuration')}</Tooltip>
}
>
<a href="#">
@@ -211,11 +220,10 @@ export default class DatasourceControl extends React.PureComponent {
/>
</a>
</OverlayTrigger>
- <Collapse in={this.state.showDatasource}>
- {this.renderDatasource()}
- </Collapse>
+ <Collapse in={this.state.showDatasource}>{this.renderDatasource()}</Collapse>
{this.renderModal()}
- </div>);
+ </div>
+ );
}
}
diff --git a/superset/assets/src/profile/components/TableLoader.jsx b/superset/assets/src/profile/components/TableLoader.jsx
index 1e67426..462e009 100644
--- a/superset/assets/src/profile/components/TableLoader.jsx
+++ b/superset/assets/src/profile/components/TableLoader.jsx
@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Table, Tr, Td } from 'reactable';
import $ from 'jquery';
-
+import Loading from '../../components/Loading';
import '../../../stylesheets/reactable-pagination.css';
const propTypes = {
@@ -29,6 +29,9 @@ export default class TableLoader extends React.PureComponent {
});
}
render() {
+ if (this.state.isLoading) {
+ return <Loading />;
+ }
const tableProps = Object.assign({}, this.props);
let { columns } = this.props;
if (!columns && this.state.data.length > 0) {
@@ -37,11 +40,14 @@ export default class TableLoader extends React.PureComponent {
delete tableProps.dataEndpoint;
delete tableProps.mutator;
delete tableProps.columns;
- if (this.state.isLoading) {
- return <img alt="loading" width="25" src="/static/assets/images/loading.gif" />;
- }
+
return (
- <Table {...tableProps} className="table" itemsPerPage={50} style={{ textTransform: 'capitalize' }}>
+ <Table
+ {...tableProps}
+ className="table"
+ itemsPerPage={50}
+ style={{ textTransform: 'capitalize' }}
+ >
{this.state.data.map((row, i) => (
<Tr key={i}>
{columns.map((col) => {
@@ -49,9 +55,14 @@ export default class TableLoader extends React.PureComponent {
return (
<Td key={col} column={col} value={row['_' + col]}>
{row[col]}
- </Td>);
+ </Td>
+ );
}
- return <Td key={col} column={col}>{row[col]}</Td>;
+ return (
+ <Td key={col} column={col}>
+ {row[col]}
+ </Td>
+ );
})}
</Tr>
))}
diff --git a/superset/assets/src/welcome/DashboardTable.jsx b/superset/assets/src/welcome/DashboardTable.jsx
index 78d4bdd..f7f3007 100644
--- a/superset/assets/src/welcome/DashboardTable.jsx
+++ b/superset/assets/src/welcome/DashboardTable.jsx
@@ -4,6 +4,7 @@ import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { Table, Tr, Td, Thead, Th, unsafe } from 'reactable';
+import Loading from '../components/Loading';
import '../../stylesheets/reactable-pagination.css';
const $ = window.$ = require('jquery');
@@ -60,12 +61,7 @@ export default class DashboardTable extends React.PureComponent {
</Table>
);
}
- return (
- <img
- className="loading"
- alt="Loading..."
- src="/static/assets/images/loading.gif"
- />);
+ return <Loading />;
}
}
DashboardTable.propTypes = propTypes;
diff --git a/superset/assets/yarn.lock b/superset/assets/yarn.lock
index 2add797..9d1a39b 100644
--- a/superset/assets/yarn.lock
+++ b/superset/assets/yarn.lock
@@ -2051,6 +2051,10 @@ clap@^1.0.9:
dependencies:
chalk "^1.1.3"
+classnames@2.x, classnames@^2.1.2:
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
+
classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
@@ -3591,6 +3595,10 @@ execa@^0.7.0:
signal-exit "^3.0.0"
strip-eof "^1.0.0"
+exenv@^1.2.0:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
+
exit-hook@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
@@ -5731,7 +5739,7 @@ lodash.isarray@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
-lodash.isequal@^4.1.1:
+lodash.isequal@^4.0.0, lodash.isequal@^4.1.1:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
@@ -7953,6 +7961,15 @@ react-bootstrap-slider@2.0.1:
react "^15.6.1"
react-dom "^15.6.1"
+react-bootstrap-table@^4.0.2:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/react-bootstrap-table/-/react-bootstrap-table-4.3.1.tgz#f704be55b7f6bf0557d2fc5bec6d25fd307d0cde"
+ dependencies:
+ classnames "^2.1.2"
+ prop-types "^15.5.10"
+ react-modal "^3.1.7"
+ react-s-alert "^1.3.2"
+
react-bootstrap@^0.31.5:
version "0.31.5"
resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-0.31.5.tgz#57040fa8b1274e1e074803c21a1b895fdabea05a"
@@ -8013,6 +8030,13 @@ react-dnd@^2.5.4:
object-assign "^4.1.0"
prop-types "^15.5.10"
+react-draggable@3.x:
+ version "3.0.5"
+ resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.5.tgz#c031e0ed4313531f9409d6cd84c8ebcec0ddfe2d"
+ dependencies:
+ classnames "^2.2.5"
+ prop-types "^15.6.0"
+
"react-draggable@^2.2.6 || ^3.0.3":
version "3.0.3"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.3.tgz#a6f9b3a7171981b76dadecf238316925cb9eacf4"
@@ -8028,6 +8052,16 @@ react-gravatar@^2.6.1:
md5 "^2.1.0"
query-string "^4.2.2"
+react-grid-layout@0.16.5:
+ version "0.16.5"
+ resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-0.16.5.tgz#1ff12d12afa875c11fe05802f7509e52bfe9a2cb"
+ dependencies:
+ classnames "2.x"
+ lodash.isequal "^4.0.0"
+ prop-types "15.x"
+ react-draggable "3.x"
+ react-resizable "1.x"
+
react-html-attributes@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/react-html-attributes/-/react-html-attributes-1.4.1.tgz#97b5ec710da68833598c8be6f89ac436216840a5"
@@ -8047,6 +8081,10 @@ react-input-autosize@^2.1.2:
dependencies:
prop-types "^15.5.8"
+react-lifecycles-compat@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
+
react-map-gl@^3.0.4:
version "3.0.5"
resolved "https://registry.yarnpkg.com/react-map-gl/-/react-map-gl-3.0.5.tgz#8797b4a1a85be1404a2409f43f577ad939475a60"
@@ -8069,6 +8107,15 @@ react-markdown@^3.3.0:
unist-util-visit "^1.3.0"
xtend "^4.0.1"
+react-modal@^3.1.7:
+ version "3.4.5"
+ resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.4.5.tgz#75a7eefb8f4c8247278d5ce1c41249d7785d9f69"
+ dependencies:
+ exenv "^1.2.0"
+ prop-types "^15.5.10"
+ react-lifecycles-compat "^3.0.0"
+ warning "^3.0.0"
+
react-onclickoutside@^5.9.0:
version "5.11.1"
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-5.11.1.tgz#00314e52567cf55faba94cabbacd119619070623"
@@ -8096,13 +8143,19 @@ react-redux@^5.0.2:
loose-envify "^1.1.0"
prop-types "^15.5.10"
-react-resizable@^1.3.3:
+react-resizable@1.x, react-resizable@^1.3.3:
version "1.7.5"
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e"
dependencies:
prop-types "15.x"
react-draggable "^2.2.6 || ^3.0.3"
+react-s-alert@^1.3.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/react-s-alert/-/react-s-alert-1.4.1.tgz#ef3665a9d98c4cf2e448fc2d84e48aeca799bb5a"
+ dependencies:
+ babel-runtime "^6.23.0"
+
react-search-input@^0.11.3:
version "0.11.3"
resolved "https://registry.yarnpkg.com/react-search-input/-/react-search-input-0.11.3.tgz#3dd1f9fc584b6bc40a6ee133ae042b6fbb7ae8dd"