[Vis Editor] EUIfication of agg and agg-group directives (#40866)

* Create default_editor_agg.tsx

* Create default_editor_agg_group

* Apply drag and drop

* Remove unused dragula dependency

* Remove old mocha tests

* Add ts for state

* Update functional tests

* Update touched condition

* Apply styles for accordion button content

* Apply truncate for agg description

* Remove unused styles

* Separate common props

* Move aggGroupNamesMap to agg_group.js

* Update _sidebar.scss

* Pass schemas prop

* Prevent scroll bar and add space

* Remove unused min from stats

* Add OnAggParamsChange type

* Show error as an icon

* Update background color

* Update title size

* Remove Schema.deprecate since it's not used
This commit is contained in:
Maryia Lapata 2019-07-29 13:58:50 +03:00 committed by GitHub
parent 91adbfc88b
commit 85f9ef6433
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 1033 additions and 1514 deletions

View file

@ -150,7 +150,6 @@
"d3": "3.5.17",
"d3-cloud": "1.2.5",
"del": "^4.0.0",
"dragula": "3.7.2",
"elasticsearch": "^16.2.0",
"elasticsearch-browser": "^16.2.0",
"encode-uri-query": "1.0.1",

View file

@ -23,21 +23,6 @@ kbn-management-objects-view {
.ace_editor { height: 300px; }
}
// SASSTODO: These are some dragula settings.
.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;
}
// Hack because the management wrapper is flat HTML and needs a class
.mgtPage__body {
max-width: map-get($euiBreakpoints, 'xl');

View file

@ -30,6 +30,7 @@ import {
EuiLink,
EuiSpacer,
EuiText,
EuiFormRow,
} from '@elastic/eui';
import dateMath from '@elastic/datemath';
import { FormattedMessage } from '@kbn/i18n/react';
@ -110,96 +111,99 @@ function DateRangesParamEditor({
);
return (
<>
<EuiText size="xs">
<EuiLink href={getDocLink('date.dateMath')} target="_blank" rel="noopener">
<FormattedMessage
id="common.ui.aggTypes.dateRanges.acceptedDateFormatsLinkText"
defaultMessage="Acceptable date formats"
/>
</EuiLink>
</EuiText>
<EuiSpacer size="s" />
<EuiFormRow compressed>
<>
<EuiText size="xs">
<EuiLink href={getDocLink('date.dateMath')} target="_blank" rel="noopener">
<FormattedMessage
id="common.ui.aggTypes.dateRanges.acceptedDateFormatsLinkText"
defaultMessage="Acceptable date formats"
/>
</EuiLink>
</EuiText>
<EuiSpacer size="s" />
{ranges.map(({ from, to, id }) => {
const deleteBtnTitle = i18n.translate(
'common.ui.aggTypes.dateRanges.removeRangeButtonAriaLabel',
{
defaultMessage: 'Remove the range of {from} to {to}',
values: { from: from || FROM_PLACEHOLDER, to: to || TO_PLACEHOLDER },
}
);
const areBothEmpty = !from && !to;
{ranges.map(({ from, to, id }) => {
const deleteBtnTitle = i18n.translate(
'common.ui.aggTypes.dateRanges.removeRangeButtonAriaLabel',
{
defaultMessage: 'Remove the range of {from} to {to}',
values: { from: from || FROM_PLACEHOLDER, to: to || TO_PLACEHOLDER },
}
);
const areBothEmpty = !from && !to;
return (
<Fragment key={id}>
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
<EuiFlexItem>
<EuiFieldText
aria-label={i18n.translate('common.ui.aggTypes.dateRanges.fromColumnLabel', {
defaultMessage: 'From',
description: 'Beginning of a date range, e.g. *From* 2018-02-26 To 2018-02-28',
})}
compressed
fullWidth={true}
isInvalid={areBothEmpty || !validateDateMath(from)}
placeholder={FROM_PLACEHOLDER}
value={from || ''}
onChange={ev => onChangeRange(id, 'from', ev.target.value)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type="sortRight" color="subdued" />
</EuiFlexItem>
<EuiFlexItem>
<EuiFieldText
aria-label={i18n.translate('common.ui.aggTypes.dateRanges.toColumnLabel', {
defaultMessage: 'To',
description: 'End of a date range, e.g. From 2018-02-26 *To* 2018-02-28',
})}
compressed
fullWidth={true}
isInvalid={areBothEmpty || !validateDateMath(to)}
placeholder={TO_PLACEHOLDER}
value={to || ''}
onChange={ev => onChangeRange(id, 'to', ev.target.value)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
title={deleteBtnTitle}
aria-label={deleteBtnTitle}
disabled={value.length === 1}
color="danger"
iconType="trash"
onClick={() => onRemoveRange(id)}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
</Fragment>
);
})}
return (
<Fragment key={id}>
<EuiFlexGroup responsive={false} gutterSize="s" alignItems="center">
<EuiFlexItem>
<EuiFieldText
aria-label={i18n.translate('common.ui.aggTypes.dateRanges.fromColumnLabel', {
defaultMessage: 'From',
description:
'Beginning of a date range, e.g. *From* 2018-02-26 To 2018-02-28',
})}
compressed
fullWidth={true}
isInvalid={areBothEmpty || !validateDateMath(from)}
placeholder={FROM_PLACEHOLDER}
value={from || ''}
onChange={ev => onChangeRange(id, 'from', ev.target.value)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type="sortRight" color="subdued" />
</EuiFlexItem>
<EuiFlexItem>
<EuiFieldText
aria-label={i18n.translate('common.ui.aggTypes.dateRanges.toColumnLabel', {
defaultMessage: 'To',
description: 'End of a date range, e.g. From 2018-02-26 *To* 2018-02-28',
})}
compressed
fullWidth={true}
isInvalid={areBothEmpty || !validateDateMath(to)}
placeholder={TO_PLACEHOLDER}
value={to || ''}
onChange={ev => onChangeRange(id, 'to', ev.target.value)}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
title={deleteBtnTitle}
aria-label={deleteBtnTitle}
disabled={value.length === 1}
color="danger"
iconType="trash"
onClick={() => onRemoveRange(id)}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
</Fragment>
);
})}
{hasInvalidRange && (
<EuiFormErrorText>
<FormattedMessage
id="common.ui.aggTypes.dateRanges.errorMessage"
defaultMessage="Each range should have at least one valid date."
/>
</EuiFormErrorText>
)}
{hasInvalidRange && (
<EuiFormErrorText>
<FormattedMessage
id="common.ui.aggTypes.dateRanges.errorMessage"
defaultMessage="Each range should have at least one valid date."
/>
</EuiFormErrorText>
)}
<EuiSpacer size="s" />
<EuiFlexItem>
<EuiButtonEmpty iconType="plusInCircleFilled" onClick={onAddRange} size="xs">
<FormattedMessage
id="common.ui.aggTypes.dateRanges.addRangeButtonLabel"
defaultMessage="Add range"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</>
<EuiSpacer size="s" />
<EuiFlexItem>
<EuiButtonEmpty iconType="plusInCircleFilled" onClick={onAddRange} size="xs">
<FormattedMessage
id="common.ui.aggTypes.dateRanges.addRangeButtonLabel"
defaultMessage="Add range"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</>
</EuiFormRow>
);
}

View file

@ -27,6 +27,7 @@ import {
EuiIcon,
EuiSpacer,
EuiButtonEmpty,
EuiFormRow,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@ -88,75 +89,77 @@ function RangesParamEditor({ agg, value = [], setValue }: AggParamEditorProps<Ra
);
return (
<>
{ranges.map(({ from, to, id }) => {
const deleteBtnTitle = i18n.translate(
'common.ui.aggTypes.ranges.removeRangeButtonAriaLabel',
{
defaultMessage: 'Remove the range of {from} to {to}',
values: {
from: isEmpty(from) ? FROM_PLACEHOLDER : from,
to: isEmpty(to) ? TO_PLACEHOLDER : to,
},
}
);
<EuiFormRow compressed>
<>
{ranges.map(({ from, to, id }) => {
const deleteBtnTitle = i18n.translate(
'common.ui.aggTypes.ranges.removeRangeButtonAriaLabel',
{
defaultMessage: 'Remove the range of {from} to {to}',
values: {
from: isEmpty(from) ? FROM_PLACEHOLDER : from,
to: isEmpty(to) ? TO_PLACEHOLDER : to,
},
}
);
return (
<Fragment key={id}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem>
<EuiFieldNumber
aria-label={i18n.translate('common.ui.aggTypes.ranges.fromLabel', {
defaultMessage: 'From',
})}
value={isEmpty(from) ? '' : from}
placeholder={FROM_PLACEHOLDER}
onChange={ev => onChangeRange(id, 'from', ev.target.value)}
fullWidth={true}
compressed={true}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type="sortRight" color="subdued" />
</EuiFlexItem>
<EuiFlexItem>
<EuiFieldNumber
aria-label={i18n.translate('common.ui.aggTypes.ranges.toLabel', {
defaultMessage: 'To',
})}
value={isEmpty(to) ? '' : to}
placeholder={TO_PLACEHOLDER}
onChange={ev => onChangeRange(id, 'to', ev.target.value)}
fullWidth={true}
compressed={true}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
title={deleteBtnTitle}
aria-label={deleteBtnTitle}
disabled={value.length === 1}
color="danger"
iconType="trash"
onClick={() => onRemoveRange(id)}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
</Fragment>
);
})}
return (
<Fragment key={id}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem>
<EuiFieldNumber
aria-label={i18n.translate('common.ui.aggTypes.ranges.fromLabel', {
defaultMessage: 'From',
})}
value={isEmpty(from) ? '' : from}
placeholder={FROM_PLACEHOLDER}
onChange={ev => onChangeRange(id, 'from', ev.target.value)}
fullWidth={true}
compressed={true}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIcon type="sortRight" color="subdued" />
</EuiFlexItem>
<EuiFlexItem>
<EuiFieldNumber
aria-label={i18n.translate('common.ui.aggTypes.ranges.toLabel', {
defaultMessage: 'To',
})}
value={isEmpty(to) ? '' : to}
placeholder={TO_PLACEHOLDER}
onChange={ev => onChangeRange(id, 'to', ev.target.value)}
fullWidth={true}
compressed={true}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
title={deleteBtnTitle}
aria-label={deleteBtnTitle}
disabled={value.length === 1}
color="danger"
iconType="trash"
onClick={() => onRemoveRange(id)}
/>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xs" />
</Fragment>
);
})}
<EuiSpacer size="s" />
<EuiFlexItem>
<EuiButtonEmpty iconType="plusInCircleFilled" onClick={onAddRange} size="xs">
<FormattedMessage
id="common.ui.aggTypes.ranges.addRangeButtonLabel"
defaultMessage="Add range"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</>
<EuiSpacer size="s" />
<EuiFlexItem>
<EuiButtonEmpty iconType="plusInCircleFilled" onClick={onAddRange} size="xs">
<FormattedMessage
id="common.ui.aggTypes.ranges.addRangeButtonLabel"
defaultMessage="Add range"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</>
</EuiFormRow>
);
}

View file

@ -17,4 +17,11 @@
* under the License.
*/
export type AggConfigs = any;
import { IndexedArray } from '../indexed_array';
import { AggConfig } from './agg_config';
export interface AggConfigs extends IndexedArray<AggConfig> {
bySchemaGroup: {
[key: string]: AggConfig[];
};
}

View file

@ -1,139 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import angular from 'angular';
import expect from '@kbn/expect';
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

@ -1,133 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import dragula from 'dragula';
import 'dragula/dist/dragula.css';
import { uiModules } from '../../modules';
import { move } from '../../utils/collection';
uiModules
.get('kibana')
.directive('draggableContainer', function () {
const $scopes = new WeakMap();
return {
restrict: 'A',
scope: true,
controllerAs: 'draggableContainerCtrl',
controller($scope, $attrs, $parse, $element) {
$scopes.set($element.get(0), $scope);
this.linkDraggableItem = (el, $scope) => {
$scopes.set(el, $scope);
};
this.getList = () => $parse($attrs.draggableContainer)($scope);
},
link($scope, $el) {
const drake = dragula({
containers: $el.toArray(),
moves(el, source, handle) {
const itemScope = $scopes.get(el);
if (!itemScope || !('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 = $scopes.get(el);
if (!scope) return;
scope.isDragging = isDragging;
scope.$apply();
};
}
function forwardEvent(type, el, ...args) {
const name = `drag-${prettifiedDrakeEvents[type] || type}`;
const scope = $scopes.get(el);
if (!scope) return;
scope.$broadcast(name, el, ...args);
}
function drop(el, target, source, sibling) {
const list = $scope.draggableContainerCtrl.getList();
const itemScope = $scopes.get(el);
if (!itemScope) return;
const item = itemScope.draggableItemCtrl.getItem();
const fromIndex = list.indexOf(item);
const siblingIndex = getItemIndexFromElement(list, sibling);
const toIndex = getTargetIndex(list, fromIndex, siblingIndex);
move(list, item, toIndex);
}
function getTargetIndex(list, fromIndex, siblingIndex) {
if (siblingIndex === -1) {
// means the item was dropped at the end of the list
return list.length - 1;
} else if (fromIndex < siblingIndex) {
// An item moving from a lower index to a higher index will offset the
// index of the earlier items by one.
return siblingIndex - 1;
}
return siblingIndex;
}
function getItemIndexFromElement(list, element) {
if (!element) return -1;
const scope = $scopes.get(element);
if (!scope) return;
const item = scope.draggableItemCtrl.getItem();
const index = list.indexOf(item);
return index;
}
}
};
});

View file

@ -1,33 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { uiModules } from '../../modules';
uiModules
.get('kibana')
.directive('draggableHandle', function () {
return {
restrict: 'A',
require: '^draggableItem',
link($scope, $el, attr, ctrl) {
ctrl.registerHandle($el);
$el.addClass('gu-handle');
}
};
});

View file

@ -1,49 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import $ from 'jquery';
import { uiModules } from '../../modules';
uiModules
.get('kibana')
.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, draggableController) {
draggableController.linkDraggableItem($el.get(0), $scope);
}
};
});

View file

@ -1,101 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import angular from 'angular';
import _ from 'lodash';
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import '../agg';
describe('Vis-Editor-Agg plugin directive', function () {
const $parentScope = {};
let $elem;
function makeConfig(which) {
const schemaMap = {
radius: {
title: 'Dot Size',
min: 0,
max: 1
},
metric: {
title: 'Y-Axis',
min: 1,
max: Infinity
}
};
const typeOptions = ['count', 'avg', 'sum', 'min', 'max', 'cardinality'];
which = which || 'metric';
const schema = schemaMap[which];
return {
min: schema.min,
max: schema.max,
name: which,
title: schema.title,
group: 'metrics',
aggFilter: typeOptions,
// AggParams object
params: []
};
}
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($rootScope, $compile) {
$parentScope.agg = {
id: 1,
params: {},
schema: makeConfig()
};
$parentScope.groupName = 'metrics';
$parentScope.group = [{
id: '1',
schema: makeConfig()
}, {
id: '2',
schema: makeConfig('radius')
}];
// share the scope
_.defaults($parentScope, $rootScope, Object.getPrototypeOf($rootScope));
// make the element
$elem = angular.element(
'<ng-form ><div vis-editor-agg ng-model="name"></div></ng-form>'
);
// compile the html
$compile($elem)($parentScope);
// Digest everything
$elem.scope().$digest();
}));
it('should only add the close button if there is more than the minimum', function () {
expect($parentScope.canRemove($parentScope.agg)).to.be(false);
$parentScope.group.push({
id: '3',
schema: makeConfig()
});
expect($parentScope.canRemove($parentScope.agg)).to.be(true);
});
});

View file

@ -1,63 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import angular from 'angular';
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import sinon from 'sinon';
import { Direction } from '../keyboard_move';
import { keyCodes } from '@elastic/eui';
describe('keyboardMove directive', () => {
let $compile;
let $rootScope;
function createTestButton(callback) {
const scope = $rootScope.$new();
scope.callback = callback;
return $compile('<button keyboard-move="callback(direction)">Test</button>')(scope);
}
function createKeydownEvent(keyCode) {
const e = angular.element.Event('keydown'); // eslint-disable-line new-cap
e.which = keyCode;
return e;
}
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject((_$rootScope_, _$compile_) => {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('should call the callback when pressing up', () => {
const spy = sinon.spy();
const button = createTestButton(spy);
button.trigger(createKeydownEvent(keyCodes.UP));
expect(spy.calledWith(Direction.up)).to.be(true);
});
it('should call the callback when pressing down', () => {
const spy = sinon.spy();
const button = createTestButton(spy);
button.trigger(createKeydownEvent(keyCodes.DOWN));
expect(spy.calledWith(Direction.down)).to.be(true);
});
});

View file

@ -25,13 +25,6 @@
}
}
/**
* 1. Hack to split child elements evenly.
*/
.visEditorAgg__formRow--split {
flex: 1 1 0 !important; /* 1 */
}
.visEditorAgg__sliderValue {
@include euiFontSize;
align-self: center;

View file

@ -1,7 +0,0 @@
.visEditorAggSelect__helpLink {
@include euiFontSizeXS;
}
.visEditorAggSelect__formRow {
margin-bottom: $euiSizeS;
}

View file

@ -10,4 +10,3 @@ $vis-editor-resizer-width: $euiSizeM;
// Components
@import './agg';
@import './agg_params';
@import './agg_select';

View file

@ -119,7 +119,7 @@
// Collapsible section
.visEditorSidebar__collapsible {
background-color: transparentize($euiColorLightShade, .85);
background-color: lightOrDarkTheme($euiPageBackgroundColor, $euiColorLightestShade);
}
.visEditorSidebar__collapsible--margin {
@ -170,12 +170,6 @@
@include euiTextTruncate;
}
.visEditorSidebar__collapsibleTitleDescription--danger {
color: $euiColorDanger;
font-weight: $euiFontWeightBold;
}
//
// FORMS
//
@ -225,3 +219,11 @@
margin-top: $euiSizeS;
margin-bottom: $euiSizeS;
}
.visEditorSidebar__aggGroupAccordionButtonContent {
font-size: $euiFontSizeS;
span {
color: $euiColorDarkShade;
}
}

View file

@ -1,143 +0,0 @@
<div class="visEditorSidebar__section visEditorSidebar__collapsible visEditorSidebar__collapsible--marginBottom">
<!-- header -->
<div class="visEditorSidebar__collapsibleTitle">
<!-- open/close editor -->
<button
aria-label="{{::'common.ui.vis.editors.agg.toggleEditorButtonAriaLabel' | i18n: { defaultMessage: 'Toggle {schema} editor', values: { schema: agg.schema.title } } }}"
ng-click="editorOpen = !editorOpen"
aria-expanded="{{ !!editorOpen }}"
aria-controls="visAggEditorParams{{agg.id}}"
type="button"
data-test-subj="toggleEditor"
class="visEditorSidebar__collapsibleTitleLabel">
<icon aria-hidden="true" ng-if="editorOpen" type="'arrowDown'" size="'s'"></icon>
<icon aria-hidden="true" ng-if="!editorOpen" type="'arrowRight'" size="'s'"></icon>
<!-- title -->
<span class="visEditorSidebar__collapsibleTitleText">
{{ agg.schema.title }}
</span>
</button>
<!-- description -->
<span ng-if="!editorOpen && aggForm.softErrorCount() < 1" class="visEditorSidebar__collapsibleTitleDescription" title="{{describe()}}">
{{ describe() }}
</span>
<!-- error -->
<span
ng-if="!editorOpen && aggForm.softErrorCount() > 0"
class="visEditorSidebar__collapsibleTitleDescription visEditorSidebar__collapsibleTitleDescription--danger"
title="{{::'common.ui.vis.editors.agg.errorsText' | i18n: { defaultMessage: 'Errors' } }}"
i18n-id="common.ui.vis.editors.agg.errorsText"
i18n-default-message="Errors"
>
</span>
<!-- controls !!!actually disabling buttons will break tooltips¡¡¡ -->
<div class="visEditorAggHeader__controls kuiButtonGroup kuiButtonGroup--united">
<!-- disable aggregation -->
<button
ng-if="agg.enabled && canRemove(agg)"
ng-click="agg.enabled = false"
aria-label="{{::'common.ui.vis.editors.agg.disableAggButtonAriaLabel' | i18n: { defaultMessage: 'Disable aggregation' } }}"
tooltip="{{::'common.ui.vis.editors.agg.disableAggButtonTooltip' | i18n: { defaultMessage: 'Disable aggregation' } }}"
tooltip-append-to-body="true"
data-test-subj="disableAggregationBtn"
type="button"
class="kuiButton kuiButton--basic kuiButton--small">
<i aria-hidden="true" class="fa fa-toggle-on"></i>
</button>
<!-- enable aggregation -->
<button
ng-if="!agg.enabled"
ng-click="agg.enabled = true"
aria-label="{{::'common.ui.vis.editors.agg.enableAggButtonAriaLabel' | i18n: { defaultMessage: 'Enable aggregation' } }}"
tooltip="{{::'common.ui.vis.editors.agg.enableAggButtonTooltip' | i18n: { defaultMessage: 'Enable aggregation' } }}"
tooltip-append-to-body="true"
data-test-subj="disableAggregationBtn"
type="button"
class="kuiButton kuiButton--basic kuiButton--small">
<i aria-hidden="true" class="fa fa-toggle-off"></i>
</button>
<!-- drag handle -->
<button
draggable-handle
ng-if="stats.count > 1"
tooltip="{{::'common.ui.vis.editors.agg.modifyPriorityButtonTooltip' | i18n: { defaultMessage: 'Modify Priority by Dragging' } }}"
tooltip-append-to-body="true"
type="button"
keyboard-move="onPriorityReorder(direction)"
class="kuiButton kuiButton--basic kuiButton--small">
<i aria-hidden="true" class="fa fa-arrows-v"></i>
<span class="euiScreenReaderOnly"
i18n-id="common.ui.vis.editors.howToModifyScreenReaderPriorityDescription"
i18n-default-message="Use up and down key on this button to move this aggregation up and down in the priority order."
>
</span>
</button>
<!-- remove button -->
<button
data-test-subj="removeDimensionBtn"
ng-if="canRemove(agg)"
aria-label="{{::'common.ui.vis.editors.agg.removeDimensionButtonAriaLabel' | i18n: { defaultMessage: 'Remove Dimension' } }}"
ng-if="stats.count > stats.min"
ng-click="remove(agg)"
tooltip="{{::'common.ui.vis.editors.agg.removeDimensionButtonTooltip' | i18n: { defaultMessage: 'Remove Dimension' } }}"
tooltip-append-to-body="true"
type="button"
class="kuiButton kuiButton--basic kuiButton--small">
<icon aria-hidden="true" color="'danger'" type="'cross'" size="'m'"></icon>
</button>
</div>
</div>
<vis-agg-control-react-wrapper
ng-if="agg.schema.editorComponent"
ng-show="editorOpen"
agg-params="agg.params"
component="agg.schema.editorComponent"
editor-state-params="state.params"
set-value="onAggParamsChange"
/>
<!-- should be rendered after link function run, i.e. 'onAggTypeChange' is defined -->
<vis-editor-agg-params
ng-if="onAggTypeChange"
ng-show="editorOpen"
agg="agg"
agg-index="$index"
agg-is-too-low="aggIsTooLow"
agg-params="agg.params"
agg-error="error"
disabled-params="disabledParams"
editor-config="editorConfig"
form-is-touched="formIsTouched"
group-name="groupName"
index-pattern="vis.indexPattern"
metric-aggs="metricAggs"
state="state"
on-agg-type-change="onAggTypeChange"
on-agg-params-change="onAggParamsChange"
set-touched="setTouched"
set-validity="setValidity">
</vis-editor-agg-params>
</div>
<vis-editor-agg-add
ng-if="$index + 1 === stats.count"
ng-hide="dragging"
group="group"
group-name="groupName"
schemas="schemas"
stats="stats"
add-schema="addSchema">
</vis-editor-agg-add>

View file

@ -1,188 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
import './agg_params';
import './agg_add';
import './controls/agg_controls';
import { Direction } from './keyboard_move';
import _ from 'lodash';
import './fancy_forms';
import { uiModules } from '../../../modules';
import aggTemplate from './agg.html';
import { move } from '../../../utils/collection';
uiModules
.get('app/visualize')
.directive('visEditorAgg', () => {
return {
restrict: 'A',
template: aggTemplate,
require: ['^form', '^ngModel'],
link: function ($scope, $el, attrs, [kbnForm, ngModelCtrl]) {
$scope.editorOpen = !!$scope.agg.brandNew;
$scope.aggIsTooLow = false;
$scope.$watch('editorOpen', function (open) {
// make sure that all of the form inputs are "touched"
// so that their errors propagate
if (!open) kbnForm.$setTouched();
});
$scope.$watchMulti([
'$index',
'group.length'
], function () {
$scope.aggIsTooLow = calcAggIsTooLow();
});
if ($scope.groupName === 'buckets') {
$scope.$watchMulti([
'$last',
'lastParentPipelineAggTitle',
'agg.type'
], function ([isLastBucket, lastParentPipelineAggTitle, aggType]) {
$scope.error = null;
$scope.disabledParams = [];
if (!lastParentPipelineAggTitle || !isLastBucket || !aggType) {
return;
}
if (['date_histogram', 'histogram'].includes(aggType.name)) {
$scope.onAggParamsChange(
$scope.agg.params,
'min_doc_count',
// "histogram" agg has an editor for "min_doc_count" param, which accepts boolean
// "date_histogram" agg doesn't have an editor for "min_doc_count" param, it should be set as a numeric value
aggType.name === 'histogram' ? true : 0);
$scope.disabledParams = ['min_doc_count'];
} else {
$scope.error = i18n.translate('common.ui.aggTypes.metrics.wrongLastBucketTypeErrorMessage', {
defaultMessage: 'Last bucket aggregation must be "Date Histogram" or "Histogram" when using "{type}" metric aggregation.',
values: { type: lastParentPipelineAggTitle },
description: 'Date Histogram and Histogram should not be translated',
});
}
});
}
/**
* Describe the aggregation, for display in the collapsed agg header
* @return {[type]} [description]
*/
$scope.describe = function () {
if (!$scope.agg.type || !$scope.agg.type.makeLabel) return '';
const label = $scope.agg.type.makeLabel($scope.agg);
return label ? label : '';
};
$scope.$on('drag-start', () => {
$scope.editorWasOpen = $scope.editorOpen;
$scope.editorOpen = false;
$scope.$emit('agg-drag-start', $scope.agg);
});
$scope.$on('drag-end', () => {
$scope.editorOpen = $scope.editorWasOpen;
$scope.$emit('agg-drag-end', $scope.agg);
});
/**
* Move aggregations down/up in the priority list by pressing arrow keys.
*/
$scope.onPriorityReorder = function (direction) {
const positionOffset = direction === Direction.down ? 1 : -1;
const currentPosition = $scope.group.indexOf($scope.agg);
const newPosition = Math.max(0, Math.min(currentPosition + positionOffset, $scope.group.length - 1));
move($scope.group, currentPosition, newPosition);
$scope.$emit('agg-reorder');
};
$scope.remove = function (agg) {
const aggs = $scope.state.aggs;
const index = aggs.indexOf(agg);
if (index === -1) {
return;
}
aggs.splice(index, 1);
};
$scope.canRemove = function (aggregation) {
const metricCount = _.reduce($scope.group, function (count, agg) {
return (agg.schema.name === aggregation.schema.name) ? ++count : count;
}, 0);
// make sure the the number of these aggs is above the min
return metricCount > aggregation.schema.min;
};
function calcAggIsTooLow() {
if (!$scope.agg.schema.mustBeFirst) {
return false;
}
const firstDifferentSchema = _.findIndex($scope.group, function (agg) {
return agg.schema !== $scope.agg.schema;
});
if (firstDifferentSchema === -1) {
return false;
}
return $scope.$index > firstDifferentSchema;
}
// The model can become touched either onBlur event or when the form is submitted.
// We watch $touched to identify when the form is submitted.
$scope.$watch(() => {
return ngModelCtrl.$touched;
}, (value) => {
$scope.formIsTouched = value;
}, true);
$scope.onAggTypeChange = (agg, value) => {
if (agg.type !== value) {
agg.type = value;
}
};
$scope.onAggParamsChange = (params, paramName, value) => {
if (params[paramName] !== value) {
params[paramName] = value;
}
};
$scope.setValidity = (isValid) => {
ngModelCtrl.$setValidity(`aggParams${$scope.agg.id}`, isValid);
};
$scope.setTouched = (isTouched) => {
if (isTouched) {
ngModelCtrl.$setTouched();
} else {
ngModelCtrl.$setUntouched();
}
};
}
};
});

View file

@ -1,25 +0,0 @@
<div class="visEditorSidebar__section">
<div class="visEditorSidebar__sectionTitle">
{{ groupNameLabel }}
</div>
<div ng-class="groupName" draggable-container="group">
<div ng-repeat="agg in group track by agg.id" data-test-subj="aggregationEditor{{agg.id}}" draggable-item="agg">
<!-- agg.html - controls for aggregation -->
<ng-form name="aggForm">
<div vis-editor-agg ng-model="_internalNgModelState"></div>
</ng-form>
</div>
<vis-editor-agg-add
ng-if="stats.count === 0"
group="group"
group-name="groupName"
schemas="schemas"
stats="stats"
add-schema="addSchema">
</vis-editor-agg-add>
</div>
</div>
<br>

View file

@ -17,79 +17,80 @@
* under the License.
*/
import _ from 'lodash';
import './agg';
import './agg_add';
import 'ngreact';
import { wrapInI18nContext } from 'ui/i18n';
import { uiModules } from '../../../modules';
import aggGroupTemplate from './agg_group.html';
import { move } from '../../../utils/collection';
import { aggGroupNameMaps } from './agg_group_names';
import { AggConfig } from '../../agg_config';
import '../../draggable/draggable_container';
import '../../draggable/draggable_item';
import '../../draggable/draggable_handle';
import { DefaultEditorAggGroup } from './components/default_editor_agg_group';
uiModules
.get('app/visualize')
.directive('visEditorAggGroupWrapper', reactDirective =>
reactDirective(wrapInI18nContext(DefaultEditorAggGroup), [
['metricAggs', { watchDepth: 'reference' }], // we watch reference to identify each aggs change in useEffects
['schemas', { watchDepth: 'collection' }],
['state', { watchDepth: 'reference' }],
['addSchema', { watchDepth: 'reference' }],
['onAggParamsChange', { watchDepth: 'reference' }],
['onAggTypeChange', { watchDepth: 'reference' }],
['onToggleEnableAgg', { watchDepth: 'reference' }],
['removeAgg', { watchDepth: 'reference' }],
['reorderAggs', { watchDepth: 'reference' }],
['setTouched', { watchDepth: 'reference' }],
['setValidity', { watchDepth: 'reference' }],
'groupName',
'formIsTouched',
'lastParentPipelineAggTitle',
])
)
.directive('visEditorAggGroup', function () {
return {
restrict: 'E',
template: aggGroupTemplate,
scope: true,
link: function ($scope, $el, attr) {
require: '?^ngModel',
template: function () {
return `<vis-editor-agg-group-wrapper
ng-if="setValidity"
form-is-touched="formIsTouched"
group-name="groupName"
last-parent-pipeline-agg-title="lastParentPipelineAggTitle"
metric-aggs="metricAggs"
state="state"
schemas="schemas"
add-schema="addSchema"
on-agg-params-change="onAggParamsChange"
on-agg-type-change="onAggTypeChange"
on-toggle-enable-agg="onToggleEnableAgg"
remove-agg="removeAgg"
reorder-aggs="reorderAggs"
set-validity="setValidity"
set-touched="setTouched"
></vis-editor-agg-group-wrapper>`;
},
link: function ($scope, $el, attr, ngModelCtrl) {
$scope.groupName = attr.groupName;
$scope.groupNameLabel = aggGroupNameMaps()[$scope.groupName];
$scope.$bind('group', 'state.aggs.bySchemaGroup["' + $scope.groupName + '"]');
$scope.$bind('schemas', 'vis.type.schemas["' + $scope.groupName + '"]');
$scope.$bind('schemas', attr.schemas);
// The model can become touched either onBlur event or when the form is submitted.
// We also watch $touched to identify when the form is submitted.
$scope.$watch(
() => {
return ngModelCtrl.$touched;
},
value => {
$scope.formIsTouched = value;
}
);
$scope.$watchMulti([
'schemas',
'[]group'
], function () {
const stats = $scope.stats = {
min: 0,
max: 0,
count: $scope.group ? $scope.group.length : 0
};
if (!$scope.schemas) return;
$scope.schemas.forEach(function (schema) {
stats.min += schema.min;
stats.max += schema.max;
stats.deprecate = schema.deprecate;
});
});
function reorderFinished() {
//the aggs have been reordered in [group] and we need
//to apply that ordering to [vis.aggs]
const indexOffset = $scope.state.aggs.indexOf($scope.group[0]);
_.forEach($scope.group, (agg, index) => {
move($scope.state.aggs, agg, indexOffset + index);
});
}
$scope.$on('agg-reorder', reorderFinished);
$scope.$on('agg-drag-start', () => $scope.dragging = true);
$scope.$on('agg-drag-end', () => {
$scope.dragging = false;
reorderFinished();
});
$scope.addSchema = function (schema) {
const aggConfig = new AggConfig($scope.state.aggs, {
schema,
id: AggConfig.nextId($scope.state.aggs),
});
aggConfig.brandNew = true;
$scope.state.aggs.push(aggConfig);
$scope.setValidity = isValid => {
ngModelCtrl.$setValidity(`aggGroup${$scope.groupName}`, isValid);
};
}
};
$scope.setTouched = isTouched => {
if (isTouched) {
ngModelCtrl.$setTouched();
} else {
ngModelCtrl.$setUntouched();
}
};
},
};
});

View file

@ -1,25 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { i18n } from '@kbn/i18n';
export const aggGroupNameMaps = () => ({
metrics: i18n.translate('common.ui.vis.editors.aggGroups.metricsText', { defaultMessage: 'metrics' }),
buckets: i18n.translate('common.ui.vis.editors.aggGroups.bucketsText', { defaultMessage: 'buckets' })
});

View file

@ -17,7 +17,18 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
export enum AggGroupNames {
Buckets = 'buckets',
Metrics = 'metrics',
}
export const aggGroupNamesMap = () => ({
[AggGroupNames.Metrics]: i18n.translate('common.ui.vis.editors.aggGroups.metricsText', {
defaultMessage: 'Metrics',
}),
[AggGroupNames.Buckets]: i18n.translate('common.ui.vis.editors.aggGroups.bucketsText', {
defaultMessage: 'Buckets',
}),
});

View file

@ -1,43 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import 'ngreact';
import { wrapInI18nContext } from 'ui/i18n';
import { uiModules } from '../../../modules';
import { DefaultEditorAggParams } from './components/default_editor_agg_params';
uiModules
.get('app/visualize')
.directive('visEditorAggParams', reactDirective => reactDirective(wrapInI18nContext(DefaultEditorAggParams), [
['agg', { watchDepth: 'reference' }],
['aggParams', { watchDepth: 'collection' }],
['indexPattern', { watchDepth: 'reference' }],
['metricAggs', { watchDepth: 'reference' }], // we watch reference to identify each aggs change in useEffects
['state', { watchDepth: 'reference' }],
['onAggTypeChange', { watchDepth: 'reference' }],
['onAggParamsChange', { watchDepth: 'reference' }],
['setTouched', { watchDepth: 'reference' }],
['setValidity', { watchDepth: 'reference' }],
'aggError',
'aggIndex',
'disabledParams',
'groupName',
'aggIsTooLow',
'formIsTouched',
]));

View file

@ -46,39 +46,43 @@ exports[`DefaultEditorAggParams component should init with the default set of pa
}
}
/>
<EuiAccordion
buttonContent="Advanced"
id="advancedAccordion"
initialIsOpen={false}
paddingSize="none"
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
labelType="label"
>
<EuiSpacer
size="m"
/>
<DefaultEditorAggParam
aggParam={
Object {
"advanced": true,
"name": "json",
"type": "json",
<EuiAccordion
buttonContent="Advanced"
id="advancedAccordion"
initialIsOpen={false}
paddingSize="none"
>
<EuiSpacer
size="s"
/>
<DefaultEditorAggParam
aggParam={
Object {
"advanced": true,
"name": "json",
"type": "json",
}
}
}
key="jsonundefined"
onChange={[MockFunction]}
setTouched={[Function]}
setValidity={[Function]}
showValidation={false}
subAggParams={
Object {
"formIsTouched": false,
"onAggParamsChange": [MockFunction],
"onAggTypeChange": [MockFunction],
key="jsonundefined"
onChange={[MockFunction]}
setTouched={[Function]}
setValidity={[Function]}
showValidation={false}
subAggParams={
Object {
"formIsTouched": false,
"onAggParamsChange": [MockFunction],
"onAggTypeChange": [MockFunction],
}
}
}
/>
</EuiAccordion>
<EuiSpacer
size="m"
/>
/>
</EuiAccordion>
</EuiFormRow>
</EuiForm>
`;

View file

@ -0,0 +1,263 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useEffect } from 'react';
import {
EuiAccordion,
EuiToolTip,
EuiButtonIcon,
EuiSpacer,
EuiIconTip,
Color,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AggConfig } from '../../../';
import { DefaultEditorAggParams } from './default_editor_agg_params';
import { DefaultEditorAggCommonProps } from './default_editor_agg_common_props';
interface DefaultEditorAggProps extends DefaultEditorAggCommonProps {
agg: AggConfig;
aggIndex: number;
aggIsTooLow: boolean;
dragHandleProps: {} | null;
isDraggable: boolean;
isLastBucket: boolean;
isRemovable: boolean;
}
function DefaultEditorAgg({
agg,
aggIndex,
aggIsTooLow,
dragHandleProps,
formIsTouched,
groupName,
isDraggable,
isLastBucket,
isRemovable,
metricAggs,
lastParentPipelineAggTitle,
state,
onAggParamsChange,
onAggTypeChange,
onToggleEnableAgg,
removeAgg,
setTouched,
setValidity,
}: DefaultEditorAggProps) {
const [isEditorOpen, setIsEditorOpen] = useState(agg.brandNew);
const [validState, setValidState] = useState(true);
const showDescription = !isEditorOpen && validState;
const showError = !isEditorOpen && !validState;
let disabledParams;
let aggError;
// When a Parent Pipeline agg is selected and this agg is the last bucket.
const isLastBucketAgg = isLastBucket && lastParentPipelineAggTitle && agg.type;
const SchemaComponent = agg.schema.editorComponent;
if (isLastBucketAgg) {
if (['date_histogram', 'histogram'].includes(agg.type.name)) {
disabledParams = ['min_doc_count'];
} else {
aggError = i18n.translate('common.ui.aggTypes.metrics.wrongLastBucketTypeErrorMessage', {
defaultMessage:
'Last bucket aggregation must be "Date Histogram" or "Histogram" when using "{type}" metric aggregation.',
values: { type: lastParentPipelineAggTitle },
description: 'Date Histogram and Histogram should not be translated',
});
}
}
useEffect(() => {
if (isLastBucketAgg && ['date_histogram', 'histogram'].includes(agg.type.name)) {
onAggParamsChange(
agg.params,
'min_doc_count',
// "histogram" agg has an editor for "min_doc_count" param, which accepts boolean
// "date_histogram" agg doesn't have an editor for "min_doc_count" param, it should be set as a numeric value
agg.type.name === 'histogram' ? true : 0
);
}
}, [lastParentPipelineAggTitle, isLastBucket, agg.type]);
// A description of the aggregation, for displaying in the collapsed agg header
const aggDescription = agg.type && agg.type.makeLabel ? agg.type.makeLabel(agg) : '';
const onToggle = (isOpen: boolean) => {
setIsEditorOpen(isOpen);
if (!isOpen) {
setTouched(true);
}
};
const onSetValidity = (isValid: boolean) => {
setValidity(isValid);
setValidState(isValid);
};
const renderAggButtons = () => {
const actionIcons = [];
if (showError) {
actionIcons.push({
id: 'hasErrors',
color: 'danger',
type: 'alert',
tooltip: i18n.translate('common.ui.vis.editors.agg.errorsAriaLabel', {
defaultMessage: 'Aggregation has errors',
}),
dataTestSubj: 'hasErrorsAggregationIcon',
});
}
if (agg.enabled && isRemovable) {
actionIcons.push({
id: 'disableAggregation',
color: 'text',
type: 'eye',
onClick: () => onToggleEnableAgg(agg, false),
tooltip: i18n.translate('common.ui.vis.editors.agg.disableAggButtonTooltip', {
defaultMessage: 'Disable aggregation',
}),
dataTestSubj: 'toggleDisableAggregationBtn',
});
}
if (!agg.enabled) {
actionIcons.push({
id: 'enableAggregation',
color: 'text',
type: 'eyeClosed',
onClick: () => onToggleEnableAgg(agg, true),
tooltip: i18n.translate('common.ui.vis.editors.agg.enableAggButtonTooltip', {
defaultMessage: 'Enable aggregation',
}),
dataTestSubj: 'toggleDisableAggregationBtn',
});
}
if (isDraggable) {
actionIcons.push({
id: 'dragHandle',
type: 'grab',
tooltip: i18n.translate('common.ui.vis.editors.agg.modifyPriorityButtonTooltip', {
defaultMessage: 'Modify priority by dragging',
}),
dataTestSubj: 'dragHandleBtn',
});
}
if (isRemovable) {
actionIcons.push({
id: 'removeDimension',
color: 'danger',
type: 'cross',
onClick: () => removeAgg(agg),
tooltip: i18n.translate('common.ui.vis.editors.agg.removeDimensionButtonTooltip', {
defaultMessage: 'Remove dimension',
}),
dataTestSubj: 'removeDimensionBtn',
});
}
return (
<div {...dragHandleProps}>
{actionIcons.map(icon => {
if (icon.id === 'dragHandle') {
return (
<EuiIconTip
key={icon.id}
type={icon.type}
content={icon.tooltip}
iconProps={{
['aria-label']: icon.tooltip,
['data-test-subj']: icon.dataTestSubj,
}}
position="bottom"
/>
);
}
return (
<EuiToolTip key={icon.id} position="bottom" content={icon.tooltip}>
<EuiButtonIcon
iconType={icon.type}
color={icon.color as Color}
onClick={icon.onClick}
aria-label={icon.tooltip}
data-test-subj={icon.dataTestSubj}
/>
</EuiToolTip>
);
})}
</div>
);
};
const buttonContent = (
<>
{agg.schema.title} {showDescription && <span>{aggDescription}</span>}
</>
);
return (
<EuiAccordion
id={`visEditorAggAccordion${agg.id}`}
initialIsOpen={isEditorOpen}
buttonContent={buttonContent}
buttonClassName="eui-textTruncate"
buttonContentClassName="visEditorSidebar__aggGroupAccordionButtonContent eui-textTruncate"
className="visEditorSidebar__section visEditorSidebar__collapsible visEditorSidebar__collapsible--marginBottom"
aria-label={i18n.translate('common.ui.vis.editors.agg.toggleEditorButtonAriaLabel', {
defaultMessage: 'Toggle {schema} editor',
values: { schema: agg.schema.title },
})}
data-test-subj={`visEditorAggAccordion${agg.id}`}
extraAction={renderAggButtons()}
onToggle={onToggle}
>
<>
<EuiSpacer size="m" />
{SchemaComponent && (
<SchemaComponent
aggParams={agg.params}
editorStateParams={state.params}
setValue={onAggParamsChange}
/>
)}
<DefaultEditorAggParams
agg={agg}
aggError={aggError}
aggIndex={aggIndex}
aggIsTooLow={aggIsTooLow}
disabledParams={disabledParams}
formIsTouched={formIsTouched}
groupName={groupName}
indexPattern={agg.getIndexPattern()}
metricAggs={metricAggs}
state={state}
onAggParamsChange={onAggParamsChange}
onAggTypeChange={onAggTypeChange}
setTouched={setTouched}
setValidity={onSetValidity}
/>
</>
</EuiAccordion>
);
}
export { DefaultEditorAgg };

View file

@ -39,9 +39,7 @@ interface DefaultEditorAggAddProps {
schemas: Schema[];
stats: {
max: number;
min: number;
count: number;
deprecate: boolean;
};
addSchema(schema: Schema): void;
}
@ -80,7 +78,7 @@ function DefaultEditorAggAdd({
return count >= schema.max;
};
return stats.max > stats.count ? (
return (
<EuiFlexGroup justifyContent="center" responsive={false}>
<EuiFlexItem grow={false}>
<EuiPopover
@ -92,14 +90,14 @@ function DefaultEditorAggAdd({
closePopover={() => setIsPopoverOpen(false)}
>
<EuiPopoverTitle>
{(groupName !== AggGroupNames.Buckets || (!stats.count && !stats.deprecate)) && (
{(groupName !== AggGroupNames.Buckets || !stats.count) && (
<FormattedMessage
id="common.ui.vis.editors.aggAdd.addGroupButtonLabel"
defaultMessage="Add {groupNameLabel}"
values={{ groupNameLabel }}
/>
)}
{groupName === AggGroupNames.Buckets && stats.count > 0 && !stats.deprecate && (
{groupName === AggGroupNames.Buckets && stats.count > 0 && (
<FormattedMessage
id="common.ui.vis.editors.aggAdd.addSubGroupButtonLabel"
defaultMessage="Add sub-{groupNameLabel}"
@ -108,24 +106,21 @@ function DefaultEditorAggAdd({
)}
</EuiPopoverTitle>
<EuiContextMenuPanel
items={schemas.map(
schema =>
!schema.deprecate && (
<EuiContextMenuItem
key={`${schema.name}_${schema.title}`}
data-test-subj={`visEditorAdd_${groupName}_${schema.title}`}
disabled={isPopoverOpen && isSchemaDisabled(schema)}
onClick={() => onSelectSchema(schema)}
>
{schema.title}
</EuiContextMenuItem>
)
)}
items={schemas.map(schema => (
<EuiContextMenuItem
key={`${schema.name}_${schema.title}`}
data-test-subj={`visEditorAdd_${groupName}_${schema.title}`}
disabled={isPopoverOpen && isSchemaDisabled(schema)}
onClick={() => onSelectSchema(schema)}
>
{schema.title}
</EuiContextMenuItem>
))}
/>
</EuiPopover>
</EuiFlexItem>
</EuiFlexGroup>
) : null;
);
}
export { DefaultEditorAggAdd };

View file

@ -17,18 +17,26 @@
* under the License.
*/
import { uiModules } from '../../../modules';
import { DefaultEditorAggAdd } from './components/default_editor_agg_add';
import { wrapInI18nContext } from 'ui/i18n';
import { AggType } from 'ui/agg_types';
import { AggConfig, VisState, AggParams, VisParams } from '../../../';
import { AggGroupNames } from '../agg_groups';
uiModules
.get('kibana')
.directive('visEditorAggAdd', reactDirective =>
reactDirective(wrapInI18nContext(DefaultEditorAggAdd), [
['group', { watchDepth: 'collection' }],
['schemas', { watchDepth: 'collection' }],
['stats', { watchDepth: 'reference' }],
'groupName',
'addSchema'
])
);
export type OnAggParamsChange = (
params: AggParams | VisParams,
paramName: string,
value: unknown
) => void;
export interface DefaultEditorAggCommonProps {
formIsTouched: boolean;
groupName: AggGroupNames;
lastParentPipelineAggTitle?: string;
metricAggs: AggConfig[];
state: VisState;
onAggParamsChange: OnAggParamsChange;
onAggTypeChange: (agg: AggConfig, aggType: AggType) => void;
onToggleEnableAgg: (agg: AggConfig, isEnable: boolean) => void;
removeAgg: (agg: AggConfig) => void;
setTouched: (isTouched: boolean) => void;
setValidity: (isValid: boolean) => void;
}

View file

@ -0,0 +1,193 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useReducer } from 'react';
import {
EuiTitle,
EuiDragDropContext,
EuiDroppable,
EuiDraggable,
EuiSpacer,
EuiPanel,
} from '@elastic/eui';
import { AggConfig } from '../../../agg_config';
import { aggGroupNamesMap, AggGroupNames } from '../agg_groups';
import { DefaultEditorAgg } from './default_editor_agg';
import { DefaultEditorAggAdd } from './default_editor_agg_add';
import { DefaultEditorAggCommonProps } from './default_editor_agg_common_props';
import {
isInvalidAggsTouched,
isAggRemovable,
calcAggIsTooLow,
} from './default_editor_agg_group_helper';
import { aggGroupReducer, initAggsState, AGGS_ACTION_KEYS } from './default_editor_agg_group_state';
import { Schema } from '../schemas';
interface DefaultEditorAggGroupProps extends DefaultEditorAggCommonProps {
schemas: Schema[];
addSchema: (schems: Schema) => void;
reorderAggs: (group: AggConfig[]) => void;
}
function DefaultEditorAggGroup({
formIsTouched,
groupName,
lastParentPipelineAggTitle,
metricAggs,
state,
schemas = [],
addSchema,
onAggParamsChange,
onAggTypeChange,
onToggleEnableAgg,
removeAgg,
reorderAggs,
setTouched,
setValidity,
}: DefaultEditorAggGroupProps) {
const groupNameLabel = aggGroupNamesMap()[groupName];
// e.g. buckets can have no aggs
const group: AggConfig[] = state.aggs.bySchemaGroup[groupName] || [];
const stats = {
max: 0,
count: group.length,
};
schemas.forEach((schema: Schema) => {
stats.max += schema.max;
});
const [aggsState, setAggsState] = useReducer(aggGroupReducer, group, initAggsState);
const isGroupValid = Object.values(aggsState).every(item => item.valid);
const isAllAggsTouched = isInvalidAggsTouched(aggsState);
useEffect(() => {
// when isAllAggsTouched is true, it means that all invalid aggs are touched and we will set ngModel's touched to true
// which indicates that Apply button can be changed to Error button (when all invalid ngModels are touched)
setTouched(isAllAggsTouched);
}, [isAllAggsTouched]);
useEffect(() => {
// when not all invalid aggs are touched and formIsTouched becomes true, it means that Apply button was clicked.
// and in such case we set touched state to true for all aggs
if (formIsTouched && !isAllAggsTouched) {
Object.keys(aggsState).map(([aggId]) => {
setAggsState({
type: AGGS_ACTION_KEYS.TOUCHED,
payload: true,
aggId: Number(aggId),
});
});
}
}, [formIsTouched]);
useEffect(() => {
setValidity(isGroupValid);
}, [isGroupValid]);
interface DragDropResultProps {
source: { index: number };
destination?: { index: number } | null;
}
const onDragEnd = ({ source, destination }: DragDropResultProps) => {
if (source && destination) {
const orderedGroup = Array.from(group);
const [removed] = orderedGroup.splice(source.index, 1);
orderedGroup.splice(destination.index, 0, removed);
reorderAggs(orderedGroup);
}
};
const setTouchedHandler = (aggId: number, touched: boolean) => {
setAggsState({
type: AGGS_ACTION_KEYS.TOUCHED,
payload: touched,
aggId,
});
};
const setValidityHandler = (aggId: number, valid: boolean) => {
setAggsState({
type: AGGS_ACTION_KEYS.VALID,
payload: valid,
aggId,
});
};
return (
<EuiDragDropContext onDragEnd={onDragEnd}>
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<div>{groupNameLabel}</div>
</EuiTitle>
<EuiSpacer size="s" />
<EuiDroppable droppableId={`agg_group_dnd_${groupName}`}>
<>
{group.map((agg: AggConfig, index: number) => (
<EuiDraggable
key={agg.id}
index={index}
draggableId={`agg_group_dnd_${groupName}_${agg.id}`}
customDragHandle={true}
>
{provided => (
<DefaultEditorAgg
agg={agg}
aggIndex={index}
aggIsTooLow={calcAggIsTooLow(agg, index, group)}
dragHandleProps={provided.dragHandleProps}
formIsTouched={aggsState[agg.id] ? aggsState[agg.id].touched : false}
groupName={groupName}
isDraggable={stats.count > 1}
isLastBucket={groupName === AggGroupNames.Buckets && index === group.length - 1}
isRemovable={isAggRemovable(agg, group)}
lastParentPipelineAggTitle={lastParentPipelineAggTitle}
metricAggs={metricAggs}
state={state}
onAggParamsChange={onAggParamsChange}
onAggTypeChange={onAggTypeChange}
onToggleEnableAgg={onToggleEnableAgg}
removeAgg={removeAgg}
setTouched={isTouched => setTouchedHandler(agg.id, isTouched)}
setValidity={isValid => setValidityHandler(agg.id, isValid)}
/>
)}
</EuiDraggable>
))}
</>
</EuiDroppable>
{stats.max > stats.count && (
<DefaultEditorAggAdd
group={group}
groupName={groupName}
schemas={schemas}
stats={stats}
addSchema={addSchema}
/>
)}
</EuiPanel>
</EuiDragDropContext>
);
}
export { DefaultEditorAggGroup };

View file

@ -0,0 +1,62 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { findIndex, reduce, isEmpty } from 'lodash';
import { AggConfig } from '../../../agg_config';
import { AggsState } from './default_editor_agg_group_state';
const isAggRemovable = (agg: AggConfig, group: AggConfig[]) => {
const metricCount = reduce(
group,
(count, aggregation: AggConfig) => {
return aggregation.schema.name === agg.schema.name ? ++count : count;
},
0
);
// make sure the the number of these aggs is above the min
return metricCount > agg.schema.min;
};
const calcAggIsTooLow = (agg: AggConfig, aggIndex: number, group: AggConfig[]) => {
if (!agg.schema.mustBeFirst) {
return false;
}
const firstDifferentSchema = findIndex(group, (aggr: AggConfig) => {
return aggr.schema !== agg.schema;
});
if (firstDifferentSchema === -1) {
return false;
}
return aggIndex > firstDifferentSchema;
};
function isInvalidAggsTouched(aggsState: AggsState) {
const invalidAggs = Object.values(aggsState).filter(agg => !agg.valid);
if (isEmpty(invalidAggs)) {
return false;
}
return invalidAggs.every(agg => agg.touched);
}
export { isAggRemovable, calcAggIsTooLow, isInvalidAggsTouched };

View file

@ -0,0 +1,62 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { AggConfig } from '../../../agg_config';
export enum AGGS_ACTION_KEYS {
TOUCHED = 'aggsTouched',
VALID = 'aggsValid',
}
interface AggsItem {
touched: boolean;
valid: boolean;
}
export interface AggsState {
[aggId: number]: AggsItem;
}
interface AggsAction {
type: AGGS_ACTION_KEYS;
payload: boolean;
aggId: number;
newState?: AggsState;
}
function aggGroupReducer(state: AggsState, action: AggsAction): AggsState {
const aggState = state[action.aggId] || { touched: false, valid: true };
switch (action.type) {
case AGGS_ACTION_KEYS.TOUCHED:
return { ...state, [action.aggId]: { ...aggState, touched: action.payload } };
case AGGS_ACTION_KEYS.VALID:
return { ...state, [action.aggId]: { ...aggState, valid: action.payload } };
default:
throw new Error();
}
}
function initAggsState(group: AggConfig[]): AggsState {
return group.reduce((state, agg) => {
state[agg.id] = { touched: false, valid: true };
return state;
}, {});
}
export { aggGroupReducer, initAggsState };

View file

@ -19,12 +19,12 @@
import React, { useEffect } from 'react';
import { AggParams } from '../agg_params';
import { AggParamEditorProps, AggParamCommonProps } from './default_editor_agg_param_props';
import { OnAggParamsChange } from './default_editor_agg_common_props';
interface DefaultEditorAggParamProps<T> extends AggParamCommonProps<T> {
paramEditor: React.ComponentType<AggParamEditorProps<T>>;
onChange(aggParams: AggParams, paramName: string, value?: T): void;
onChange: OnAggParamsChange;
}
function DefaultEditorAggParam<T>(props: DefaultEditorAggParamProps<T>) {

View file

@ -18,11 +18,11 @@
*/
import React, { useReducer, useEffect } from 'react';
import { EuiForm, EuiAccordion, EuiSpacer } from '@elastic/eui';
import { EuiForm, EuiAccordion, EuiSpacer, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { aggTypes, AggType, AggParam } from 'ui/agg_types';
import { AggConfig, VisState, AggParams } from 'ui/vis';
import { AggConfig, VisState } from 'ui/vis';
import { IndexPattern } from 'ui/index_patterns';
import { DefaultEditorAggSelect } from './default_editor_agg_select';
import { DefaultEditorAggParam } from './default_editor_agg_param';
@ -47,6 +47,7 @@ import { FixedParam, TimeIntervalParam, EditorParamConfig } from '../../config/t
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { useUnmount } from '../../../../../../../plugins/kibana_react/public/util/use_unmount';
import { AggGroupNames } from '../agg_groups';
import { OnAggParamsChange } from './default_editor_agg_common_props';
const FIXED_VALUE_PROP = 'fixedValue';
const DEFAULT_PROP = 'default';
@ -55,12 +56,12 @@ type EditorParamConfigType = EditorParamConfig & {
};
export interface SubAggParamsProp {
formIsTouched: boolean;
onAggParamsChange: (agg: AggParams, paramName: string, value: unknown) => void;
onAggParamsChange: OnAggParamsChange;
onAggTypeChange: (agg: AggConfig, aggType: AggType) => void;
}
export interface DefaultEditorAggParamsProps extends SubAggParamsProp {
agg: AggConfig;
aggError?: string | null;
aggError?: string;
aggIndex?: number;
aggIsTooLow?: boolean;
className?: string;
@ -227,7 +228,7 @@ function DefaultEditorAggParams({
})}
{params.advanced.length ? (
<>
<EuiFormRow>
<EuiAccordion
id="advancedAccordion"
buttonContent={i18n.translate(
@ -236,9 +237,8 @@ function DefaultEditorAggParams({
defaultMessage: 'Advanced',
}
)}
paddingSize="none"
>
<EuiSpacer size="m" />
<EuiSpacer size="s" />
{params.advanced.map((param: ParamInstance) => {
const model = paramsState[param.aggParam.name] || {
touched: false,
@ -247,8 +247,7 @@ function DefaultEditorAggParams({
return renderParam(param, model);
})}
</EuiAccordion>
<EuiSpacer size="m" />
</>
</EuiFormRow>
) : null}
</EuiForm>
);

View file

@ -172,12 +172,6 @@ describe('DefaultEditorAggParams helpers', () => {
expect(errors).toEqual(['"Split series" aggs must run before all other buckets!']);
});
it('should push an error if a schema is deprecated', () => {
const errors = getError({ schema: { title: 'Split series', deprecate: true } }, false);
expect(errors).toEqual(['"Split series" has been deprecated.']);
});
});
describe('getAggTypeOptions', () => {

View file

@ -108,16 +108,6 @@ function getError(agg: AggConfig, aggIsTooLow: boolean) {
})
);
}
if (agg.schema.deprecate) {
errors.push(
agg.schema.deprecateMessage
? agg.schema.deprecateMessage
: i18n.translate('common.ui.vis.editors.aggParams.errors.schemaIsDeprecatedErrorMessage', {
defaultMessage: '"{schema}" has been deprecated.',
values: { schema: agg.schema.title },
})
);
}
return errors;
}

View file

@ -19,7 +19,7 @@
import { get, has } from 'lodash';
import React, { useEffect } from 'react';
import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow, EuiLink } from '@elastic/eui';
import { EuiComboBox, EuiComboBoxOptionProps, EuiFormRow, EuiLink, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { AggType } from 'ui/agg_types';
@ -28,7 +28,7 @@ import { documentationLinks } from '../../../../documentation_links/documentatio
import { ComboBoxGroupedOption } from '../default_editor_utils';
interface DefaultEditorAggSelectProps {
aggError?: string | null;
aggError?: string;
aggTypeOptions: AggType[];
id: string;
indexPattern: IndexPattern;
@ -72,17 +72,14 @@ function DefaultEditorAggSelect({
}
const helpLink = value && aggHelpLink && (
<EuiLink
href={aggHelpLink}
target="_blank"
rel="noopener"
className="visEditorAggSelect__helpLink"
>
<FormattedMessage
id="common.ui.vis.defaultEditor.aggSelect.helpLinkLabel"
defaultMessage="{aggTitle} help"
values={{ aggTitle: value ? value.title : '' }}
/>
<EuiLink href={aggHelpLink} target="_blank" rel="noopener">
<EuiText size="xs">
<FormattedMessage
id="common.ui.vis.defaultEditor.aggSelect.helpLinkLabel"
defaultMessage="{aggTitle} help"
values={{ aggTitle: value ? value.title : '' }}
/>
</EuiText>
</EuiLink>
);

View file

@ -1,31 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { wrapInI18nContext } from 'ui/i18n';
import { uiModules } from '../../../../modules';
import { AggControlReactWrapper } from './agg_control_react_wrapper';
uiModules
.get('app/visualize')
.directive('visAggControlReactWrapper', reactDirective => reactDirective(wrapInI18nContext(AggControlReactWrapper), [
['aggParams', { watchDepth: 'collection' }],
['editorStateParams', { watchDepth: 'collection' }],
['component', { wrapApply: false }],
'setValue'
]));

View file

@ -18,6 +18,7 @@
*/
import 'ui/angular-bootstrap';
import './fancy_forms';
import './sidebar';
import { i18n } from '@kbn/i18n';
import './vis_options';

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { groupAggregationsBy } from '../default_editor_utils';
import { AggGroupNames } from '../agg_groups';
import { groupAggregationsBy } from './default_editor_utils';
import { AggGroupNames } from './agg_groups';
const aggs = [
{

View file

@ -1,75 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* The keyboardMove directive can be attached to elements, that can receive keydown events.
* It will call the passed callback function and pass the direction in which an
* arrow key was pressed to the callback (as the argument with the name `direction`).
* The passed value will be one of `Direction.up` or `Direction.down`, which can be
* imported to compare against those values. The directive will also make sure, that
* the pressed button will get the focus back (e.g. if it was lost due to a ng-repeat
* reordering).
*
* Usage example:
*
* <button keyboard-move="onMoved(direction)">...</button>
*
* import { Direction } from './keyboard_move';
* function onMoved(dir) {
* if (dir === Direction.up) {
* // moved up
* } else if (dir === Direction.down) {
* // moved down
* }
* }
*/
import { uiModules } from '../../../modules';
import { keyCodes } from '@elastic/eui';
export const Direction = {
up: 'up',
down: 'down'
};
const directionMapping = {
[keyCodes.UP]: Direction.up,
[keyCodes.DOWN]: Direction.down
};
uiModules.get('kibana')
.directive('keyboardMove', ($parse, $timeout) => ({
restrict: 'A',
link(scope, el, attr) {
const callbackFn = $parse(attr.keyboardMove);
el.keydown((ev) => {
if (ev.which in directionMapping) {
ev.preventDefault();
const direction = directionMapping[ev.which];
scope.$apply(() => callbackFn(scope, { direction }));
// Keep focus on that element, even though it might be attached somewhere
// else in the DOM (e.g. because it has a new position in an ng-repeat).
$timeout(() => el.focus());
}
});
scope.$on('$destroy', () => {
el.off('keydown');
});
}
}));

View file

@ -22,7 +22,6 @@ import { AggGroupNames } from './agg_groups';
export interface Schema {
aggFilter: string | string[];
deprecate: boolean;
editor: boolean | string;
group: AggGroupNames;
max: number;

View file

@ -51,7 +51,6 @@ class Schemas {
aggFilter: '*',
editor: false,
params: [],
deprecate: false
});
// convert the params into a params registry

View file

@ -151,10 +151,24 @@
<div class="visEditorSidebar__config" ng-show="sidebar.section == 'data'">
<!-- metrics -->
<vis-editor-agg-group ng-if="vis.type.schemas.metrics" data-test-subj="metricsAggGroup" group-name="metrics"></vis-editor-agg-group>
<vis-editor-agg-group
class="visEditorSidebar__aggGroup"
ng-if="vis.type.schemas.metrics"
group-name="metrics"
ng-model="_internalNgModelState"
data-test-subj="metricsAggGroup"
schemas="vis.type.schemas.metrics"
></vis-editor-agg-group>
<div class="euiSpacer euiSpacer--s"></div>
<!-- buckets -->
<vis-editor-agg-group ng-if="vis.type.schemas.buckets" data-test-subj="bucketsAggGroup" group-name="buckets"></vis-editor-agg-group>
<vis-editor-agg-group
class="visEditorSidebar__aggGroup"
ng-if="vis.type.schemas.buckets"
group-name="buckets"
ng-model="_internalNgModelState"
data-test-subj="bucketsAggGroup"
schemas="vis.type.schemas.buckets"
></vis-editor-agg-group>
</div>
<div class="visEditorSidebar__config" ng-repeat="tab in vis.type.editorConfig.optionTabs" ng-show="sidebar.section == tab.name">

View file

@ -23,36 +23,79 @@ import './vis_options';
import 'ui/directives/css_truncate';
import { uiModules } from '../../../modules';
import sidebarTemplate from './sidebar.html';
import { move } from '../../../utils/collection';
import { AggConfig } from '../../agg_config';
uiModules
.get('app/visualize')
.directive('visEditorSidebar', function () {
return {
restrict: 'E',
template: sidebarTemplate,
scope: true,
controllerAs: 'sidebar',
controller: function ($scope) {
$scope.$watch('vis.type', (visType) => {
if (visType) {
this.showData = visType.schemas.buckets || visType.schemas.metrics;
if (_.has(visType, 'editorConfig.optionTabs')) {
const activeTabs = visType.editorConfig.optionTabs.filter((tab) => {
return _.get(tab, 'active', false);
});
if (activeTabs.length > 0) {
this.section = activeTabs[0].name;
}
uiModules.get('app/visualize').directive('visEditorSidebar', function () {
return {
restrict: 'E',
template: sidebarTemplate,
scope: true,
require: '?^ngModel',
controllerAs: 'sidebar',
controller: function ($scope) {
$scope.$watch('vis.type', visType => {
if (visType) {
this.showData = visType.schemas.buckets || visType.schemas.metrics;
if (_.has(visType, 'editorConfig.optionTabs')) {
const activeTabs = visType.editorConfig.optionTabs.filter(tab => {
return _.get(tab, 'active', false);
});
if (activeTabs.length > 0) {
this.section = activeTabs[0].name;
}
this.section = this.section || (this.showData ? 'data' : _.get(visType, 'editorConfig.optionTabs[0].name'));
}
});
this.section =
this.section ||
(this.showData ? 'data' : _.get(visType, 'editorConfig.optionTabs[0].name'));
}
});
$scope.onAggParamsChange = (params, paramName, value) => {
if (params[paramName] !== value) {
params[paramName] = value;
}
};
}
};
});
$scope.onAggTypeChange = (agg, value) => {
if (agg.type !== value) {
agg.type = value;
}
};
$scope.onAggParamsChange = (params, paramName, value) => {
if (params[paramName] !== value) {
params[paramName] = value;
}
};
$scope.addSchema = function (schema) {
const aggConfig = new AggConfig($scope.state.aggs, {
schema,
id: AggConfig.nextId($scope.state.aggs),
});
aggConfig.brandNew = true;
$scope.state.aggs.push(aggConfig);
};
$scope.removeAgg = function (agg) {
const aggs = $scope.state.aggs;
const index = aggs.indexOf(agg);
if (index === -1) {
return;
}
aggs.splice(index, 1);
};
$scope.onToggleEnableAgg = (agg, isEnable) => {
agg.enabled = isEnable;
};
$scope.reorderAggs = (group) => {
//the aggs have been reordered in [group] and we need
//to apply that ordering to [vis.aggs]
const indexOffset = $scope.state.aggs.indexOf(group[0]);
_.forEach(group, (agg, index) => {
move($scope.state.aggs, agg, indexOffset + index);
});
};
},
};
});

View file

@ -24,12 +24,12 @@ import { SearchSource } from '../../courier';
import { QueryFilter } from '../../filter_manager/query_filter';
import { Adapters } from '../../inspector/types';
import { PersistedState } from '../../persisted_state';
import { AggConfigs } from '../agg_configs';
import { AggConfig } from '../agg_config';
import { Vis } from '../vis';
export interface RequestHandlerParams {
searchSource: SearchSource;
aggs: AggConfigs;
aggs: AggConfig[];
timeRange?: TimeRange;
query?: Query;
filters?: Filter[];

View file

@ -18,6 +18,7 @@
*/
import { VisType } from './vis_types/vis_type';
import { AggConfigs } from './agg_configs';
export interface Vis {
type: VisType;
@ -39,5 +40,5 @@ export interface VisState {
title: string;
type: VisType;
params: VisParams;
aggs: any[];
aggs: AggConfigs;
}

View file

@ -20,7 +20,7 @@ import { mockDataLoaderFetch, timefilter } from './embedded_visualize_handler.te
// @ts-ignore
import MockState from '../../../../../fixtures/mock_state';
import { RequestHandlerParams, Vis } from '../../vis';
import { RequestHandlerParams, Vis, AggConfig } from '../../vis';
import { VisResponseData } from './types';
import { Inspector } from '../../inspector';
@ -49,7 +49,7 @@ describe('EmbeddedVisualizeHandler', () => {
jest.clearAllMocks();
dataLoaderParams = {
aggs: [],
aggs: [] as AggConfig[],
filters: undefined,
forceFetch: false,
inspectorAdapters: {},

View file

@ -57,7 +57,6 @@ export default function ({ getService, getPageObjects }) {
});
it('should show Split Gauges', async function () {
await PageObjects.visualize.clickMetricEditor();
log.debug('Bucket = Split Group');
await PageObjects.visualize.clickBucket('Split group');
log.debug('Aggregation = Terms');
@ -81,7 +80,6 @@ export default function ({ getService, getPageObjects }) {
it('should show correct values for fields with fieldFormatters', async function () {
const expectedTexts = [ '2,904', 'win 8: Count', '0B', 'win 8: Min bytes' ];
await PageObjects.visualize.clickMetricEditor();
await PageObjects.visualize.selectAggregation('Terms');
await PageObjects.visualize.selectField('machine.os.raw');
await PageObjects.visualize.setSize('1');

View file

@ -183,7 +183,6 @@ export default function ({ getService, getPageObjects }) {
});
it('should allow filtering with buckets', async function () {
await PageObjects.visualize.clickMetricEditor();
log.debug('Bucket = Split Group');
await PageObjects.visualize.clickBucket('Split group');
log.debug('Aggregation = Terms');

View file

@ -407,7 +407,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
}
async clickMetricEditor() {
await find.clickByCssSelector('button[data-test-subj="toggleEditor"]');
await find.clickByCssSelector('[group-name="metrics"] .euiAccordion__button');
}
async clickMetricByIndex(index) {
@ -450,8 +450,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
async selectAggregation(myString, groupName = 'buckets', childAggregationType = null) {
const comboBoxElement = await find.byCssSelector(`
[group-name="${groupName}"]
vis-editor-agg-params:not(.ng-hide)
[data-test-subj="visAggEditorParams"]
[data-test-subj^="visEditorAggAccordion"].euiAccordion-isOpen
${childAggregationType ? '.visEditorAgg__subAgg' : ''}
[data-test-subj="defaultEditorAggSelect"]
`);
@ -479,7 +478,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
async toggleOpenEditor(index, toState = 'true') {
// index, see selectYAxisAggregation
const toggle = await find.byCssSelector(`button[aria-controls="visAggEditorParams${index}"]`);
const toggle = await find.byCssSelector(`button[aria-controls="visEditorAggAccordion${index}"]`);
const toggleOpen = await toggle.getAttribute('aria-expanded');
log.debug(`toggle ${index} expand = ${toggleOpen}`);
if (toggleOpen !== toState) {
@ -497,12 +496,10 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
// select our agg
const aggSelect = await find
.byCssSelector(`[data-test-subj="aggregationEditor${index}"]
vis-editor-agg-params:not(.ng-hide) [data-test-subj="defaultEditorAggSelect"]`);
.byCssSelector(`#visEditorAggAccordion${index} [data-test-subj="defaultEditorAggSelect"]`);
await comboBox.setElement(aggSelect, agg);
const fieldSelect = await find.byCssSelector(`[data-test-subj="aggregationEditor${index}"]
vis-editor-agg-params:not(.ng-hide) [data-test-subj="visDefaultEditorField"]`);
const fieldSelect = await find.byCssSelector(`#visEditorAggAccordion${index} [data-test-subj="visDefaultEditorField"]`);
// select our field
await comboBox.setElement(fieldSelect, field);
// enter custom label
@ -553,7 +550,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
log.debug(`selectField ${fieldValue}`);
const selector = `
[group-name="${groupName}"]
vis-editor-agg-params:not(.ng-hide)
[data-test-subj^="visEditorAggAccordion"].euiAccordion-isOpen
[data-test-subj="visAggEditorParams"]
${childAggregationType ? '.visEditorAgg__subAgg' : ''}
[data-test-subj="visDefaultEditorField"]
@ -593,26 +590,26 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
}
async setSize(newValue, aggId) {
const dataTestSubj = aggId ? `aggregationEditor${aggId} sizeParamEditor` : 'sizeParamEditor';
const dataTestSubj = aggId ? `visEditorAggAccordion${aggId} sizeParamEditor` : 'sizeParamEditor';
await testSubjects.setValue(dataTestSubj, String(newValue));
}
async toggleDisabledAgg(agg) {
await testSubjects.click(`aggregationEditor${agg} disableAggregationBtn`);
await testSubjects.click(`visEditorAggAccordion${agg} toggleDisableAggregationBtn`);
await PageObjects.header.waitUntilLoadingHasFinished();
}
async toggleAggregationEditor(agg) {
await testSubjects.click(`aggregationEditor${agg} toggleEditor`);
await find.clickByCssSelector(`[data-test-subj="visEditorAggAccordion${agg}"] .euiAccordion__button`);
await PageObjects.header.waitUntilLoadingHasFinished();
}
async toggleOtherBucket(agg = 2) {
return await testSubjects.click(`aggregationEditor${agg} otherBucketSwitch`);
return await testSubjects.click(`visEditorAggAccordion${agg} otherBucketSwitch`);
}
async toggleMissingBucket(agg = 2) {
return await testSubjects.click(`aggregationEditor${agg} missingBucketSwitch`);
return await testSubjects.click(`visEditorAggAccordion${agg} missingBucketSwitch`);
}
async isApplyEnabled() {
@ -1271,7 +1268,7 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
}
async removeDimension(agg) {
await testSubjects.click(`aggregationEditor${agg} removeDimensionBtn`);
await testSubjects.click(`visEditorAggAccordion${agg} removeDimensionBtn`);
}
}

View file

@ -570,12 +570,9 @@
"common.ui.vis.defaultEditor.aggSelect.subAggregationLabel": "サブ集約",
"common.ui.vis.defaultFeedbackMessage": "フィードバックがありますか?{link} で問題を報告してください。",
"common.ui.vis.editors.advancedToggle.advancedLinkLabel": "高度な設定",
"common.ui.vis.editors.agg.disableAggButtonAriaLabel": "集約を無効にする",
"common.ui.vis.editors.agg.disableAggButtonTooltip": "集約を無効にする",
"common.ui.vis.editors.agg.enableAggButtonAriaLabel": "集約を有効にする",
"common.ui.vis.editors.agg.enableAggButtonTooltip": "集約を有効にする",
"common.ui.vis.editors.agg.modifyPriorityButtonTooltip": "ドラッグして優先順位を変更します",
"common.ui.vis.editors.agg.removeDimensionButtonAriaLabel": "ディメンションを削除",
"common.ui.vis.editors.agg.removeDimensionButtonTooltip": "ディメンションを削除",
"common.ui.vis.editors.agg.toggleEditorButtonAriaLabel": "{schema} エディターを切り替える",
"common.ui.vis.editors.aggAdd.addGroupButtonLabel": "{groupNameLabel} を追加",
@ -583,8 +580,6 @@
"common.ui.vis.editors.aggGroups.bucketsText": "バケット",
"common.ui.vis.editors.aggGroups.metricsText": "メトリック",
"common.ui.vis.editors.aggParams.errors.aggWrongRunOrderErrorMessage": "「{schema}」集約は他のバケットの前に実行する必要があります!",
"common.ui.vis.editors.aggParams.errors.schemaIsDeprecatedErrorMessage": "「{schema}」は廃止されました。",
"common.ui.vis.editors.howToModifyScreenReaderPriorityDescription": "このボタンの上下の矢印キーで、集約の優先順位の上下を変更します。",
"common.ui.vis.editors.resizeAriaLabels": "左右のキーでエディターのサイズを変更します",
"common.ui.vis.editors.sidebar.applyChangesAriaLabel": "ビジュアライゼーションを変更と共に更新します",
"common.ui.vis.editors.sidebar.applyChangesTooltip": "変更を適用",

View file

@ -570,12 +570,9 @@
"common.ui.vis.defaultEditor.aggSelect.subAggregationLabel": "子聚合",
"common.ui.vis.defaultFeedbackMessage": "想反馈?请在“{link}中创建问题。",
"common.ui.vis.editors.advancedToggle.advancedLinkLabel": "高级",
"common.ui.vis.editors.agg.disableAggButtonAriaLabel": "禁用聚合",
"common.ui.vis.editors.agg.disableAggButtonTooltip": "禁用聚合",
"common.ui.vis.editors.agg.enableAggButtonAriaLabel": "启用聚合",
"common.ui.vis.editors.agg.enableAggButtonTooltip": "启用聚合",
"common.ui.vis.editors.agg.modifyPriorityButtonTooltip": "通过拖动来修改优先级",
"common.ui.vis.editors.agg.removeDimensionButtonAriaLabel": "删除维度",
"common.ui.vis.editors.agg.removeDimensionButtonTooltip": "删除维度",
"common.ui.vis.editors.agg.toggleEditorButtonAriaLabel": "切换 {schema} 编辑器",
"common.ui.vis.editors.aggAdd.addGroupButtonLabel": "添加{groupNameLabel}",
@ -583,8 +580,6 @@
"common.ui.vis.editors.aggGroups.bucketsText": "存储桶",
"common.ui.vis.editors.aggGroups.metricsText": "指标",
"common.ui.vis.editors.aggParams.errors.aggWrongRunOrderErrorMessage": "“{schema}” 聚合必须在所有其他存储桶之前运行!",
"common.ui.vis.editors.aggParams.errors.schemaIsDeprecatedErrorMessage": "{schema}”已弃用。",
"common.ui.vis.editors.howToModifyScreenReaderPriorityDescription": "使用此按钮上的向上和向下键上移和下移此聚合的优先级顺序。",
"common.ui.vis.editors.resizeAriaLabels": "按向左/向右键以调整编辑器的大小",
"common.ui.vis.editors.sidebar.applyChangesAriaLabel": "使用您的更改更新可视化",
"common.ui.vis.editors.sidebar.applyChangesTooltip": "应用更改",

View file

@ -6072,11 +6072,6 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
atoa@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/atoa/-/atoa-1.0.0.tgz#0cc0e91a480e738f923ebc103676471779b34a49"
integrity sha1-DMDpGkgOc4+SPrwQNnZHF3mzSkk=
atob-lite@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696"
@ -8853,14 +8848,6 @@ contour_plot@^0.0.1:
resolved "https://registry.yarnpkg.com/contour_plot/-/contour_plot-0.0.1.tgz#475870f032b8e338412aa5fc507880f0bf495c77"
integrity sha1-R1hw8DK44zhBKqX8UHiA8L9JXHc=
contra@1.9.4:
version "1.9.4"
resolved "https://registry.yarnpkg.com/contra/-/contra-1.9.4.tgz#f53bde42d7e5b5985cae4d99a8d610526de8f28d"
integrity sha1-9TveQtfltZhcrk2ZqNYQUm3o8o0=
dependencies:
atoa "1.0.0"
ticky "1.0.1"
convert-source-map@1.X, convert-source-map@^1.1.0, convert-source-map@^1.4.0, convert-source-map@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
@ -9234,13 +9221,6 @@ cross-spawn@^5.0.1, cross-spawn@^5.1.0:
shebang-command "^1.2.0"
which "^1.2.9"
crossvent@1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/crossvent/-/crossvent-1.5.4.tgz#da2c4f8f40c94782517bf2beec1044148194ab92"
integrity sha1-2ixPj0DJR4JRe/K+7BBEFIGUq5I=
dependencies:
custom-event "1.0.0"
cryptiles@2.x.x:
version "2.0.5"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
@ -9487,11 +9467,6 @@ custom-event-polyfill@^0.3.0:
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-0.3.0.tgz#99807839be62edb446b645832e0d80ead6fa1888"
integrity sha1-mYB4Ob5i7bRGtkWDLg2A6tb6GIg=
custom-event@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.0.tgz#2e4628be19dc4b214b5c02630c5971e811618062"
integrity sha1-LkYovhncSyFLXAJjDFlx6BFhgGI=
custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
@ -10711,14 +10686,6 @@ dragselect@1.8.1:
resolved "https://registry.yarnpkg.com/dragselect/-/dragselect-1.8.1.tgz#63f71a6f980f710c87e28b328e175b7afc9e162b"
integrity sha512-4YbJCcS6zwK8vMX2GiIX3tUrXFSo9a6xmV2z66EIJ8nj+iMHP1o4j0PeFdf5zjfhqVZJi+6zuVKPZInnrTLMbw==
dragula@3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/dragula/-/dragula-3.7.2.tgz#4a35c9d3981ffac1a949c29ca7285058e87393ce"
integrity sha1-SjXJ05gf+sGpScKcpyhQWOhzk84=
dependencies:
contra "1.9.4"
crossvent "1.5.4"
duplexer2@0.0.2, duplexer2@~0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db"
@ -26958,11 +26925,6 @@ thunky@^1.0.2:
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.2.tgz#a862e018e3fb1ea2ec3fce5d55605cf57f247371"
integrity sha1-qGLgGOP7HqLsP85dVWBc9X8kc3E=
ticky@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ticky/-/ticky-1.0.1.tgz#b7cfa71e768f1c9000c497b9151b30947c50e46d"
integrity sha1-t8+nHnaPHJAAxJe5FRswlHxQ5G0=
tildify@^1.0.0, tildify@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/tildify/-/tildify-1.2.0.tgz#dcec03f55dca9b7aa3e5b04f21817eb56e63588a"