mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
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:
commit
1cf2979ab2
20 changed files with 344 additions and 42 deletions
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -201,3 +201,5 @@ kbn-settings-indices {
|
|||
.kbn-settings-indices-create {
|
||||
.time-and-pattern > div {}
|
||||
}
|
||||
|
||||
@import "~ui/dragula/gu-dragula.less";
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
});
|
|
@ -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');
|
||||
}
|
||||
};
|
||||
});
|
29
src/plugins/kibana/public/visualize/editor/draggable_item.js
Normal file
29
src/plugins/kibana/public/visualize/editor/draggable_item.js
Normal 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) {
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
13
src/ui/public/dragula/gu-dragula.less
Normal file
13
src/ui/public/dragula/gu-dragula.less
Normal 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;
|
||||
}
|
|
@ -607,3 +607,5 @@ fieldset {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@import (reference) "~dragula/dist/dragula.css";
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue