mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* EUIficate time interval control * Update tests * Remove empty option, update fix tests * Bind vis to scope for react component only * Combine two interval inputs into one EuiCombobox * Add error message * Add migration script; remove unused translations * Update fuctional tests * Update unit test * Update tests; refactoring * Use flow to invoke several functions * Update test * Refactoring * Reset options when timeBase * Add type for editorConfig prop * Add placeholder * Fix lint errors * Call write after interval changing * Fix code review comments * Make replace for model name global * Revert error catch * Remove old dependency * Add unit test for migration test * Fix message * Fix code review comments * Update functional test
This commit is contained in:
parent
813b6ef8b7
commit
aba6d6233c
23 changed files with 524 additions and 258 deletions
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { cloneDeep, get, omit, has } from 'lodash';
|
||||
import { cloneDeep, get, omit, has, flow } from 'lodash';
|
||||
|
||||
function migrateIndexPattern(doc) {
|
||||
const searchSourceJSON = get(doc, 'attributes.kibanaSavedObjectMeta.searchSourceJSON');
|
||||
|
@ -57,6 +57,85 @@ function migrateIndexPattern(doc) {
|
|||
doc.attributes.kibanaSavedObjectMeta.searchSourceJSON = JSON.stringify(searchSource);
|
||||
}
|
||||
|
||||
// [TSVB] Migrate percentile-rank aggregation (value -> values)
|
||||
const migratePercentileRankAggregation = doc => {
|
||||
const visStateJSON = get(doc, 'attributes.visState');
|
||||
let visState;
|
||||
|
||||
if (visStateJSON) {
|
||||
try {
|
||||
visState = JSON.parse(visStateJSON);
|
||||
} catch (e) {
|
||||
// Let it go, the data is invalid and we'll leave it as is
|
||||
}
|
||||
if (visState && visState.type === 'metrics') {
|
||||
const series = get(visState, 'params.series') || [];
|
||||
|
||||
series.forEach(part => {
|
||||
(part.metrics || []).forEach(metric => {
|
||||
if (metric.type === 'percentile_rank' && has(metric, 'value')) {
|
||||
metric.values = [metric.value];
|
||||
|
||||
delete metric.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
visState: JSON.stringify(visState),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
|
||||
// Migrate date histogram aggregation (remove customInterval)
|
||||
const migrateDateHistogramAggregation = doc => {
|
||||
const visStateJSON = get(doc, 'attributes.visState');
|
||||
let visState;
|
||||
|
||||
if (visStateJSON) {
|
||||
try {
|
||||
visState = JSON.parse(visStateJSON);
|
||||
} catch (e) {
|
||||
// Let it go, the data is invalid and we'll leave it as is
|
||||
}
|
||||
|
||||
if (visState && visState.aggs) {
|
||||
visState.aggs.forEach(agg => {
|
||||
if (agg.type === 'date_histogram' && agg.params) {
|
||||
if (agg.params.interval === 'custom') {
|
||||
agg.params.interval = agg.params.customInterval;
|
||||
}
|
||||
delete agg.params.customInterval;
|
||||
}
|
||||
|
||||
if (get(agg, 'params.customBucket.type', null) === 'date_histogram'
|
||||
&& agg.params.customBucket.params
|
||||
) {
|
||||
if (agg.params.customBucket.params.interval === 'custom') {
|
||||
agg.params.customBucket.params.interval = agg.params.customBucket.params.customInterval;
|
||||
}
|
||||
delete agg.params.customBucket.params.customInterval;
|
||||
}
|
||||
});
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
visState: JSON.stringify(visState),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
|
||||
const executeMigrations710 = flow(migratePercentileRankAggregation, migrateDateHistogramAggregation);
|
||||
|
||||
function removeDateHistogramTimeZones(doc) {
|
||||
const visStateJSON = get(doc, 'attributes.visState');
|
||||
if (visStateJSON) {
|
||||
|
@ -185,44 +264,7 @@ export const migrations = {
|
|||
}
|
||||
},
|
||||
'7.0.1': removeDateHistogramTimeZones,
|
||||
'7.1.0': doc => {
|
||||
// [TSVB] Migrate percentile-rank aggregation (value -> values)
|
||||
const migratePercentileRankAggregation = doc => {
|
||||
const visStateJSON = get(doc, 'attributes.visState');
|
||||
let visState;
|
||||
|
||||
if (visStateJSON) {
|
||||
try {
|
||||
visState = JSON.parse(visStateJSON);
|
||||
} catch (e) {
|
||||
// Let it go, the data is invalid and we'll leave it as is
|
||||
}
|
||||
if (visState && visState.type === 'metrics') {
|
||||
const series = get(visState, 'params.series') || [];
|
||||
|
||||
series.forEach(part => {
|
||||
(part.metrics || []).forEach(metric => {
|
||||
if (metric.type === 'percentile_rank' && has(metric, 'value')) {
|
||||
metric.values = [metric.value];
|
||||
|
||||
delete metric.value;
|
||||
}
|
||||
});
|
||||
});
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
visState: JSON.stringify(visState),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
|
||||
return migratePercentileRankAggregation(doc);
|
||||
}
|
||||
'7.1.0': doc => executeMigrations710(doc)
|
||||
},
|
||||
dashboard: {
|
||||
'7.0.0': (doc) => {
|
||||
|
|
|
@ -716,6 +716,133 @@ Object {
|
|||
expect(() => migrate(doc)).toThrowError(/My Vis/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('date histogram custom interval removal', () => {
|
||||
const migrate = doc => migrations.visualization['7.1.0'](doc);
|
||||
let doc;
|
||||
beforeEach(() => {
|
||||
doc = {
|
||||
attributes: {
|
||||
visState: JSON.stringify({
|
||||
aggs: [
|
||||
{
|
||||
'enabled': true,
|
||||
'id': '1',
|
||||
'params': {
|
||||
'customInterval': '1h'
|
||||
},
|
||||
'schema': 'metric',
|
||||
'type': 'count'
|
||||
},
|
||||
{
|
||||
'enabled': true,
|
||||
'id': '2',
|
||||
'params': {
|
||||
'customInterval': '2h',
|
||||
'drop_partials': false,
|
||||
'extended_bounds': {},
|
||||
'field': 'timestamp',
|
||||
'interval': 'auto',
|
||||
'min_doc_count': 1,
|
||||
'useNormalizedEsInterval': true
|
||||
},
|
||||
'schema': 'segment',
|
||||
'type': 'date_histogram'
|
||||
},
|
||||
{
|
||||
'enabled': true,
|
||||
'id': '4',
|
||||
'params': {
|
||||
'customInterval': '2h',
|
||||
'drop_partials': false,
|
||||
'extended_bounds': {},
|
||||
'field': 'timestamp',
|
||||
'interval': 'custom',
|
||||
'min_doc_count': 1,
|
||||
'useNormalizedEsInterval': true
|
||||
},
|
||||
'schema': 'segment',
|
||||
'type': 'date_histogram'
|
||||
},
|
||||
{
|
||||
'enabled': true,
|
||||
'id': '3',
|
||||
'params': {
|
||||
'customBucket': {
|
||||
'enabled': true,
|
||||
'id': '1-bucket',
|
||||
'params': {
|
||||
'customInterval': '2h',
|
||||
'drop_partials': false,
|
||||
'extended_bounds': {},
|
||||
'field': 'timestamp',
|
||||
'interval': 'custom',
|
||||
'min_doc_count': 1,
|
||||
'useNormalizedEsInterval': true
|
||||
},
|
||||
'type': 'date_histogram'
|
||||
},
|
||||
'customMetric': {
|
||||
'enabled': true,
|
||||
'id': '1-metric',
|
||||
'params': {},
|
||||
'type': 'count'
|
||||
}
|
||||
},
|
||||
'schema': 'metric',
|
||||
'type': 'max_bucket'
|
||||
},
|
||||
]
|
||||
}),
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should remove customInterval from date_histogram aggregations', () => {
|
||||
const migratedDoc = migrate(doc);
|
||||
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
|
||||
expect(aggs[1]).not.toHaveProperty('params.customInterval');
|
||||
});
|
||||
|
||||
it('should not change interval from date_histogram aggregations', () => {
|
||||
const migratedDoc = migrate(doc);
|
||||
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
|
||||
expect(aggs[1].params.interval).toBe(JSON.parse(doc.attributes.visState).aggs[1].params.interval);
|
||||
});
|
||||
|
||||
it('should not remove customInterval from non date_histogram aggregations', () => {
|
||||
const migratedDoc = migrate(doc);
|
||||
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
|
||||
expect(aggs[0]).toHaveProperty('params.customInterval');
|
||||
});
|
||||
|
||||
it('should set interval with customInterval value and remove customInterval when interval equals "custom"', () => {
|
||||
const migratedDoc = migrate(doc);
|
||||
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
|
||||
expect(aggs[2].params.interval).toBe(JSON.parse(doc.attributes.visState).aggs[2].params.customInterval);
|
||||
expect(aggs[2]).not.toHaveProperty('params.customInterval');
|
||||
});
|
||||
|
||||
it('should remove customInterval from nested aggregations', () => {
|
||||
const migratedDoc = migrate(doc);
|
||||
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
|
||||
expect(aggs[3]).not.toHaveProperty('params.customBucket.params.customInterval');
|
||||
});
|
||||
|
||||
it('should remove customInterval from nested aggregations and set interval with customInterval value', () => {
|
||||
const migratedDoc = migrate(doc);
|
||||
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
|
||||
expect(aggs[3].params.customBucket.params.interval)
|
||||
.toBe(JSON.parse(doc.attributes.visState).aggs[3].params.customBucket.params.customInterval);
|
||||
expect(aggs[3]).not.toHaveProperty('params.customBucket.params.customInterval');
|
||||
});
|
||||
|
||||
it('should not fail on date histograms without a customInterval', () => {
|
||||
const migratedDoc = migrate(doc);
|
||||
const aggs = JSON.parse(migratedDoc.attributes.visState).aggs;
|
||||
expect(aggs[3]).not.toHaveProperty('params.customInterval');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dashboard', () => {
|
||||
|
|
|
@ -100,7 +100,7 @@ describe('editor', function () {
|
|||
beforeEach(ngMock.inject(function () {
|
||||
field = _.sample(indexPattern.fields);
|
||||
interval = _.sample(intervalOptions);
|
||||
params = render({ field: field, interval: interval });
|
||||
params = render({ field: field, interval: interval.val });
|
||||
}));
|
||||
|
||||
it('renders the field editor', function () {
|
||||
|
@ -112,11 +112,11 @@ describe('editor', function () {
|
|||
});
|
||||
|
||||
it('renders the interval editor', function () {
|
||||
expect(agg.params.interval).to.be(interval);
|
||||
expect(agg.params.interval).to.be(interval.val);
|
||||
|
||||
expect(params).to.have.property('interval');
|
||||
expect(params.interval).to.have.property('$el');
|
||||
expect(params.interval.modelValue()).to.be(interval);
|
||||
expect($scope.agg.params.interval).to.be(interval.val);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -68,9 +68,8 @@ describe('date_histogram params', function () {
|
|||
expect(output.params).to.have.property('interval', '1d');
|
||||
});
|
||||
|
||||
it('ignores invalid intervals', function () {
|
||||
const output = writeInterval('foo');
|
||||
expect(output.params).to.have.property('interval', '0ms');
|
||||
it('throws error when interval is invalid', function () {
|
||||
expect(() => writeInterval('foo')).to.throw('TypeError: "foo" is not a valid interval.');
|
||||
});
|
||||
|
||||
it('automatically picks an interval', function () {
|
||||
|
|
|
@ -22,10 +22,17 @@ import { AggConfig } from '../vis';
|
|||
interface AggParam {
|
||||
type: string;
|
||||
name: string;
|
||||
options?: AggParamOption[];
|
||||
required?: boolean;
|
||||
displayName?: string;
|
||||
onChange?(agg: AggConfig): void;
|
||||
shouldShow?(agg: AggConfig): boolean;
|
||||
}
|
||||
|
||||
export { AggParam };
|
||||
interface AggParamOption {
|
||||
val: string;
|
||||
display: string;
|
||||
enabled?(agg: AggConfig): void;
|
||||
}
|
||||
|
||||
export { AggParam, AggParamOption };
|
||||
|
|
|
@ -77,11 +77,5 @@ export const intervalOptions = [
|
|||
defaultMessage: 'Yearly',
|
||||
}),
|
||||
val: 'y'
|
||||
},
|
||||
{
|
||||
display: i18n.translate('common.ui.aggTypes.buckets.intervalOptions.customDisplayName', {
|
||||
defaultMessage: 'Custom',
|
||||
}),
|
||||
val: 'custom'
|
||||
}
|
||||
];
|
||||
|
|
|
@ -20,12 +20,11 @@
|
|||
import _ from 'lodash';
|
||||
import chrome from '../../chrome';
|
||||
import moment from 'moment-timezone';
|
||||
import '../directives/validate_date_interval';
|
||||
import { BucketAggType } from './_bucket_agg_type';
|
||||
import { TimeBuckets } from '../../time_buckets';
|
||||
import { createFilterDateHistogram } from './create_filter/date_histogram';
|
||||
import { intervalOptions } from './_interval_options';
|
||||
import intervalTemplate from '../controls/time_interval.html';
|
||||
import { TimeIntervalParamEditor } from '../controls/time_interval';
|
||||
import { timefilter } from '../../timefilter';
|
||||
import { DropPartialsParamEditor } from '../controls/drop_partials';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -35,11 +34,7 @@ const detectedTimezone = moment.tz.guess();
|
|||
const tzOffset = moment().format('Z');
|
||||
|
||||
function getInterval(agg) {
|
||||
const interval = _.get(agg, ['params', 'interval']);
|
||||
if (interval && interval.val === 'custom') {
|
||||
return _.get(agg, ['params', 'customInterval']);
|
||||
}
|
||||
return interval;
|
||||
return _.get(agg, ['params', 'interval']);
|
||||
}
|
||||
|
||||
export function setBounds(agg, force) {
|
||||
|
@ -99,7 +94,7 @@ export const dateHistogramBucketAgg = new BucketAggType({
|
|||
return agg.getIndexPattern().timeFieldName;
|
||||
},
|
||||
onChange: function (agg) {
|
||||
if (_.get(agg, 'params.interval.val') === 'auto' && !agg.fieldIsTimeField()) {
|
||||
if (_.get(agg, 'params.interval') === 'auto' && !agg.fieldIsTimeField()) {
|
||||
delete agg.params.interval;
|
||||
}
|
||||
|
||||
|
@ -118,18 +113,24 @@ export const dateHistogramBucketAgg = new BucketAggType({
|
|||
},
|
||||
{
|
||||
name: 'interval',
|
||||
type: 'optioned',
|
||||
deserialize: function (state) {
|
||||
editorComponent: TimeIntervalParamEditor,
|
||||
deserialize: function (state, agg) {
|
||||
// For upgrading from 7.0.x to 7.1.x - intervals are now stored as key of options or custom value
|
||||
if (state === 'custom') {
|
||||
return _.get(agg, 'params.customInterval');
|
||||
}
|
||||
|
||||
const interval = _.find(intervalOptions, { val: state });
|
||||
return interval || _.find(intervalOptions, function (option) {
|
||||
// For upgrading from 4.0.x to 4.1.x - intervals are now stored as 'y' instead of 'year',
|
||||
// but this maps the old values to the new values
|
||||
return Number(moment.duration(1, state)) === Number(moment.duration(1, option.val));
|
||||
});
|
||||
|
||||
// For upgrading from 4.0.x to 4.1.x - intervals are now stored as 'y' instead of 'year',
|
||||
// but this maps the old values to the new values
|
||||
if (!interval && state === 'year') {
|
||||
return 'y';
|
||||
}
|
||||
return state;
|
||||
},
|
||||
default: 'auto',
|
||||
options: intervalOptions,
|
||||
editor: intervalTemplate,
|
||||
modifyAggConfigOnSearchRequestStart: function (agg) {
|
||||
setBounds(agg, true);
|
||||
},
|
||||
|
@ -186,12 +187,6 @@ export const dateHistogramBucketAgg = new BucketAggType({
|
|||
return field && field.name && field.name === agg.getIndexPattern().timeFieldName;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'customInterval',
|
||||
default: '2h',
|
||||
write: _.noop
|
||||
},
|
||||
{
|
||||
name: 'format'
|
||||
},
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import '../directives/validate_date_interval';
|
||||
import chrome from '../../chrome';
|
||||
import { BucketAggType } from './_bucket_agg_type';
|
||||
import { createFilterHistogram } from './create_filter/histogram';
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
<div class="form-group">
|
||||
<label
|
||||
for="visEditorInterval{{agg.id}}"
|
||||
>
|
||||
<span
|
||||
i18n-id="common.ui.aggTypes.timeIntervalLabel"
|
||||
i18n-default-message="Interval"
|
||||
></span>
|
||||
<icon-tip
|
||||
ng-if="agg.buckets.getInterval().scaled && (agg.buckets.getInterval().scale <= 1)"
|
||||
type="'alert'"
|
||||
position="'right'"
|
||||
content="'common.ui.aggTypes.intervalCreatesTooManyBucketsTooltip' | i18n: {
|
||||
defaultMessage: 'This interval creates too many buckets to show in the selected time range, so it has been scaled to {bucketDescription}',
|
||||
values: { bucketDescription: agg.buckets.getInterval().description } }"
|
||||
></icon-tip>
|
||||
<icon-tip
|
||||
ng-if="agg.buckets.getInterval().scaled && (agg.buckets.getInterval().scale > 1)"
|
||||
type="'alert'"
|
||||
position="'right'"
|
||||
content="'common.ui.aggTypes.intervalCreatesTooLargeBucketsTooltip' | i18n: {
|
||||
defaultMessage: 'This interval creates buckets that are too large to show in the selected time range, so it has been scaled to {bucketDescription}',
|
||||
values: { bucketDescription: agg.buckets.getInterval().description } }"
|
||||
></icon-tip>
|
||||
</label>
|
||||
<select
|
||||
ng-if="!editorConfig.customInterval.timeBase"
|
||||
id="visEditorInterval{{agg.id}}"
|
||||
ng-model="agg.params.interval"
|
||||
ng-change="agg.write()"
|
||||
required
|
||||
ng-options="opt as opt.display for opt in aggParam.options.raw | filter: optionEnabled"
|
||||
class="form-control"
|
||||
name="interval">
|
||||
<option
|
||||
value=""
|
||||
i18n-id="common.ui.aggTypes.selectTimeIntervalLabel"
|
||||
i18n-default-message="-- select a valid interval --"
|
||||
></option>
|
||||
</select>
|
||||
<input
|
||||
aria-label="{{ ::'common.ui.aggTypes.customTimeIntervalAriaLabel' | i18n: { defaultMessage: 'Custom interval' } }}"
|
||||
type="text"
|
||||
name="customInterval"
|
||||
ng-model="agg.params.customInterval"
|
||||
validate-date-interval="{{editorConfig.customInterval.timeBase}}"
|
||||
ng-change="aggForm.customInterval.$valid && agg.write()"
|
||||
ng-if="agg.params.interval.val == 'custom'"
|
||||
class="form-control"
|
||||
required />
|
||||
<div
|
||||
ng-if="editorConfig.customInterval.help"
|
||||
class="kuiSubText kuiSubduedText kuiVerticalRhythmSmall"
|
||||
style="margin-top: 5px"
|
||||
>
|
||||
<span>{{editorConfig.customInterval.help}}</span>
|
||||
</div>
|
||||
</div>
|
172
src/legacy/ui/public/agg_types/controls/time_interval.tsx
Normal file
172
src/legacy/ui/public/agg_types/controls/time_interval.tsx
Normal file
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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 { get, find } from 'lodash';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { EuiFormRow, EuiIconTip, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { AggParamEditorProps } from '../../vis/editors/default';
|
||||
import { AggParamOption } from '../agg_param';
|
||||
import { isValidInterval } from '../utils';
|
||||
|
||||
interface ComboBoxOption extends EuiComboBoxOptionProps {
|
||||
key: string;
|
||||
}
|
||||
|
||||
function TimeIntervalParamEditor({
|
||||
agg,
|
||||
aggParam,
|
||||
editorConfig,
|
||||
value,
|
||||
setValue,
|
||||
showValidation,
|
||||
setValidity,
|
||||
}: AggParamEditorProps<string>) {
|
||||
const timeBase: string = get(editorConfig, 'interval.timeBase');
|
||||
const options = timeBase
|
||||
? []
|
||||
: (aggParam.options || []).reduce(
|
||||
(filtered: ComboBoxOption[], option: AggParamOption) => {
|
||||
if (option.enabled ? option.enabled(agg) : true) {
|
||||
filtered.push({ label: option.display, key: option.val });
|
||||
}
|
||||
return filtered;
|
||||
},
|
||||
[] as ComboBoxOption[]
|
||||
);
|
||||
|
||||
let selectedOptions: ComboBoxOption[] = [];
|
||||
let definedOption: ComboBoxOption | undefined;
|
||||
let isValid = false;
|
||||
if (value) {
|
||||
definedOption = find(options, { key: value });
|
||||
selectedOptions = definedOption ? [definedOption] : [{ label: value, key: 'custom' }];
|
||||
isValid = !!(definedOption || isValidInterval(value, timeBase));
|
||||
}
|
||||
|
||||
const interval = get(agg, 'buckets.getInterval') && agg.buckets.getInterval();
|
||||
const scaledHelpText =
|
||||
interval && interval.scaled && isValid ? (
|
||||
<strong className="eui-displayBlock">
|
||||
<FormattedMessage
|
||||
id="common.ui.aggTypes.timeInterval.scaledHelpText"
|
||||
defaultMessage="Currently scaled to {bucketDescription}"
|
||||
values={{ bucketDescription: get(interval, 'description') || '' }}
|
||||
/>{' '}
|
||||
<EuiIconTip
|
||||
position="right"
|
||||
type="questionInCircle"
|
||||
content={interval.scale <= 1 ? tooManyBucketsTooltip : tooLargeBucketsTooltip}
|
||||
/>
|
||||
</strong>
|
||||
) : null;
|
||||
|
||||
const helpText = (
|
||||
<>
|
||||
{scaledHelpText}
|
||||
{get(editorConfig, 'interval.help') || selectOptionHelpText}
|
||||
</>
|
||||
);
|
||||
|
||||
const errors = [];
|
||||
|
||||
if (!isValid && value) {
|
||||
errors.push(
|
||||
i18n.translate('common.ui.aggTypes.timeInterval.invalidFormatErrorMessage', {
|
||||
defaultMessage: 'Invalid interval format.',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const onCustomInterval = (customValue: string) => {
|
||||
const normalizedCustomValue = customValue.trim();
|
||||
setValue(normalizedCustomValue);
|
||||
|
||||
if (normalizedCustomValue && isValidInterval(normalizedCustomValue, timeBase)) {
|
||||
agg.write();
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (opts: EuiComboBoxOptionProps[]) => {
|
||||
const selectedOpt: ComboBoxOption = get(opts, '0');
|
||||
setValue(selectedOpt ? selectedOpt.key : selectedOpt);
|
||||
|
||||
if (selectedOpt) {
|
||||
agg.write();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
setValidity(isValid);
|
||||
},
|
||||
[isValid]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
className="visEditorSidebar__aggParamFormRow"
|
||||
error={errors}
|
||||
fullWidth={true}
|
||||
helpText={helpText}
|
||||
isInvalid={showValidation ? !isValid : false}
|
||||
label={i18n.translate('common.ui.aggTypes.timeInterval.minimumIntervalLabel', {
|
||||
defaultMessage: 'Minimum interval',
|
||||
})}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth={true}
|
||||
data-test-subj="visEditorInterval"
|
||||
isInvalid={showValidation ? !isValid : false}
|
||||
noSuggestions={!!timeBase}
|
||||
onChange={onChange}
|
||||
onCreateOption={onCustomInterval}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
singleSelection={{ asPlainText: true }}
|
||||
placeholder={i18n.translate('common.ui.aggTypes.timeInterval.selectIntervalPlaceholder', {
|
||||
defaultMessage: 'Select an interval',
|
||||
})}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
}
|
||||
|
||||
const tooManyBucketsTooltip = (
|
||||
<FormattedMessage
|
||||
id="common.ui.aggTypes.timeInterval.createsTooManyBucketsTooltip"
|
||||
defaultMessage="This interval creates too many buckets to show in the selected time range, so it has been scaled up."
|
||||
/>
|
||||
);
|
||||
const tooLargeBucketsTooltip = (
|
||||
<FormattedMessage
|
||||
id="common.ui.aggTypes.timeInterval.createsTooLargeBucketsTooltip"
|
||||
defaultMessage="This interval creates buckets that are too large to show in the selected time range, so it has been scaled down."
|
||||
/>
|
||||
);
|
||||
const selectOptionHelpText = (
|
||||
<FormattedMessage
|
||||
id="common.ui.aggTypes.timeInterval.selectOptionHelpText"
|
||||
defaultMessage="Select an option or create a custom value. Examples: 30s, 20m, 24h, 2d, 1w, 1M"
|
||||
/>
|
||||
);
|
||||
|
||||
export { TimeIntervalParamEditor };
|
|
@ -1,59 +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 '../../utils/parse_interval';
|
||||
import { uiModules } from '../../modules';
|
||||
import { leastCommonInterval } from '../../vis/lib/least_common_interval';
|
||||
|
||||
uiModules
|
||||
.get('kibana')
|
||||
.directive('validateDateInterval', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function ($scope, $el, attrs, ngModelCntrl) {
|
||||
const baseInterval = attrs.validateDateInterval || null;
|
||||
|
||||
ngModelCntrl.$parsers.push(check);
|
||||
ngModelCntrl.$formatters.push(check);
|
||||
|
||||
function check(value) {
|
||||
if(baseInterval) {
|
||||
ngModelCntrl.$setValidity('dateInterval', parseWithBase(value) === true);
|
||||
} else {
|
||||
ngModelCntrl.$setValidity('dateInterval', parseInterval(value) != null);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// When base interval is set, check for least common interval and allow
|
||||
// input the value is the same. This means that the input interval is a
|
||||
// multiple of the base interval.
|
||||
function parseWithBase(value) {
|
||||
try {
|
||||
const interval = leastCommonInterval(baseInterval, value);
|
||||
return interval === value.replace(/\s/g, '');
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
2
src/legacy/ui/public/agg_types/index.d.ts
vendored
2
src/legacy/ui/public/agg_types/index.d.ts
vendored
|
@ -17,5 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { AggParam } from './agg_param';
|
||||
export { AggParam, AggParamOption } from './agg_param';
|
||||
export { AggType } from './agg_type';
|
||||
|
|
72
src/legacy/ui/public/agg_types/utils.ts
Normal file
72
src/legacy/ui/public/agg_types/utils.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 '../utils/parse_interval';
|
||||
import { leastCommonInterval } from '../vis/lib/least_common_interval';
|
||||
|
||||
/**
|
||||
* Check a string if it's a valid JSON.
|
||||
*
|
||||
* @param {string} value a string that should be validated
|
||||
* @returns {boolean} true if value is a valid JSON or if value is an empty string, or a string with whitespaces, otherwise false
|
||||
*/
|
||||
function isValidJson(value: string): boolean {
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
if (trimmedValue.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trimmedValue[0] === '{' || trimmedValue[0] === '[') {
|
||||
try {
|
||||
JSON.parse(trimmedValue);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isValidInterval(value: string, baseInterval: string) {
|
||||
if (baseInterval) {
|
||||
return _parseWithBase(value, baseInterval);
|
||||
} else {
|
||||
return parseInterval(value) !== null;
|
||||
}
|
||||
}
|
||||
|
||||
// When base interval is set, check for least common interval and allow
|
||||
// input the value is the same. This means that the input interval is a
|
||||
// multiple of the base interval.
|
||||
function _parseWithBase(value: string, baseInterval: string) {
|
||||
try {
|
||||
const interval = leastCommonInterval(baseInterval, value);
|
||||
return interval === value.replace(/\s/g, '');
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { isValidJson, isValidInterval };
|
|
@ -17,27 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
function isValidJson(value: string): boolean {
|
||||
if (!value || value.length === 0) {
|
||||
return true;
|
||||
}
|
||||
import moment from 'moment';
|
||||
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
if (trimmedValue.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (trimmedValue[0] === '{' || trimmedValue[0] === '[') {
|
||||
try {
|
||||
JSON.parse(trimmedValue);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { isValidJson };
|
||||
export function parseInterval(interval: string): moment.Duration | null;
|
|
@ -17,7 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { isFunction } from 'lodash';
|
||||
import { wrapInI18nContext } from 'ui/i18n';
|
||||
import { uiModules } from '../../../modules';
|
||||
import { AggParamReactWrapper } from './agg_param_react_wrapper';
|
||||
|
@ -74,22 +73,12 @@ uiModules
|
|||
link: {
|
||||
pre: function ($scope, $el, attr) {
|
||||
$scope.$bind('aggParam', attr.aggParam);
|
||||
$scope.$bind('agg', attr.agg);
|
||||
$scope.$bind('editorComponent', attr.editorComponent);
|
||||
$scope.$bind('indexedFields', attr.indexedFields);
|
||||
},
|
||||
post: function ($scope, $el, attr, ngModelCtrl) {
|
||||
$scope.config = config;
|
||||
$scope.showValidation = false;
|
||||
|
||||
$scope.optionEnabled = function (option) {
|
||||
if (option && isFunction(option.enabled)) {
|
||||
return option.enabled($scope.agg);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (attr.editorComponent) {
|
||||
$scope.$watch('agg.params[aggParam.name]', (value) => {
|
||||
// Whenever the value of the parameter changed (e.g. by a reset or actually by calling)
|
||||
|
@ -105,14 +94,13 @@ uiModules
|
|||
$scope.showValidation = true;
|
||||
}
|
||||
}, true);
|
||||
|
||||
$scope.paramValue = $scope.agg.params[$scope.aggParam.name];
|
||||
}
|
||||
|
||||
$scope.onChange = (value) => {
|
||||
// This is obviously not a good code quality, but without using scope binding (which we can't see above)
|
||||
// to bind function values, this is right now the best temporary fix, until all of this will be gone.
|
||||
$scope.$parent.onParamChange($scope.agg, $scope.aggParam.name, value);
|
||||
|
||||
$scope.paramValue = value;
|
||||
$scope.onParamChange($scope.agg, $scope.aggParam.name, value);
|
||||
$scope.showValidation = true;
|
||||
ngModelCtrl.$setDirty();
|
||||
};
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { AggParam } from '../../../agg_types';
|
||||
import { AggConfig } from '../../agg_config';
|
||||
import { FieldParamType } from '../../../agg_types/param_types';
|
||||
import { EditorConfig } from '../config/types';
|
||||
|
||||
// NOTE: we cannot export the interface with export { InterfaceName }
|
||||
|
@ -29,7 +30,7 @@ export interface AggParamEditorProps<T> {
|
|||
agg: AggConfig;
|
||||
aggParam: AggParam;
|
||||
editorConfig: EditorConfig;
|
||||
indexedFields?: any[];
|
||||
indexedFields?: FieldParamType[];
|
||||
showValidation: boolean;
|
||||
value: T;
|
||||
setValidity(isValid: boolean): void;
|
||||
|
|
|
@ -217,7 +217,7 @@ uiModules
|
|||
}
|
||||
|
||||
function normalizeModelName(modelName = '') {
|
||||
return modelName.replace('-', '_');
|
||||
return modelName.replace(/-/g, '_');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -50,7 +50,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
expect(fieldValues[0]).to.be('@timestamp');
|
||||
const intervalValue = await PageObjects.visualize.getInterval();
|
||||
log.debug('intervalValue = ' + intervalValue);
|
||||
expect(intervalValue).to.be('Auto');
|
||||
expect(intervalValue[0]).to.be('Auto');
|
||||
return PageObjects.visualize.clickGo();
|
||||
};
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }) {
|
|||
log.debug('Field = @timestamp');
|
||||
await PageObjects.visualize.selectField('@timestamp');
|
||||
await PageObjects.visualize.setCustomInterval('3h');
|
||||
await PageObjects.visualize.clickGo();
|
||||
await PageObjects.visualize.waitForVisualizationRenderingStabilized();
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -567,23 +567,17 @@ export function VisualizePageProvider({ getService, getPageObjects, updateBaseli
|
|||
}
|
||||
|
||||
async getInterval() {
|
||||
const intervalElement = await find.byCssSelector(
|
||||
`select[ng-model="agg.params.interval"] option[selected]`);
|
||||
return await intervalElement.getProperty('label');
|
||||
return await comboBox.getComboBoxSelectedOptions('visEditorInterval');
|
||||
}
|
||||
|
||||
async setInterval(newValue) {
|
||||
log.debug(`Visualize.setInterval(${newValue})`);
|
||||
const input = await find.byCssSelector('select[ng-model="agg.params.interval"]');
|
||||
const option = await input.findByCssSelector(`option[label="${newValue}"]`);
|
||||
await option.click();
|
||||
return await comboBox.set('visEditorInterval', newValue);
|
||||
}
|
||||
|
||||
async setCustomInterval(newValue) {
|
||||
await this.setInterval('Custom');
|
||||
const input = await find.byCssSelector('input[name="customInterval"]');
|
||||
await input.clearValue();
|
||||
await input.type(newValue);
|
||||
log.debug(`Visualize.setCustomInterval(${newValue})`);
|
||||
return await comboBox.setCustom('visEditorInterval', newValue);
|
||||
}
|
||||
|
||||
async getNumericInterval(agg = 2) {
|
||||
|
|
|
@ -17,13 +17,14 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export function ComboBoxProvider({ getService }) {
|
||||
export function ComboBoxProvider({ getService, getPageObjects }) {
|
||||
const config = getService('config');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const find = getService('find');
|
||||
const log = getService('log');
|
||||
const retry = getService('retry');
|
||||
const browser = getService('browser');
|
||||
const PageObjects = getPageObjects(['common']);
|
||||
|
||||
const WAIT_FOR_EXISTS_TIME = config.get('timeouts.waitForExists');
|
||||
|
||||
|
@ -60,6 +61,21 @@ export function ComboBoxProvider({ getService }) {
|
|||
await this.closeOptionsList(comboBoxElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method set custom value to comboBox.
|
||||
* It applies changes by pressing Enter key. Sometimes it may lead to auto-submitting a form.
|
||||
*
|
||||
* @param {string} comboBoxSelector
|
||||
* @param {string} value
|
||||
*/
|
||||
async setCustom(comboBoxSelector, value) {
|
||||
log.debug(`comboBox.setCustom, comboBoxSelector: ${comboBoxSelector}, value: ${value}`);
|
||||
const comboBoxElement = await testSubjects.find(comboBoxSelector);
|
||||
await this._filterOptionsList(comboBoxElement, value);
|
||||
await PageObjects.common.pressEnterKey();
|
||||
await this.closeOptionsList(comboBoxElement);
|
||||
}
|
||||
|
||||
async filterOptionsList(comboBoxSelector, filterValue) {
|
||||
log.debug(`comboBox.filterOptionsList, comboBoxSelector: ${comboBoxSelector}, filter: ${filterValue}`);
|
||||
const comboBox = await testSubjects.find(comboBoxSelector);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { editorConfigProviders } from 'ui/vis/editors/config/editor_config_providers';
|
||||
|
||||
export function initEditorConfig() {
|
||||
|
@ -45,7 +46,10 @@ export function initEditorConfig() {
|
|||
},
|
||||
interval: {
|
||||
base: interval,
|
||||
help: `Must be a multiple of rollup configuration interval: ${interval}`
|
||||
help: i18n.translate('xpack.rollupJobs.editorConfig.histogram.interval.helpText', {
|
||||
defaultMessage: 'Must be a multiple of rollup configuration interval: {interval}',
|
||||
values: { interval }
|
||||
})
|
||||
}
|
||||
} : {};
|
||||
}
|
||||
|
@ -54,16 +58,16 @@ export function initEditorConfig() {
|
|||
if (aggTypeName === 'date_histogram') {
|
||||
const interval = fieldAgg.interval;
|
||||
return {
|
||||
interval: {
|
||||
fixedValue: 'custom',
|
||||
},
|
||||
useNormalizedEsInterval: {
|
||||
fixedValue: false,
|
||||
},
|
||||
customInterval: {
|
||||
interval: {
|
||||
default: interval,
|
||||
timeBase: interval,
|
||||
help: `Must be a multiple of rollup configuration interval: ${interval}`
|
||||
help: i18n.translate('xpack.rollupJobs.editorConfig.dateHistogram.customInterval.helpText', {
|
||||
defaultMessage: 'Must be a multiple of rollup configuration interval: {interval}',
|
||||
values: { interval }
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -89,7 +89,6 @@
|
|||
"common.ui.aggTypes.buckets.geohashGridTitle": "Geohash",
|
||||
"common.ui.aggTypes.buckets.histogramTitle": "直方图",
|
||||
"common.ui.aggTypes.buckets.intervalOptions.autoDisplayName": "自动",
|
||||
"common.ui.aggTypes.buckets.intervalOptions.customDisplayName": "定制",
|
||||
"common.ui.aggTypes.buckets.intervalOptions.dailyDisplayName": "每日",
|
||||
"common.ui.aggTypes.buckets.intervalOptions.hourlyDisplayName": "每小时",
|
||||
"common.ui.aggTypes.buckets.intervalOptions.millisecondDisplayName": "毫秒",
|
||||
|
@ -112,7 +111,6 @@
|
|||
"common.ui.aggTypes.buckets.termsTitle": "词",
|
||||
"common.ui.aggTypes.changePrecisionLabel": "更改地图缩放的精确度",
|
||||
"common.ui.aggTypes.customMetricLabel": "定制指标",
|
||||
"common.ui.aggTypes.customTimeIntervalAriaLabel": "定制时间间隔",
|
||||
"common.ui.aggTypes.dateRanges.acceptedDateFormatsLinkText": "已接受日期格式",
|
||||
"common.ui.aggTypes.dateRanges.addRangeButtonLabel": "添加范围",
|
||||
"common.ui.aggTypes.dateRanges.fromColumnLabel": "从",
|
||||
|
@ -135,8 +133,6 @@
|
|||
"common.ui.aggTypes.filters.requiredFilterLabel": "必需:",
|
||||
"common.ui.aggTypes.filters.toggleFilterButtonAriaLabel": "切换筛选标签",
|
||||
"common.ui.aggTypes.histogram.missingMaxMinValuesWarning": "无法检索最大值和最小值以自动缩放直方图存储桶。这可能会导致可视化性能低下。",
|
||||
"common.ui.aggTypes.intervalCreatesTooLargeBucketsTooltip": "此时间间隔将创建过大而无法在选定时间范围内显示的存储桶,因此其已缩放至 {bucketDescription}",
|
||||
"common.ui.aggTypes.intervalCreatesTooManyBucketsTooltip": "此时间间隔将创建过多的存储桶,而无法在选定时间范围内全部显示,因此其已缩放至 {bucketDescription}",
|
||||
"common.ui.aggTypes.ipRanges.cidrMask.addRangeButtonLabel": "添加范围",
|
||||
"common.ui.aggTypes.ipRanges.cidrMask.requiredIpRangeDescription": "必须指定至少一个 IP 范围。",
|
||||
"common.ui.aggTypes.ipRanges.cidrMask.requiredIpRangeLabel": "必需:",
|
||||
|
@ -239,7 +235,6 @@
|
|||
"common.ui.aggTypes.ranges.requiredRangeDescription": "必须指定至少一个范围。",
|
||||
"common.ui.aggTypes.ranges.requiredRangeTitle": "必需:",
|
||||
"common.ui.aggTypes.ranges.toColumnLabel": "到",
|
||||
"common.ui.aggTypes.selectTimeIntervalLabel": "-- 选择有效的时间间隔 --",
|
||||
"common.ui.aggTypes.showEmptyBucketsLabel": "显示空存储桶",
|
||||
"common.ui.aggTypes.showEmptyBucketsTooltip": "显示所有存储桶,不仅仅有结果的存储桶",
|
||||
"common.ui.aggTypes.sizeLabel": "大小",
|
||||
|
@ -247,7 +242,6 @@
|
|||
"common.ui.aggTypes.sortOnLabel": "排序依据",
|
||||
"common.ui.aggTypes.sortOnPlaceholder": "选择字段",
|
||||
"common.ui.aggTypes.sortOnTooltip": "排序依据",
|
||||
"common.ui.aggTypes.timeIntervalLabel": "时间间隔",
|
||||
"common.ui.aggTypes.valuesLabel": "值",
|
||||
"common.ui.chrome.bigUrlWarningNotificationMessage": "在“{advancedSettingsLink}”启用“{storeInSessionStorageParam}”选项,或简化屏幕视觉效果。",
|
||||
"common.ui.chrome.bigUrlWarningNotificationMessage.advancedSettingsLinkText": "高级设置",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue