Merge pull request #6566 from bevacqua/feature/sort-dimensions-dragging

Drag aggregations to sort instead of having up/down arrows.
This commit is contained in:
Nicolás Bevacqua 2016-05-27 18:22:25 -03:00
commit 1cf2979ab2
20 changed files with 344 additions and 42 deletions

View file

@ -95,6 +95,7 @@
"commander": "2.8.1",
"css-loader": "0.17.0",
"d3": "3.5.6",
"dragula": "3.7.0",
"elasticsearch": "10.1.2",
"elasticsearch-browser": "10.1.2",
"expiry-js": "0.1.7",

View file

@ -9,7 +9,7 @@ function VisDetailsSpyProvider(Notifier, $filter, $rootScope, config) {
template: visDebugSpyPanelTemplate,
order: 5,
link: function ($scope, $el) {
$scope.$watch('vis.getState() | json', function (json) {
$scope.$watch('vis.getEnabledState() | json', function (json) {
$scope.visStateJson = json;
});
}

View file

@ -496,7 +496,7 @@ app.controller('discover', function ($scope, config, courier, $route, $window, N
// we have a vis, just modify the aggs
if ($scope.vis) {
const visState = $scope.vis.getState();
const visState = $scope.vis.getEnabledState();
visState.aggs = visStateAggs;
$scope.vis.setState(visState);

View file

@ -201,3 +201,5 @@ kbn-settings-indices {
.kbn-settings-indices-create {
.time-and-pattern > div {}
}
@import "~ui/dragula/gu-dragula.less";

View file

@ -0,0 +1,121 @@
import angular from 'angular';
import sinon from 'sinon';
import expect from 'expect.js';
import ngMock from 'ng_mock';
let init;
let $rootScope;
let $compile;
describe('draggable_* directives', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector) {
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
init = function init(markup = '') {
const $parentScope = $rootScope.$new();
$parentScope.items = [
{ name: 'item_1' },
{ name: 'item_2' },
{ name: 'item_3' }
];
// create the markup
const $elem = angular.element(`<div draggable-container="items">`);
$elem.html(markup);
// compile the directive
$compile($elem)($parentScope);
$parentScope.$apply();
const $scope = $elem.scope();
return { $parentScope, $scope, $elem };
};
}));
describe('draggable_container directive', function () {
it('should expose the drake', function () {
const { $scope } = init();
expect($scope.drake).to.be.an(Object);
});
it('should expose the controller', function () {
const { $scope } = init();
expect($scope.draggableContainerCtrl).to.be.an(Object);
});
it('should pull item list from directive attribute', function () {
const { $scope, $parentScope } = init();
expect($scope.draggableContainerCtrl.getList()).to.eql($parentScope.items);
});
it('should not be able to move extraneous DOM elements', function () {
const bare = angular.element(`<div>`);
const { $scope } = init();
expect($scope.drake.canMove(bare[0])).to.eql(false);
});
it('should not be able to move non-[draggable-item] elements', function () {
const bare = angular.element(`<div>`);
const { $scope, $elem } = init();
$elem.append(bare);
expect($scope.drake.canMove(bare[0])).to.eql(false);
});
it('shouldn\'t be able to move extraneous [draggable-item] elements', function () {
const anotherParent = angular.element(`<div draggable-container="items">`);
const item = angular.element(`<div draggable-item="items[0]">`);
const scope = $rootScope.$new();
anotherParent.append(item);
$compile(anotherParent)(scope);
$compile(item)(scope);
scope.$apply();
const { $scope } = init();
expect($scope.drake.canMove(item[0])).to.eql(false);
});
it('shouldn\'t be able to move [draggable-item] if it has a handle', function () {
const { $scope, $elem } = init(`
<div draggable-item="items[0]">
<div draggable-handle></div>
</div>
`);
const item = $elem.find(`[draggable-item]`);
expect($scope.drake.canMove(item[0])).to.eql(false);
});
it('should be able to move [draggable-item] by its handle', function () {
const { $scope, $elem } = init(`
<div draggable-item="items[0]">
<div draggable-handle></div>
</div>
`);
const handle = $elem.find(`[draggable-handle]`);
expect($scope.drake.canMove(handle[0])).to.eql(true);
});
});
describe('draggable_item', function () {
it('should be required to be a child to [draggable-container]', function () {
const item = angular.element(`<div draggable-item="items[0]">`);
const scope = $rootScope.$new();
expect(() => {
$compile(item)(scope);
scope.$apply();
}).to.throwException(/controller(.+)draggableContainer(.+)required/i);
});
});
describe('draggable_handle', function () {
it('should be required to be a child to [draggable-item]', function () {
const handle = angular.element(`<div draggable-handle>`);
const scope = $rootScope.$new();
expect(() => {
$compile(handle)(scope);
scope.$apply();
}).to.throwException(/controller(.+)draggableItem(.+)required/i);
});
});
});

View file

@ -27,30 +27,40 @@
<!-- controls !!!actually disabling buttons will break tooltips¡¡¡ -->
<div class="vis-editor-agg-header-controls btn-group">
<!-- up button -->
<!-- disable aggregation -->
<button
aria-label="Increase Priority"
ng-if="stats.count > 1"
ng-class="{ disabled: $first }"
ng-click="moveUp(agg)"
tooltip="Increase Priority"
ng-if="agg.enabled && canRemove(agg)"
ng-click="agg.enabled = false"
aria-label="Disable aggregation"
tooltip="Disable aggregation"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-primary">
<i aria-hidden="true" class="fa fa-caret-up"></i>
class="btn btn-xs">
<i aria-hidden="true" class="fa fa-toggle-on"></i>
</button>
<!-- down button -->
<!-- enable aggregation -->
<button
aria-label="Decrease Priority"
ng-if="stats.count > 1"
ng-class="{ disabled: $last }"
ng-click="moveDown(agg)"
tooltip="Decrease Priority"
ng-if="!agg.enabled"
ng-click="agg.enabled = true"
aria-label="Enable aggregation"
tooltip="Enable aggregation"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs btn-primary">
<i aria-hidden="true" class="fa fa-caret-down"></i>
class="btn btn-xs">
<i aria-hidden="true" class="fa fa-toggle-off"></i>
</button>
<!-- drag handle -->
<button
draggable-handle
aria-label="Modify Priority by Dragging"
ng-if="stats.count > 1"
tooltip="Modify Priority by Dragging"
tooltip-append-to-body="true"
type="button"
class="btn btn-xs">
<i aria-hidden="true" class="fa fa-arrows-v"></i>
</button>
<!-- remove button -->
@ -79,5 +89,6 @@
<vis-editor-agg-add
ng-if="$index + 1 === stats.count"
ng-hide="dragging"
class="vis-editor-agg-add vis-editor-agg-add-subagg">
</vis-editor-agg-add>

View file

@ -46,13 +46,16 @@ uiModules
return label ? label : '';
};
function move(below, agg) {
_.move($scope.vis.aggs, agg, below, function (otherAgg) {
return otherAgg.schema.group === agg.schema.group;
});
}
$scope.moveUp = _.partial(move, false);
$scope.moveDown = _.partial(move, true);
$scope.$on('drag-start', e => {
$scope.editorWasOpen = $scope.editorOpen;
$scope.editorOpen = false;
$scope.$emit('agg-drag-start', $scope.agg);
});
$scope.$on('drag-end', e => {
$scope.editorOpen = $scope.editorWasOpen;
$scope.$emit('agg-drag-end', $scope.agg);
});
$scope.remove = function (agg) {
const aggs = $scope.vis.aggs;

View file

@ -3,9 +3,9 @@
{{ groupName }}
</div>
<div class="vis-editor-agg-group" ng-class="groupName">
<div ng-class="groupName" draggable-container="vis.aggs" class="vis-editor-agg-group">
<!-- wrapper needed for nesting-indicator -->
<div ng-repeat="agg in group" class="vis-editor-agg-wrapper">
<div ng-repeat="agg in group" draggable-item="agg" class="vis-editor-agg-wrapper">
<!-- agg.html - controls for aggregation -->
<ng-form vis-editor-agg name="aggForm" class="vis-editor-agg"></ng-form>
</div>

View file

@ -41,6 +41,9 @@ uiModules
if (count < schema.max) return true;
});
});
$scope.$on('agg-drag-start', e => $scope.dragging = true);
$scope.$on('agg-drag-end', e => $scope.dragging = false);
}
};

View file

@ -0,0 +1,88 @@
import _ from 'lodash';
import $ from 'jquery';
import dragula from 'dragula';
import uiModules from 'ui/modules';
uiModules
.get('app/visualize')
.directive('draggableContainer', function () {
return {
restrict: 'A',
scope: true,
controllerAs: 'draggableContainerCtrl',
controller($scope, $attrs, $parse) {
this.getList = () => $parse($attrs.draggableContainer)($scope);
},
link($scope, $el, attr) {
const drake = dragula({
containers: $el.toArray(),
moves(el, source, handle) {
const itemScope = $(el).scope();
if (!('draggableItemCtrl' in itemScope)) {
return; // only [draggable-item] is draggable
}
return itemScope.draggableItemCtrl.moves(handle);
}
});
const drakeEvents = [
'cancel',
'cloned',
'drag',
'dragend',
'drop',
'out',
'over',
'remove',
'shadow'
];
const prettifiedDrakeEvents = {
drag: 'start',
dragend: 'end'
};
drakeEvents.forEach(type => {
drake.on(type, (el, ...args) => forwardEvent(type, el, ...args));
});
drake.on('drag', markDragging(true));
drake.on('dragend', markDragging(false));
drake.on('drop', drop);
$scope.$on('$destroy', drake.destroy);
$scope.drake = drake;
function markDragging(isDragging) {
return el => {
const scope = $(el).scope();
scope.isDragging = isDragging;
scope.$apply();
};
}
function forwardEvent(type, el, ...args) {
const name = `drag-${prettifiedDrakeEvents[type] || type}`;
const scope = $(el).scope();
scope.$broadcast(name, el, ...args);
}
function drop(el, target, source, sibling) {
const list = $scope.draggableContainerCtrl.getList();
const itemScope = $(el).scope();
const item = itemScope.draggableItemCtrl.getItem();
const toIndex = getSiblingItemIndex(list, sibling);
_.move(list, item, toIndex);
}
function getSiblingItemIndex(list, sibling) {
if (!sibling) { // means the item was dropped at the end of the list
return list.length - 1;
}
const siblingScope = $(sibling).scope();
const siblingItem = siblingScope.draggableItemCtrl.getItem();
const siblingIndex = list.indexOf(siblingItem);
return siblingIndex;
}
}
};
});

View file

@ -0,0 +1,14 @@
import uiModules from 'ui/modules';
uiModules
.get('app/visualize')
.directive('draggableHandle', function () {
return {
restrict: 'A',
require: '^draggableItem',
link($scope, $el, attr, ctrl) {
ctrl.registerHandle($el);
$el.addClass('gu-handle');
}
};
});

View file

@ -0,0 +1,29 @@
import $ from 'jquery';
import uiModules from 'ui/modules';
uiModules
.get('app/visualize')
.directive('draggableItem', function () {
return {
restrict: 'A',
require: '^draggableContainer',
scope: true,
controllerAs: 'draggableItemCtrl',
controller($scope, $attrs, $parse) {
const dragHandles = $();
this.getItem = () => $parse($attrs.draggableItem)($scope);
this.registerHandle = $el => {
dragHandles.push(...$el);
};
this.moves = handle => {
const $handle = $(handle);
const $anywhereInParentChain = $handle.parents().addBack();
const movable = dragHandles.is($anywhereInParentChain);
return movable;
};
},
link($scope, $el, attr) {
}
};
});

View file

@ -119,8 +119,8 @@ uiModules
if (!angular.equals($state.vis, savedVisState)) {
Promise.try(function () {
vis.setState($state.vis);
editableVis.setState($state.vis);
vis.setState(editableVis.getEnabledState());
})
.catch(courier.redirectWhenMissing({
'index-pattern-field': '/visualize'
@ -150,9 +150,9 @@ uiModules
$scope.stageEditableVis = transferVisState(editableVis, vis, true);
$scope.resetEditableVis = transferVisState(vis, editableVis);
$scope.$watch(function () {
return editableVis.getState();
return editableVis.getEnabledState();
}, function (newState) {
editableVis.dirty = !angular.equals(newState, vis.getState());
editableVis.dirty = !angular.equals(newState, vis.getEnabledState());
$scope.responseValueAggs = null;
try {
@ -292,14 +292,16 @@ uiModules
}
};
function transferVisState(fromVis, toVis, fetch) {
function transferVisState(fromVis, toVis, stage) {
return function () {
toVis.setState(fromVis.getState());
const view = fromVis.getEnabledState();
const full = fromVis.getState();
toVis.setState(view);
editableVis.dirty = false;
$state.vis = vis.getState();
$state.vis = full;
$state.save();
if (fetch) $scope.fetch();
if (stage) $scope.fetch();
};
}

View file

@ -11,6 +11,9 @@ import 'plugins/kibana/visualize/editor/agg_params';
import 'plugins/kibana/visualize/editor/nesting_indicator';
import 'plugins/kibana/visualize/editor/sidebar';
import 'plugins/kibana/visualize/editor/vis_options';
import 'plugins/kibana/visualize/editor/draggable_container';
import 'plugins/kibana/visualize/editor/draggable_item';
import 'plugins/kibana/visualize/editor/draggable_handle';
import 'plugins/kibana/visualize/saved_visualizations/_saved_vis';
import 'plugins/kibana/visualize/saved_visualizations/saved_visualizations';
import uiRoutes from 'ui/routes';

View file

@ -0,0 +1,13 @@
.gu-handle {
cursor: move;
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
}
.gu-mirror,
.gu-mirror .gu-handle {
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}

View file

@ -607,3 +607,5 @@ fieldset {
}
}
}
@import (reference) "~dragula/dist/dragula.css";

View file

@ -58,7 +58,7 @@ describe('Vis Class', function () {
describe('getState()', function () {
it('should get a state that represents the... er... state', function () {
let state = vis.getState();
let state = vis.getEnabledState();
expect(state).to.have.property('type', 'pie');
expect(state).to.have.property('params');

View file

@ -9,6 +9,7 @@ export default function AggConfigFactory(Private, fieldTypeFilter) {
self.id = String(opts.id || AggConfig.nextId(vis.aggs));
self.vis = vis;
self._opts = opts = (opts || {});
self.enabled = typeof opts.enabled === 'boolean' ? opts.enabled : true;
// setters
self.type = opts.type;
@ -232,6 +233,7 @@ export default function AggConfigFactory(Private, fieldTypeFilter) {
return {
id: self.id,
enabled: self.enabled,
type: self.type && self.type.name,
schema: self.schema && self.schema.name,
params: outParams

View file

@ -47,7 +47,7 @@ export default function VisFactory(Notifier, Private) {
oldConfigs.forEach(function (oldConfig) {
let agg = {
schema: schema.name,
type: oldConfig.agg,
type: oldConfig.agg
};
let aggType = aggTypes.byName[agg.type];
@ -84,18 +84,27 @@ export default function VisFactory(Notifier, Private) {
this.aggs = new AggConfigs(this, state.aggs);
};
Vis.prototype.getState = function () {
Vis.prototype.getStateInternal = function (includeDisabled) {
return {
title: this.title,
type: this.type.name,
params: this.params,
aggs: this.aggs.map(function (agg) {
return agg.toJSON();
}).filter(Boolean),
aggs: this.aggs
.filter(agg => includeDisabled || agg.enabled)
.map(agg => agg.toJSON())
.filter(Boolean),
listeners: this.listeners
};
};
Vis.prototype.getEnabledState = function () {
return this.getStateInternal(false);
};
Vis.prototype.getState = function () {
return this.getStateInternal(true);
};
Vis.prototype.createEditableVis = function () {
return this._editableVis || (this._editableVis = this.clone());
};

View file

@ -29,8 +29,7 @@ uiModules
},
template: visualizeTemplate,
link: function ($scope, $el, attr) {
let chart; // set in "vis" watcher
let minVisChartHeight = 180;
const minVisChartHeight = 180;
if (_.isUndefined($scope.showSpyPanel)) {
$scope.showSpyPanel = true;