[ui/public/utils] Copy rarely used items to where they are consumed (#53819)

* [ui/public/utils] Copy rarely used items to where they are consumed

Closes: #52841

* sort_prefix_first 👉x-pack/legacy/plugins/kuery_autocomplete

* numeric 👉src/legacy/core_plugins/kibana/public/management

* diff_object + tests 👉ui/state_management

* function + tests 👉ui/state_management (function.js was removed!)

* key_map 👉ui/directives

* leastCommonMultiple 👉ui/vis

* string_utils 👉ui/saved_objects

* collection

* parse_interval

* it -> test

* fix CI

* fix PR comments

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Alexey Antonov 2020-01-08 13:15:54 +03:00 committed by GitHub
parent 007ea5fc46
commit 71ff2de7e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 756 additions and 769 deletions

View file

@ -29,7 +29,7 @@ import { uiModules } from 'ui/modules';
import { fatalError, toastNotifications } from 'ui/notify';
import 'ui/accessibility/kbn_ui_ace_keyboard_mode';
import { SavedObjectsClientProvider } from 'ui/saved_objects';
import { isNumeric } from 'ui/utils/numeric';
import { isNumeric } from './lib/numeric';
import { canViewInApp } from './lib/in_app_url';
import { castEsToKbnFieldTypeName } from '../../../../../../../plugins/data/public';

View file

@ -17,17 +17,20 @@
* under the License.
*/
import { partition } from 'lodash';
import { keysToCamelCaseShallow } from './case_conversion';
export function sortPrefixFirst(array: any[], prefix?: string | number, property?: string): any[] {
if (!prefix) {
return array;
}
const lowerCasePrefix = ('' + prefix).toLowerCase();
describe('keysToCamelCaseShallow', () => {
test("should convert all of an object's keys to camel case", () => {
const data = {
camelCase: 'camelCase',
'kebab-case': 'kebabCase',
snake_case: 'snakeCase',
};
const partitions = partition(array, entry => {
const value = ('' + (property ? entry[property] : entry)).toLowerCase();
return value.startsWith(lowerCasePrefix);
const result = keysToCamelCaseShallow(data);
expect(result.camelCase).toBe('camelCase');
expect(result.kebabCase).toBe('kebabCase');
expect(result.snakeCase).toBe('snakeCase');
});
return [...partitions[0], ...partitions[1]];
}
});

View file

@ -17,6 +17,8 @@
* under the License.
*/
import moment from 'moment';
import { mapKeys, camelCase } from 'lodash';
export function parseInterval(interval: string): moment.Duration | null;
export function keysToCamelCaseShallow(object: Record<string, any>) {
return mapKeys(object, (value, key) => camelCase(key));
}

View file

@ -18,7 +18,7 @@
*/
import { kfetch } from 'ui/kfetch';
import { keysToCamelCaseShallow } from 'ui/utils/case_conversion';
import { keysToCamelCaseShallow } from './case_conversion';
export async function findObjects(findOptions) {
const response = await kfetch({
@ -26,5 +26,6 @@ export async function findObjects(findOptions) {
pathname: '/api/kibana/management/saved_objects/_find',
query: findOptions,
});
return keysToCamelCaseShallow(response);
}

View file

@ -17,8 +17,8 @@
* under the License.
*/
import _ from 'lodash';
import { isNaN } from 'lodash';
export function isNumeric(v: any): boolean {
return !_.isNaN(v) && (typeof v === 'number' || (!Array.isArray(v) && !_.isNaN(parseFloat(v))));
return !isNaN(v) && (typeof v === 'number' || (!Array.isArray(v) && !isNaN(parseFloat(v))));
}

View file

@ -17,9 +17,10 @@
* under the License.
*/
import { StringUtils } from 'ui/utils/string_utils';
import { i18n } from '@kbn/i18n';
const upperFirst = (str = '') => str.replace(/^./, str => str.toUpperCase());
const names = {
general: i18n.translate('kbn.management.settings.categoryNames.generalLabel', {
defaultMessage: 'General',
@ -51,5 +52,5 @@ const names = {
};
export function getCategoryName(category) {
return category ? names[category] || StringUtils.upperFirst(category) : '';
return category ? names[category] || upperFirst(category) : '';
}

View file

@ -22,22 +22,21 @@ import _ from 'lodash';
/**
* just a place to put feature detection checks
*/
export const supports = {
cssFilters: (function() {
const e = document.createElement('img');
const rules = ['webkitFilter', 'mozFilter', 'msFilter', 'filter'];
const test = 'grayscale(1)';
rules.forEach(function(rule) {
e.style[rule] = test;
});
export const supportsCssFilters = (function() {
const e = document.createElement('img');
const rules = ['webkitFilter', 'mozFilter', 'msFilter', 'filter'];
const test = 'grayscale(1)';
document.body.appendChild(e);
const styles = window.getComputedStyle(e);
const can = _(styles)
.pick(rules)
.includes(test);
document.body.removeChild(e);
rules.forEach(function(rule) {
e.style[rule] = test;
});
return can;
})(),
};
document.body.appendChild(e);
const styles = window.getComputedStyle(e);
const can = _(styles)
.pick(rules)
.includes(test);
document.body.removeChild(e);
return can;
})();

View file

@ -20,7 +20,6 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { supports } from 'ui/utils/supports';
import { Schemas } from 'ui/vis/editors/default/schemas';
import { colorSchemas } from 'ui/vislib/components/color/truncated_colormaps';
import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson';
@ -29,6 +28,7 @@ import { createTileMapVisualization } from './tile_map_visualization';
import { Status } from '../../visualizations/public';
import { TileMapOptions } from './components/tile_map_options';
import { MapTypes } from './map_types';
import { supportsCssFilters } from './css_filters';
export function createTileMapTypeDefinition(dependencies) {
const CoordinateMapsVisualization = createTileMapVisualization(dependencies);
@ -44,7 +44,7 @@ export function createTileMapTypeDefinition(dependencies) {
defaultMessage: 'Plot latitude and longitude coordinates on a map',
}),
visConfig: {
canDesaturate: !!supports.cssFilters,
canDesaturate: Boolean(supportsCssFilters),
defaults: {
colorSchema: 'Yellow to Red',
mapType: 'Scaled Circle Markers',

View file

@ -19,12 +19,12 @@
import _ from 'lodash';
import rison from 'rison-node';
import { keyMap } from 'ui/utils/key_map';
import { uiModules } from 'ui/modules';
import 'ui/directives/input_focus';
import 'ui/directives/paginate';
import savedObjectFinderTemplate from './saved_object_finder.html';
import { savedSheetLoader } from '../services/saved_sheets';
import { keyMap } from 'ui/directives/key_map';
const module = uiModules.get('kibana');

View file

@ -17,9 +17,9 @@
* under the License.
*/
import { parseInterval } from 'ui/utils/parse_interval';
import { GTE_INTERVAL_RE } from '../../common/interval_regexp';
import { i18n } from '@kbn/i18n';
import { parseInterval } from '../../../../../plugins/data/public';
export function validateInterval(bounds, panel, maxBuckets) {
const { interval } = panel;

View file

@ -0,0 +1,36 @@
/*
* 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 { keysToSnakeCaseShallow } from './case_conversion';
describe('keysToSnakeCaseShallow', () => {
test("should convert all of an object's keys to snake case", () => {
const data = {
camelCase: 'camel_case',
'kebab-case': 'kebab_case',
snake_case: 'snake_case',
};
const result = keysToSnakeCaseShallow(data);
expect(result.camel_case).toBe('camel_case');
expect(result.kebab_case).toBe('kebab_case');
expect(result.snake_case).toBe('snake_case');
});
});

View file

@ -17,16 +17,8 @@
* under the License.
*/
import _ from 'lodash';
import { mapKeys, snakeCase } from 'lodash';
export function keysToSnakeCaseShallow(object: Record<string, any>) {
return _.mapKeys(object, (value, key) => {
return _.snakeCase(key);
});
}
export function keysToCamelCaseShallow(object: Record<string, any>) {
return _.mapKeys(object, (value, key) => {
return _.camelCase(key);
});
return mapKeys(object, (value, key) => snakeCase(key));
}

View file

@ -20,7 +20,7 @@
import os from 'os';
import v8 from 'v8';
import { get, isObject, merge } from 'lodash';
import { keysToSnakeCaseShallow } from '../../../utils/case_conversion';
import { keysToSnakeCaseShallow } from './case_conversion';
import { getAllStats as cGroupStats } from './cgroup';
import { getOSInfo } from './get_os_info';

View file

@ -19,7 +19,6 @@
import _ from 'lodash';
import { uiModules } from '../../modules';
import { callEach } from '../../utils/function';
export function watchMultiDecorator($provide) {
$provide.decorator('$rootScope', function($delegate) {
@ -112,7 +111,9 @@ export function watchMultiDecorator($provide) {
)
);
return _.partial(callEach, unwatchers);
return function() {
unwatchers.forEach(listener => listener());
};
};
function normalizeExpression($scope, expr) {

View file

@ -0,0 +1,63 @@
/*
* 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 { groupBy } from 'lodash';
import { organizeBy } from './organize_by';
describe('organizeBy', () => {
test('it works', () => {
const col = [
{
name: 'one',
roles: ['user', 'admin', 'owner'],
},
{
name: 'two',
roles: ['user'],
},
{
name: 'three',
roles: ['user'],
},
{
name: 'four',
roles: ['user', 'admin'],
},
];
const resp = organizeBy(col, 'roles');
expect(resp).toHaveProperty('user');
expect(resp.user.length).toBe(4);
expect(resp).toHaveProperty('admin');
expect(resp.admin.length).toBe(2);
expect(resp).toHaveProperty('owner');
expect(resp.owner.length).toBe(1);
});
test('behaves just like groupBy in normal scenarios', () => {
const col = [{ name: 'one' }, { name: 'two' }, { name: 'three' }, { name: 'four' }];
const orgs = organizeBy(col, 'name');
const groups = groupBy(col, 'name');
expect(orgs).toEqual(groups);
});
});

View file

@ -0,0 +1,61 @@
/*
* 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 { each, isFunction } from 'lodash';
/**
* Like _.groupBy, but allows specifying multiple groups for a
* single object.
*
* organizeBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a')
* // Object {1: Array[2], 2: Array[1], 3: Array[1], 4: Array[1]}
*
* _.groupBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a')
* // Object {'1,2,3': Array[1], '1,4': Array[1]}
*
* @param {array} collection - the list of values to organize
* @param {Function} callback - either a property name, or a callback.
* @return {object}
*/
export function organizeBy(collection: object[], callback: ((obj: object) => string) | string) {
const buckets: { [key: string]: object[] } = {};
function add(key: string, obj: object) {
if (!buckets[key]) {
buckets[key] = [];
}
buckets[key].push(obj);
}
each(collection, (obj: Record<string, any>) => {
const keys = isFunction(callback) ? callback(obj) : obj[callback];
if (!Array.isArray(keys)) {
add(keys, obj);
return;
}
let length = keys.length;
while (length-- > 0) {
add(keys[length], obj);
}
});
return buckets;
}

View file

@ -19,7 +19,7 @@
import _ from 'lodash';
import { inflector } from './inflector';
import { organizeBy } from '../utils/collection';
import { organizeBy } from './helpers/organize_by';
const pathGetter = _(_.get)
.rearg(1, 0)

View file

@ -16,17 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import { StringUtils } from './string_utils';
import _ from 'lodash';
describe('StringUtils class', () => {
describe('static upperFirst', () => {
test('should converts the first character of string to upper case', () => {
expect(StringUtils.upperFirst()).toBe('');
expect(StringUtils.upperFirst('')).toBe('');
/**
* Call all of the function in an array
*
* @param {array[functions]} arr
* @return {undefined}
*/
export function callEach(arr) {
return _.map(arr, function(fn) {
return _.isFunction(fn) ? fn() : undefined;
expect(StringUtils.upperFirst('Fred')).toBe('Fred');
expect(StringUtils.upperFirst('fred')).toBe('Fred');
expect(StringUtils.upperFirst('FRED')).toBe('FRED');
});
});
}
});

View file

@ -23,7 +23,7 @@ export class StringUtils {
* @param str {string}
* @returns {string}
*/
public static upperFirst(str: string): string {
public static upperFirst(str: string = ''): string {
return str ? str.charAt(0).toUpperCase() + str.slice(1) : '';
}
}

View file

@ -18,7 +18,7 @@
*/
import { SavedObject } from 'ui/saved_objects/types';
import { ChromeStart, SavedObjectsClientContract, SavedObjectsFindOptions } from 'kibana/public';
import { StringUtils } from '../utils/string_utils';
import { StringUtils } from './helpers/string_utils';
/**
* The SavedObjectLoader class provides some convenience functions

View file

@ -31,7 +31,6 @@ import { uiModules } from '../modules';
import { StateProvider } from './state';
import '../persisted_state';
import { createLegacyClass } from '../utils/legacy_class';
import { callEach } from '../utils/function';
const urlParam = '_a';
@ -62,7 +61,8 @@ export function AppStateProvider(Private, $location, $injector) {
AppState.prototype.destroy = function() {
AppState.Super.prototype.destroy.call(this);
AppState.getAppState._set(null);
callEach(eventUnsubscribers);
eventUnsubscribers.forEach(listener => listener());
};
/**

View file

@ -29,12 +29,11 @@ import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import angular from 'angular';
import rison from 'rison-node';
import { applyDiff } from '../utils/diff_object';
import { applyDiff } from './utils/diff_object';
import { EventsProvider } from '../events';
import { fatalError, toastNotifications } from '../notify';
import './config_provider';
import { createLegacyClass } from '../utils/legacy_class';
import { callEach } from '../utils/function';
import {
hashedItemStore,
isStateHash,
@ -66,7 +65,7 @@ export function StateProvider(
this._hashedItemStore = _hashedItemStore;
// When the URL updates we need to fetch the values from the URL
this._cleanUpListeners = _.partial(callEach, [
this._cleanUpListeners = [
// partial route update, no app reload
$rootScope.$on('$routeUpdate', () => {
this.fetch();
@ -85,7 +84,7 @@ export function StateProvider(
this.fetch();
}
}),
]);
];
// Initialize the State with fetch
this.fetch();
@ -242,7 +241,9 @@ export function StateProvider(
*/
State.prototype.destroy = function() {
this.off(); // removes all listeners
this._cleanUpListeners(); // Removes the $routeUpdate listener
// Removes the $routeUpdate listener
this._cleanUpListeners.forEach(listener => listener(this));
};
State.prototype.setDefaults = function(defaults) {

View file

@ -17,76 +17,88 @@
* under the License.
*/
import expect from '@kbn/expect';
import _ from 'lodash';
import { applyDiff } from '../diff_object';
import { cloneDeep } from 'lodash';
import { applyDiff } from './diff_object';
describe('ui/utils/diff_object', function() {
it('should list the removed keys', function() {
describe('diff_object', () => {
test('should list the removed keys', () => {
const target = { test: 'foo' };
const source = { foo: 'test' };
const results = applyDiff(target, source);
expect(results).to.have.property('removed');
expect(results.removed).to.eql(['test']);
expect(results).toHaveProperty('removed');
expect(results.removed).toEqual(['test']);
});
it('should list the changed keys', function() {
test('should list the changed keys', () => {
const target = { foo: 'bar' };
const source = { foo: 'test' };
const results = applyDiff(target, source);
expect(results).to.have.property('changed');
expect(results.changed).to.eql(['foo']);
expect(results).toHaveProperty('changed');
expect(results.changed).toEqual(['foo']);
});
it('should list the added keys', function() {
test('should list the added keys', () => {
const target = {};
const source = { foo: 'test' };
const results = applyDiff(target, source);
expect(results).to.have.property('added');
expect(results.added).to.eql(['foo']);
expect(results).toHaveProperty('added');
expect(results.added).toEqual(['foo']);
});
it('should list all the keys that are change or removed', function() {
test('should list all the keys that are change or removed', () => {
const target = { foo: 'bar', test: 'foo' };
const source = { foo: 'test' };
const results = applyDiff(target, source);
expect(results).to.have.property('keys');
expect(results.keys).to.eql(['foo', 'test']);
expect(results).toHaveProperty('keys');
expect(results.keys).toEqual(['foo', 'test']);
});
it('should ignore functions', function() {
test('should ignore functions', () => {
const target = { foo: 'bar', test: 'foo' };
const source = { foo: 'test', fn: _.noop };
const source = { foo: 'test', fn: () => {} };
applyDiff(target, source);
expect(target).to.not.have.property('fn');
expect(target).not.toHaveProperty('fn');
});
it('should ignore underscores', function() {
test('should ignore underscores', () => {
const target = { foo: 'bar', test: 'foo' };
const source = { foo: 'test', _private: 'foo' };
applyDiff(target, source);
expect(target).to.not.have.property('_private');
expect(target).not.toHaveProperty('_private');
});
it('should ignore dollar signs', function() {
test('should ignore dollar signs', () => {
const target = { foo: 'bar', test: 'foo' };
const source = { foo: 'test', $private: 'foo' };
applyDiff(target, source);
expect(target).to.not.have.property('$private');
expect(target).not.toHaveProperty('$private');
});
it('should not list any changes for similar objects', function() {
test('should not list any changes for similar objects', () => {
const target = { foo: 'bar', test: 'foo' };
const source = { foo: 'bar', test: 'foo', $private: 'foo' };
const results = applyDiff(target, source);
expect(results.changed).to.be.empty();
expect(results.changed).toEqual([]);
});
it('should only change keys that actually changed', function() {
test('should only change keys that actually changed', () => {
const obj = { message: 'foo' };
const target = { obj: obj, message: 'foo' };
const source = { obj: _.cloneDeep(obj), message: 'test' };
const target = { obj, message: 'foo' };
const source = { obj: cloneDeep(obj), message: 'test' };
applyDiff(target, source);
expect(target.obj).to.be(obj);
expect(target.obj).toBe(obj);
});
});

View file

@ -0,0 +1,75 @@
/*
* 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 { keys, isFunction, difference, filter, union, pick, each, assign, isEqual } from 'lodash';
export interface IDiffObject {
removed: string[];
added: string[];
changed: string[];
keys: string[];
}
/**
* Filter the private vars
* @param {string} key The keys
* @returns {boolean}
*/
const filterPrivateAndMethods = function(obj: Record<string, any>) {
return function(key: string) {
if (isFunction(obj[key])) return false;
if (key.charAt(0) === '$') return false;
return key.charAt(0) !== '_';
};
};
export function applyDiff(target: Record<string, any>, source: Record<string, any>) {
const diff: IDiffObject = {
removed: [],
added: [],
changed: [],
keys: [],
};
const targetKeys = keys(target).filter(filterPrivateAndMethods(target));
const sourceKeys = keys(source).filter(filterPrivateAndMethods(source));
// Find the keys to be removed
diff.removed = difference(targetKeys, sourceKeys);
// Find the keys to be added
diff.added = difference(sourceKeys, targetKeys);
// Find the keys that will be changed
diff.changed = filter(sourceKeys, key => !isEqual(target[key], source[key]));
// Make a list of all the keys that are changing
diff.keys = union(diff.changed, diff.removed, diff.added);
// Remove all the keys
each(diff.removed, key => {
delete target[key];
});
// Assign the changed to the source to the target
assign(target, pick(source, diff.changed));
// Assign the added to the source to the target
assign(target, pick(source, diff.added));
return diff;
}

View file

@ -20,13 +20,12 @@
import _ from 'lodash';
import moment from 'moment';
import { npStart } from 'ui/new_platform';
import { parseInterval } from '../utils/parse_interval';
import { calcAutoIntervalLessThan, calcAutoIntervalNear } from './calc_auto_interval';
import {
convertDurationToNormalizedEsInterval,
convertIntervalToEsInterval,
} from './calc_es_interval';
import { FIELD_FORMAT_IDS } from '../../../../plugins/data/public';
import { FIELD_FORMAT_IDS, parseInterval } from '../../../../plugins/data/public';
const getConfig = (...args) => npStart.core.uiSettings.get(...args);

View file

@ -1,200 +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 expect from '@kbn/expect';
import { groupBy } from 'lodash';
import { move, pushAll, organizeBy } from '../collection';
describe('collection', () => {
describe('move', function() {
it('accepts previous from->to syntax', function() {
const list = [1, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1];
expect(list[3]).to.be(1);
expect(list[8]).to.be(8);
move(list, 8, 3);
expect(list[8]).to.be(1);
expect(list[3]).to.be(8);
});
it('moves an object up based on a function callback', function() {
const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1];
expect(list[4]).to.be(0);
expect(list[5]).to.be(1);
expect(list[6]).to.be(0);
move(list, 5, false, function(v) {
return v === 0;
});
expect(list[4]).to.be(1);
expect(list[5]).to.be(0);
expect(list[6]).to.be(0);
});
it('moves an object down based on a function callback', function() {
const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1];
expect(list[4]).to.be(0);
expect(list[5]).to.be(1);
expect(list[6]).to.be(0);
move(list, 5, true, function(v) {
return v === 0;
});
expect(list[4]).to.be(0);
expect(list[5]).to.be(0);
expect(list[6]).to.be(1);
});
it('moves an object up based on a where callback', function() {
const list = [
{ v: 1 },
{ v: 1 },
{ v: 1 },
{ v: 1 },
{ v: 0 },
{ v: 1 },
{ v: 0 },
{ v: 1 },
{ v: 1 },
{ v: 1 },
{ v: 1 },
];
expect(list[4]).to.have.property('v', 0);
expect(list[5]).to.have.property('v', 1);
expect(list[6]).to.have.property('v', 0);
move(list, 5, false, { v: 0 });
expect(list[4]).to.have.property('v', 1);
expect(list[5]).to.have.property('v', 0);
expect(list[6]).to.have.property('v', 0);
});
it('moves an object down based on a where callback', function() {
const list = [
{ v: 1 },
{ v: 1 },
{ v: 1 },
{ v: 1 },
{ v: 0 },
{ v: 1 },
{ v: 0 },
{ v: 1 },
{ v: 1 },
{ v: 1 },
{ v: 1 },
];
expect(list[4]).to.have.property('v', 0);
expect(list[5]).to.have.property('v', 1);
expect(list[6]).to.have.property('v', 0);
move(list, 5, true, { v: 0 });
expect(list[4]).to.have.property('v', 0);
expect(list[5]).to.have.property('v', 0);
expect(list[6]).to.have.property('v', 1);
});
it('moves an object down based on a pluck callback', function() {
const list = [
{ id: 0, normal: true },
{ id: 1, normal: true },
{ id: 2, normal: true },
{ id: 3, normal: true },
{ id: 4, normal: true },
{ id: 5, normal: false },
{ id: 6, normal: true },
{ id: 7, normal: true },
{ id: 8, normal: true },
{ id: 9, normal: true },
];
expect(list[4]).to.have.property('id', 4);
expect(list[5]).to.have.property('id', 5);
expect(list[6]).to.have.property('id', 6);
move(list, 5, true, 'normal');
expect(list[4]).to.have.property('id', 4);
expect(list[5]).to.have.property('id', 6);
expect(list[6]).to.have.property('id', 5);
});
});
describe('pushAll', function() {
it('pushes an entire array into another', function() {
const a = [1, 2, 3, 4];
const b = [5, 6, 7, 8];
const output = pushAll(b, a);
expect(output).to.be(a);
expect(a).to.eql([1, 2, 3, 4, 5, 6, 7, 8]);
expect(b).to.eql([5, 6, 7, 8]);
});
});
describe('organizeBy', function() {
it('it works', function() {
const col = [
{
name: 'one',
roles: ['user', 'admin', 'owner'],
},
{
name: 'two',
roles: ['user'],
},
{
name: 'three',
roles: ['user'],
},
{
name: 'four',
roles: ['user', 'admin'],
},
];
const resp = organizeBy(col, 'roles');
expect(resp).to.have.property('user');
expect(resp.user).to.have.length(4);
expect(resp).to.have.property('admin');
expect(resp.admin).to.have.length(2);
expect(resp).to.have.property('owner');
expect(resp.owner).to.have.length(1);
});
it('behaves just like groupBy in normal scenarios', function() {
const col = [{ name: 'one' }, { name: 'two' }, { name: 'three' }, { name: 'four' }];
const orgs = organizeBy(col, 'name');
const groups = groupBy(col, 'name');
expect(orgs).to.eql(groups);
});
});
});

View file

@ -1,93 +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 { parseInterval } from '../parse_interval';
import expect from '@kbn/expect';
describe('parseInterval', function() {
it('should correctly parse an interval containing unit and value', function() {
let duration = parseInterval('1d');
expect(duration.as('d')).to.be(1);
duration = parseInterval('2y');
expect(duration.as('y')).to.be(2);
duration = parseInterval('5M');
expect(duration.as('M')).to.be(5);
duration = parseInterval('5m');
expect(duration.as('m')).to.be(5);
duration = parseInterval('250ms');
expect(duration.as('ms')).to.be(250);
duration = parseInterval('100s');
expect(duration.as('s')).to.be(100);
duration = parseInterval('23d');
expect(duration.as('d')).to.be(23);
duration = parseInterval('52w');
expect(duration.as('w')).to.be(52);
});
it('should correctly parse fractional intervals containing unit and value', function() {
let duration = parseInterval('1.5w');
expect(duration.as('w')).to.be(1.5);
duration = parseInterval('2.35y');
expect(duration.as('y')).to.be(2.35);
});
it('should correctly bubble up intervals which are less than 1', function() {
let duration = parseInterval('0.5y');
expect(duration.as('d')).to.be(183);
duration = parseInterval('0.5d');
expect(duration.as('h')).to.be(12);
});
it('should correctly parse a unit in an interval only', function() {
let duration = parseInterval('ms');
expect(duration.as('ms')).to.be(1);
duration = parseInterval('d');
expect(duration.as('d')).to.be(1);
duration = parseInterval('m');
expect(duration.as('m')).to.be(1);
duration = parseInterval('y');
expect(duration.as('y')).to.be(1);
duration = parseInterval('M');
expect(duration.as('M')).to.be(1);
});
it('should return null for an invalid interval', function() {
let duration = parseInterval('');
expect(duration).to.not.be.ok();
duration = parseInterval(null);
expect(duration).to.not.be.ok();
duration = parseInterval('234asdf');
expect(duration).to.not.be.ok();
});
});

View file

@ -1,87 +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 expect from '@kbn/expect';
import { sortPrefixFirst } from '../sort_prefix_first';
describe('sortPrefixFirst', function() {
it('should return the original unmodified array if no prefix is provided', function() {
const array = ['foo', 'bar', 'baz'];
const result = sortPrefixFirst(array);
expect(result).to.be(array);
expect(result).to.eql(['foo', 'bar', 'baz']);
});
it('should sort items that match the prefix first without modifying the original array', function() {
const array = ['foo', 'bar', 'baz'];
const result = sortPrefixFirst(array, 'b');
expect(result).to.not.be(array);
expect(result).to.eql(['bar', 'baz', 'foo']);
expect(array).to.eql(['foo', 'bar', 'baz']);
});
it('should not modify the order of the array other than matching prefix without modifying the original array', function() {
const array = ['foo', 'bar', 'baz', 'qux', 'quux'];
const result = sortPrefixFirst(array, 'b');
expect(result).to.not.be(array);
expect(result).to.eql(['bar', 'baz', 'foo', 'qux', 'quux']);
expect(array).to.eql(['foo', 'bar', 'baz', 'qux', 'quux']);
});
it('should sort objects by property if provided', function() {
const array = [
{ name: 'foo' },
{ name: 'bar' },
{ name: 'baz' },
{ name: 'qux' },
{ name: 'quux' },
];
const result = sortPrefixFirst(array, 'b', 'name');
expect(result).to.not.be(array);
expect(result).to.eql([
{ name: 'bar' },
{ name: 'baz' },
{ name: 'foo' },
{ name: 'qux' },
{ name: 'quux' },
]);
expect(array).to.eql([
{ name: 'foo' },
{ name: 'bar' },
{ name: 'baz' },
{ name: 'qux' },
{ name: 'quux' },
]);
});
it('should handle numbers', function() {
const array = [1, 50, 5];
const result = sortPrefixFirst(array, 5);
expect(result).to.not.be(array);
expect(result).to.eql([50, 5, 1]);
});
it('should handle mixed case', function() {
const array = ['Date Histogram', 'Histogram'];
const prefix = 'histo';
const result = sortPrefixFirst(array, prefix);
expect(result).to.not.be(array);
expect(result).to.eql(['Histogram', 'Date Histogram']);
});
});

View file

@ -0,0 +1,141 @@
/*
* 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 { move } from './collection';
describe('collection', () => {
describe('move', () => {
test('accepts previous from->to syntax', () => {
const list = [1, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1];
expect(list[3]).toBe(1);
expect(list[8]).toBe(8);
move(list, 8, 3);
expect(list[8]).toBe(1);
expect(list[3]).toBe(8);
});
test('moves an object up based on a function callback', () => {
const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1];
expect(list[4]).toBe(0);
expect(list[5]).toBe(1);
expect(list[6]).toBe(0);
move(list, 5, false, (v: any) => v === 0);
expect(list[4]).toBe(1);
expect(list[5]).toBe(0);
expect(list[6]).toBe(0);
});
test('moves an object down based on a function callback', () => {
const list = [1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1];
expect(list[4]).toBe(0);
expect(list[5]).toBe(1);
expect(list[6]).toBe(0);
move(list, 5, true, (v: any) => v === 0);
expect(list[4]).toBe(0);
expect(list[5]).toBe(0);
expect(list[6]).toBe(1);
});
test('moves an object up based on a where callback', () => {
const list = [
{ v: 1 },
{ v: 1 },
{ v: 1 },
{ v: 1 },
{ v: 0 },
{ v: 1 },
{ v: 0 },
{ v: 1 },
{ v: 1 },
{ v: 1 },
{ v: 1 },
];
expect(list[4]).toHaveProperty('v', 0);
expect(list[5]).toHaveProperty('v', 1);
expect(list[6]).toHaveProperty('v', 0);
move(list, 5, false, { v: 0 });
expect(list[4]).toHaveProperty('v', 1);
expect(list[5]).toHaveProperty('v', 0);
expect(list[6]).toHaveProperty('v', 0);
});
test('moves an object down based on a where callback', () => {
const list = [
{ v: 1 },
{ v: 1 },
{ v: 1 },
{ v: 1 },
{ v: 0 },
{ v: 1 },
{ v: 0 },
{ v: 1 },
{ v: 1 },
{ v: 1 },
{ v: 1 },
];
expect(list[4]).toHaveProperty('v', 0);
expect(list[5]).toHaveProperty('v', 1);
expect(list[6]).toHaveProperty('v', 0);
move(list, 5, true, { v: 0 });
expect(list[4]).toHaveProperty('v', 0);
expect(list[5]).toHaveProperty('v', 0);
expect(list[6]).toHaveProperty('v', 1);
});
test('moves an object down based on a pluck callback', () => {
const list = [
{ id: 0, normal: true },
{ id: 1, normal: true },
{ id: 2, normal: true },
{ id: 3, normal: true },
{ id: 4, normal: true },
{ id: 5, normal: false },
{ id: 6, normal: true },
{ id: 7, normal: true },
{ id: 8, normal: true },
{ id: 9, normal: true },
];
expect(list[4]).toHaveProperty('id', 4);
expect(list[5]).toHaveProperty('id', 5);
expect(list[6]).toHaveProperty('id', 6);
move(list, 5, true, 'normal');
expect(list[4]).toHaveProperty('id', 4);
expect(list[5]).toHaveProperty('id', 6);
expect(list[6]).toHaveProperty('id', 5);
});
});
});

View file

@ -33,10 +33,10 @@ import _ from 'lodash';
* @return {array} - the objs argument
*/
export function move(
objs: object[],
objs: any[],
obj: object | number,
below: number | boolean,
qualifier: (object: object, index: number) => any
qualifier?: ((object: object, index: number) => any) | Record<string, any> | string
): object[] {
const origI = _.isNumber(obj) ? obj : objs.indexOf(obj);
if (origI === -1) {
@ -50,7 +50,7 @@ export function move(
}
below = !!below;
qualifier = _.callback(qualifier);
qualifier = qualifier && _.callback(qualifier);
const above = !below;
const finder = below ? _.findIndex : _.findLastIndex;
@ -63,7 +63,7 @@ export function move(
if (above && otherI >= origI) {
return;
}
return !!qualifier(otherAgg, otherI);
return Boolean(_.isFunction(qualifier) && qualifier(otherAgg, otherI));
});
if (targetI === -1) {
@ -74,68 +74,3 @@ export function move(
objs.splice(targetI, 0, objs.splice(origI, 1)[0]);
return objs;
}
/**
* Like _.groupBy, but allows specifying multiple groups for a
* single object.
*
* organizeBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a')
* // Object {1: Array[2], 2: Array[1], 3: Array[1], 4: Array[1]}
*
* _.groupBy([{ a: [1, 2, 3] }, { b: true, a: [1, 4] }], 'a')
* // Object {'1,2,3': Array[1], '1,4': Array[1]}
*
* @param {array} collection - the list of values to organize
* @param {Function} callback - either a property name, or a callback.
* @return {object}
*/
export function organizeBy(collection: object[], callback: (obj: object) => string | string) {
const buckets: { [key: string]: object[] } = {};
const prop = typeof callback === 'function' ? false : callback;
function add(key: string, obj: object) {
if (!buckets[key]) {
buckets[key] = [];
}
buckets[key].push(obj);
}
_.each(collection, (obj: object) => {
const keys = prop === false ? callback(obj) : obj[prop];
if (!Array.isArray(keys)) {
add(keys, obj);
return;
}
let length = keys.length;
while (length-- > 0) {
add(keys[length], obj);
}
});
return buckets;
}
/**
* Efficient and safe version of [].push(dest, source);
*
* @param {Array} source - the array to pull values from
* @param {Array} dest - the array to push values into
* @return {Array} dest
*/
export function pushAll(source: any[], dest: any[]): any[] {
const start = dest.length;
const adding = source.length;
// allocate - http://goo.gl/e2i0S0
dest.length = start + adding;
// fill sparse positions
let i = -1;
while (++i < adding) {
dest[start + i] = source[i];
}
return dest;
}

View file

@ -1,67 +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 'lodash';
import angular from 'angular';
export function applyDiff(target, source) {
const diff = {};
/**
* Filter the private vars
* @param {string} key The keys
* @returns {boolean}
*/
const filterPrivateAndMethods = function(obj) {
return function(key) {
if (_.isFunction(obj[key])) return false;
if (key.charAt(0) === '$') return false;
return key.charAt(0) !== '_';
};
};
const targetKeys = _.keys(target).filter(filterPrivateAndMethods(target));
const sourceKeys = _.keys(source).filter(filterPrivateAndMethods(source));
// Find the keys to be removed
diff.removed = _.difference(targetKeys, sourceKeys);
// Find the keys to be added
diff.added = _.difference(sourceKeys, targetKeys);
// Find the keys that will be changed
diff.changed = _.filter(sourceKeys, function(key) {
return !angular.equals(target[key], source[key]);
});
// Make a list of all the keys that are changing
diff.keys = _.union(diff.changed, diff.removed, diff.added);
// Remove all the keys
_.each(diff.removed, function(key) {
delete target[key];
});
// Assign the changed to the source to the target
_.assign(target, _.pick(source, diff.changed));
// Assign the added to the source to the target
_.assign(target, _.pick(source, diff.added));
return diff;
}

View file

@ -1,66 +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 { greatestCommonDivisor, leastCommonMultiple } from './math';
describe('math utils', () => {
describe('greatestCommonDivisor', () => {
const tests: Array<[number, number, number]> = [
[3, 5, 1],
[30, 36, 6],
[5, 1, 1],
[9, 9, 9],
[40, 20, 20],
[3, 0, 3],
[0, 5, 5],
[0, 0, 0],
[-9, -3, 3],
[-24, 8, 8],
[22, -7, 1],
];
tests.map(([a, b, expected]) => {
it(`should return ${expected} for greatestCommonDivisor(${a}, ${b})`, () => {
expect(greatestCommonDivisor(a, b)).toBe(expected);
});
});
});
describe('leastCommonMultiple', () => {
const tests: Array<[number, number, number]> = [
[3, 5, 15],
[1, 1, 1],
[5, 6, 30],
[3, 9, 9],
[8, 20, 40],
[5, 5, 5],
[0, 5, 0],
[-4, -5, 20],
[-2, -3, 6],
[-8, 2, 8],
[-8, 5, 40],
];
tests.map(([a, b, expected]) => {
it(`should return ${expected} for leastCommonMultiple(${a}, ${b})`, () => {
expect(leastCommonMultiple(a, b)).toBe(expected);
});
});
});
});

View file

@ -21,7 +21,7 @@ import { TimeIntervalParam } from 'ui/vis/editors/config/types';
import { AggConfig } from '../..';
import { AggType } from '../../../agg_types';
import { IndexPattern } from '../../../../../../plugins/data/public';
import { leastCommonMultiple } from '../../../utils/math';
import { leastCommonMultiple } from '../../lib/least_common_multiple';
import { parseEsInterval } from '../../../../../core_plugins/data/public';
import { leastCommonInterval } from '../../lib/least_common_interval';
import { EditorConfig, EditorParamConfig, FixedParam, NumericIntervalParam } from './types';

View file

@ -18,7 +18,7 @@
*/
import dateMath from '@elastic/datemath';
import { leastCommonMultiple } from '../../utils/math';
import { leastCommonMultiple } from './least_common_multiple';
import { parseEsInterval } from '../../../../core_plugins/data/public';
/**

View file

@ -17,20 +17,26 @@
* under the License.
*/
// TODO: This file is copied from src/legacy/utils/case_conversion.ts
// because TS-imports from utils in ui are currently not possible.
// When the build process is updated, this file can be removed
import { leastCommonMultiple } from './least_common_multiple';
import _ from 'lodash';
describe('leastCommonMultiple', () => {
const tests: Array<[number, number, number]> = [
[3, 5, 15],
[1, 1, 1],
[5, 6, 30],
[3, 9, 9],
[8, 20, 40],
[5, 5, 5],
[0, 5, 0],
[-4, -5, 20],
[-2, -3, 6],
[-8, 2, 8],
[-8, 5, 40],
];
export function keysToSnakeCaseShallow(object: Record<string, any>) {
return _.mapKeys(object, (value, key) => {
return _.snakeCase(key);
tests.map(([a, b, expected]) => {
test(`should return ${expected} for leastCommonMultiple(${a}, ${b})`, () => {
expect(leastCommonMultiple(a, b)).toBe(expected);
});
});
}
export function keysToCamelCaseShallow(object: Record<string, any>) {
return _.mapKeys(object, (value, key) => {
return _.camelCase(key);
});
}
});

View file

@ -24,8 +24,10 @@
* This method does not properly work for fractional (non integer) numbers. If you
* pass in fractional numbers there usually will be an output, but that's not necessarily
* the greatest common divisor of those two numbers.
*
* @private
*/
export function greatestCommonDivisor(a: number, b: number): number {
function greatestCommonDivisor(a: number, b: number): number {
return a === 0 ? Math.abs(b) : greatestCommonDivisor(b % a, a);
}
@ -36,6 +38,8 @@ export function greatestCommonDivisor(a: number, b: number): number {
* Since this calculation suffers from rounding issues in decimal values, this method
* won't work for passing in fractional (non integer) numbers. It will return a value,
* but that value won't necessarily be the mathematical correct least common multiple.
*
* @internal
*/
export function leastCommonMultiple(a: number, b: number): number {
return Math.abs((a * b) / greatestCommonDivisor(a, b));

View file

@ -1,56 +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 { keysToCamelCaseShallow, keysToSnakeCaseShallow } from './case_conversion';
describe('keysToSnakeCaseShallow', () => {
it("should convert all of an object's keys to snake case", () => {
const result = keysToSnakeCaseShallow({
camelCase: 'camel_case',
'kebab-case': 'kebab_case',
snake_case: 'snake_case',
});
expect(result).toMatchInlineSnapshot(`
Object {
"camel_case": "camel_case",
"kebab_case": "kebab_case",
"snake_case": "snake_case",
}
`);
});
});
describe('keysToCamelCaseShallow', () => {
it("should convert all of an object's keys to camel case", () => {
const result = keysToCamelCaseShallow({
camelCase: 'camelCase',
'kebab-case': 'kebabCase',
snake_case: 'snakeCase',
});
expect(result).toMatchInlineSnapshot(`
Object {
"camelCase": "camelCase",
"kebabCase": "kebabCase",
"snakeCase": "snakeCase",
}
`);
});
});

View file

@ -18,3 +18,4 @@
*/
export { shortenDottedString } from './shorten_dotted_string';
export { parseInterval } from './parse_interval';

View file

@ -0,0 +1,119 @@
/*
* 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 { Duration, unitOfTime } from 'moment';
import { parseInterval } from './parse_interval';
const validateDuration = (duration: Duration | null, unit: unitOfTime.Base, value: number) => {
expect(duration).toBeDefined();
if (duration) {
expect(duration.as(unit)).toBe(value);
}
};
describe('parseInterval', () => {
describe('integer', () => {
test('should correctly parse 1d interval', () => {
validateDuration(parseInterval('1d'), 'd', 1);
});
test('should correctly parse 2y interval', () => {
validateDuration(parseInterval('2y'), 'y', 2);
});
test('should correctly parse 5M interval', () => {
validateDuration(parseInterval('5M'), 'M', 5);
});
test('should correctly parse 5m interval', () => {
validateDuration(parseInterval('5m'), 'm', 5);
});
test('should correctly parse 250ms interval', () => {
validateDuration(parseInterval('250ms'), 'ms', 250);
});
test('should correctly parse 100s interval', () => {
validateDuration(parseInterval('100s'), 's', 100);
});
test('should correctly parse 23d interval', () => {
validateDuration(parseInterval('23d'), 'd', 23);
});
test('should correctly parse 52w interval', () => {
validateDuration(parseInterval('52w'), 'w', 52);
});
});
describe('fractional interval', () => {
test('should correctly parse fractional 2.35y interval', () => {
validateDuration(parseInterval('2.35y'), 'y', 2.35);
});
test('should correctly parse fractional 1.5w interval', () => {
validateDuration(parseInterval('1.5w'), 'w', 1.5);
});
});
describe('less than 1', () => {
test('should correctly bubble up 0.5h interval which are less than 1', () => {
validateDuration(parseInterval('0.5h'), 'm', 30);
});
test('should correctly bubble up 0.5d interval which are less than 1', () => {
validateDuration(parseInterval('0.5d'), 'h', 12);
});
});
describe('unit in an interval only', () => {
test('should correctly parse ms interval', () => {
validateDuration(parseInterval('ms'), 'ms', 1);
});
test('should correctly parse d interval', () => {
validateDuration(parseInterval('d'), 'd', 1);
});
test('should correctly parse m interval', () => {
validateDuration(parseInterval('m'), 'm', 1);
});
test('should correctly parse y interval', () => {
validateDuration(parseInterval('y'), 'y', 1);
});
test('should correctly parse M interval', () => {
validateDuration(parseInterval('M'), 'M', 1);
});
});
test('should return null for an invalid interval', () => {
let duration = parseInterval('');
expect(duration).toBeNull();
// @ts-ignore
duration = parseInterval(null);
expect(duration).toBeNull();
duration = parseInterval('234asdf');
expect(duration).toBeNull();
});
});

View file

@ -17,14 +17,14 @@
* under the License.
*/
import _ from 'lodash';
import moment from 'moment';
import { find } from 'lodash';
import moment, { unitOfTime } from 'moment';
import dateMath from '@elastic/datemath';
// Assume interval is in the form (value)(unit), such as "1h"
const INTERVAL_STRING_RE = new RegExp('^([0-9\\.]*)\\s*(' + dateMath.units.join('|') + ')$');
export function parseInterval(interval) {
export function parseInterval(interval: string): moment.Duration | null {
const matches = String(interval)
.trim()
.match(INTERVAL_STRING_RE);
@ -33,7 +33,7 @@ export function parseInterval(interval) {
try {
const value = parseFloat(matches[1]) || 1;
const unit = matches[2];
const unit = matches[2] as unitOfTime.Base;
const duration = moment.duration(value, unit);
@ -44,9 +44,10 @@ export function parseInterval(interval) {
// adding 0.5 days until we hit the end date. However, since there is a bug in moment, when you add 0.5 days to
// the start date, you get the same exact date (instead of being ahead by 12 hours). So instead of returning
// a duration corresponding to 0.5 hours, we return a duration corresponding to 12 hours.
const selectedUnit = _.find(dateMath.units, function(unit) {
return Math.abs(duration.as(unit)) >= 1;
});
const selectedUnit = find(
dateMath.units,
u => Math.abs(duration.as(u)) >= 1
) as unitOfTime.Base;
return moment.duration(duration.as(selectedUnit), selectedUnit);
} catch (e) {

View file

@ -90,6 +90,8 @@ export {
castEsToKbnFieldTypeName,
getKbnFieldType,
getKbnTypeNames,
// utils
parseInterval,
} from '../common';
// Export plugin after all other imports

View file

@ -47,6 +47,8 @@ export {
// timefilter
RefreshInterval,
TimeRange,
// utils
parseInterval,
} from '../common';
/**

View file

@ -20,7 +20,7 @@
import $ from 'jquery';
import _ from 'lodash';
import Bluebird from 'bluebird';
import { keyMap } from 'ui/utils/key_map';
import { keyMap } from 'ui/directives/key_map';
const reverseKeyMap = _.mapValues(_.invert(keyMap), _.ary(_.parseInt, 1));
/**

View file

@ -6,7 +6,7 @@
import React from 'react';
import { flatten } from 'lodash';
import { escapeKuery } from './escape_kuery';
import { sortPrefixFirst } from 'ui/utils/sort_prefix_first';
import { sortPrefixFirst } from './sort_prefix_first';
import { isFilterable } from '../../../../../../src/plugins/data/public';
import { FormattedMessage } from '@kbn/i18n/react';

View file

@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { sortPrefixFirst } from './sort_prefix_first';
describe('sortPrefixFirst', () => {
test('should return the original unmodified array if no prefix is provided', () => {
const array = ['foo', 'bar', 'baz'];
const result = sortPrefixFirst(array);
expect(result).toBe(array);
expect(result).toEqual(['foo', 'bar', 'baz']);
});
test('should sort items that match the prefix first without modifying the original array', () => {
const array = ['foo', 'bar', 'baz'];
const result = sortPrefixFirst(array, 'b');
expect(result).not.toBe(array);
expect(result).toEqual(['bar', 'baz', 'foo']);
expect(array).toEqual(['foo', 'bar', 'baz']);
});
test('should not modify the order of the array other than matching prefix without modifying the original array', () => {
const array = ['foo', 'bar', 'baz', 'qux', 'quux'];
const result = sortPrefixFirst(array, 'b');
expect(result).not.toBe(array);
expect(result).toEqual(['bar', 'baz', 'foo', 'qux', 'quux']);
expect(array).toEqual(['foo', 'bar', 'baz', 'qux', 'quux']);
});
test('should sort objects by property if provided', () => {
const array = [
{ name: 'foo' },
{ name: 'bar' },
{ name: 'baz' },
{ name: 'qux' },
{ name: 'quux' },
];
const result = sortPrefixFirst(array, 'b', 'name');
expect(result).not.toBe(array);
expect(result).toEqual([
{ name: 'bar' },
{ name: 'baz' },
{ name: 'foo' },
{ name: 'qux' },
{ name: 'quux' },
]);
expect(array).toEqual([
{ name: 'foo' },
{ name: 'bar' },
{ name: 'baz' },
{ name: 'qux' },
{ name: 'quux' },
]);
});
test('should handle numbers', () => {
const array = [1, 50, 5];
const result = sortPrefixFirst(array, 5);
expect(result).not.toBe(array);
expect(result).toEqual([50, 5, 1]);
});
test('should handle mixed case', () => {
const array = ['Date Histogram', 'Histogram'];
const prefix = 'histo';
const result = sortPrefixFirst(array, prefix);
expect(result).not.toBe(array);
expect(result).toEqual(['Histogram', 'Date Histogram']);
});
});

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { partition } from 'lodash';
export function sortPrefixFirst(array: any[], prefix?: string | number, property?: string): any[] {
if (!prefix) {
return array;
}
const lowerCasePrefix = ('' + prefix).toLowerCase();
const partitions = partition(array, entry => {
const value = ('' + (property ? entry[property] : entry)).toLowerCase();
return value.startsWith(lowerCasePrefix);
});
return [...partitions[0], ...partitions[1]];
}