[indexPatterns] remove support for time patterns (#12158)

* [indexPatterns] remove support for time patterns

* Revert "[indexPatterns] remove support for time patterns"

This reverts commit 4263e37c66.

* [indexPatterns] remove ability to create time-based patterns

* [indexPattern/routes] fix export of routes for stub

* [Storage] export Storage class for testing

* [indexPatterns/unsupportedTimePatterns] add tests

* [indexPatterns] focus warning check module

* [indexPatterns/tests] fix method name

* add metion of this change to migration docs

* disable warnings by default until we have a migration tool

* prevent the warning from disapearing

* fix grammar

* enabled warnings in the tests
This commit is contained in:
Spencer 2017-06-13 17:22:41 -07:00 committed by GitHub
parent 2a0ed8e367
commit 1119414b49
25 changed files with 210 additions and 562 deletions

View file

@ -52,3 +52,9 @@ This is no longer the case. Now, only commas are a valid query separator: e.g. `
*Details:* In Kibana 4.2, we renamed all configuration names in kibana.yml to use `.` as a separator instead of `_`, though the legacy configurations would still continue to work. In 5.0, we started logging deprecation notices whenever the legacy configurations were encountered. In 6.0 onward, legacy configuration names that use an underscore instead of a dot will no longer work.
*Impact:* Any usages of underscore separated configuration names in kibana.yml need to be updated to their modern equivalents. See <<settings,Configuring Kibana>> for accepted configurations.
[float]
=== Time-interval based index patterns are no longer supported
*Details:* Starting in Kibana 6.0.0 we removed the ability to create index patterns that use a date-pattern and interval to identify Elasticsearch indices. Index patterns must now use wildcards which are more performant in most cases.
*Impact:* Existing index patterns and saved objects will continue to function without issue, and in a subsequent release we will provide utilities to migrate your index patterns/saved objects.

View file

@ -28,7 +28,6 @@ describe('createIndexPattern UI', () => {
beforeEach(ngMock.inject(($injector) => {
setup = function () {
const Private = $injector.get('Private');
const Promise = $injector.get('Promise');
const $compile = $injector.get('$compile');
const $rootScope = $injector.get('$rootScope');
@ -39,12 +38,6 @@ describe('createIndexPattern UI', () => {
trash.push(() => $scope.$destroy());
$scope.$apply();
// prevents errors when switching to time pattern
indexPatternsApiClient.testTimePattern = sinon.spy(() => Promise.resolve({
all: ['logstash-0', 'logstash-2017.01.01'],
matches: ['logstash-2017.01.01'],
}));
const setNameTo = (name) => {
$view.findTestSubject('createIndexPatternNameInput')
.val(name)
@ -122,13 +115,6 @@ describe('createIndexPattern UI', () => {
expect($enableExpand).to.have.length(1);
expect($enableExpand.is(':checked')).to.be(false);
});
it('displays the option (off) to use time patterns', () => {
const { $view } = setup();
const $enableTimePattern = $view.findTestSubject('createIndexPatternNameIsPatternCheckBox');
expect($enableTimePattern).to.have.length(1);
expect($enableTimePattern.is(':checked')).to.be(false);
});
});
describe('cross cluster pattern', () => {
@ -149,43 +135,5 @@ describe('createIndexPattern UI', () => {
const $enableExpand = $view.findTestSubject('createIndexPatternEnableExpand');
expect($enableExpand).to.have.length(0);
});
it('removes the option to use time patterns', () => {
const { $view, setNameTo } = setup();
setNameTo('cluster2:logstash-*');
const $enableTimePattern = $view.findTestSubject('createIndexPatternNameIsPatternCheckBox');
expect($enableTimePattern).to.have.length(0);
});
});
describe('expand selected', () => {
it('removes the option to use time patterns', () => {
const { $view } = setup();
const { controller } = $view.findTestSubject('createIndexPatternContainer').scope();
const $enableExpand = $view.findTestSubject('createIndexPatternEnableExpand');
expect($enableExpand).to.have.length(1);
$enableExpand.click();
expect(controller.isExpandWildcardEnabled()).to.be(true);
const $enableTimePattern = $view.findTestSubject('createIndexPatternNameIsPatternCheckBox');
expect($enableTimePattern).to.have.length(0);
});
});
describe('time pattern selected', () => {
it('removes the option to use wildcard expansion', () => {
const { $view } = setup();
const { controller } = $view.findTestSubject('createIndexPatternContainer').scope();
const $enableTimePattern = $view.findTestSubject('createIndexPatternNameIsPatternCheckBox');
expect($enableTimePattern).to.have.length(1);
$enableTimePattern.click();
expect(controller.formValues.nameIsPattern).to.be(true);
const $enableExpand = $view.findTestSubject('createIndexPatternEnableExpand');
expect($enableExpand).to.have.length(0);
});
});
});

View file

@ -61,37 +61,8 @@
<div class="kuiVerticalRhythm">
<p
class="kuiSubText kuiVerticalRhythmSmall"
ng-if="!controller.formValues.nameIsPattern"
translate="KIBANA-WILDCARD_DYNAMIC_INDEX_PATTERNS"
></p>
<p
class="kuiSubText kuiVerticalRhythmSmall"
ng-if="controller.formValues.nameIsPattern"
>
<span translate="KIBANA-STATIC_TEXT_IN_DYNAMIC_INDEX_PATTERNS"></span> &mdash;
<a
class="kuiLink"
href="http://momentjs.com/docs/#/displaying/format/"
target="_blank"
translate="KIBANA-DATE_FORMAT_DOCS"
></a>
</p>
<p
class="kuiSubText kuiVerticalRhythmSmall"
ng-show="controller.formValues.nameInterval.name == 'weeks'"
>
<strong translate="KIBANA-NOTE_COLON"></strong>&nbsp;
<span translate="KIBANA-WEEKLY_ISO_NOTICE"></span>
<span translate="KIBANA-SEE"></span>
<a
class="kuiLink"
href="https://en.wikipedia.org/wiki/ISO_week_date"
target="_blank"
translate="KIBANA-WIKI_ISO_WEEK_DATE"
></a>
</p>
</div>
</div>
@ -160,188 +131,6 @@
</div>
</div>
<!-- Use event times checkbox -->
<div
class="kuiVerticalRhythm"
ng-if="controller.canUseTimePattern()"
>
<label class="kuiCheckBoxLabel">
<input
class="kuiCheckBox"
type="checkbox"
data-test-subj="createIndexPatternNameIsPatternCheckBox"
ng-model="controller.formValues.nameIsPattern"
>
<span class="kuiCheckBoxLabel__text">
<span translate="KIBANA-INDEX_NAME_CREATED_BY_EVENT_TIMES"></span>
<span translate="KIBANA-DEPRECATED"></span>
</span>
</label>
</div>
<div
class="kuiVerticalRhythm"
ng-if="controller.canUseTimePattern() && controller.formValues.nameIsPattern"
>
<!-- Time-interval deprecation warning -->
<div class="kuiInfoPanel kuiInfoPanel--warning kuiVerticalRhythm">
<div class="kuiInfoPanelHeader">
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--warning fa-bolt"></span>
<span
class="kuiInfoPanelHeader__title"
translate="KIBANA-ALERT_INDEX_PATTERN_DEPRECATED"
></span>
</div>
<div class="kuiInfoPanelBody">
<div class="kuiInfoPanelBody__message">
<span translate="KIBANA-WE"></span>
<strong translate="KIBANA-STRONGLY_RECOMMEND"></strong>
<span translate="KIBANA-WILD_CARD_PATTERN"></span>
</div>
</div>
</div>
<!-- Index pattern interval select -->
<div class="kuiVerticalRhythm">
<label class="kuiLabel kuiVerticalRhythmSmall">
<span translate="KIBANA-INDEX_PATTERN_INTERVAL"></span>&nbsp;
<kbn-info info="{{ 'KIBANA-INTERVAL_INDEX_NAMES_ROTATE' | translate }}"></kbn-info>
</label>
<div class="kuiVerticalRhythmSmall">
<select
class="kuiSelect"
required
ng-options="opt.display for opt in controller.nameIntervalOptions"
ng-model="controller.formValues.nameInterval"
></select>
</div>
</div>
</div>
<!-- Errors -->
<div class="kuiVerticalRhythm">
<div
class="kuiInfoPanel kuiInfoPanel--error kuiVerticalRhythmSmall"
ng-repeat="err in controller.patternErrors"
>
<div class="kuiInfoPanelHeader">
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--error fa-warning"></span>
<span class="kuiInfoPanelHeader__title">
{{err}}
</span>
</div>
</div>
<div
class="kuiInfoPanel kuiInfoPanel--info kuiVerticalRhythmSmall"
ng-if="controller.samples"
>
<div class="kuiInfoPanelHeader">
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--info fa-info"></span>
<span
class="kuiInfoPanelHeader__title"
translate="KIBANA-SAMPLE_ALERT"
></span>
</div>
<div class="kuiInfoPanelBody">
<div
class="kuiInfoPanelBody__message"
ng-repeat="sample in controller.samples"
>
{{sample}}
</div>
<div class="kuiInfoPanelBody__message">
<button
type="button"
ng-click="controller.moreSamples(true)"
class="kuiButton kuiButton--basic"
>
<span translate="KIBANA-EXPAND_SEARCH"></span>
</button>
</div>
</div>
</div>
<!-- Percentage of indices which match the index pattern -->
<div
class="kuiInfoPanel kuiVerticalRhythmSmall"
ng-class="controller.existing.matches.length ? 'kuiInfoPanel--success' : 'kuiInfoPanel--error'"
ng-if="controller.existing"
>
<div class="kuiInfoPanelHeader">
<span
class="kuiInfoPanelHeader__icon kuiIcon"
ng-class="controller.existing.matches.length ? 'kuiIcon--success fa-check' : 'kuiIcon--error fa-warning'"
></span>
<span
class="kuiInfoPanelHeader__title"
translate="KIBANA-EXISTING_MATCH_PERCENT"
translate-values="{ indexExistingMatchPercent: '{{controller.existing.matchPercent}}' }"
></span>
</div>
<div
class="kuiInfoPanelBody"
ng-if="controller.existing.matches.length"
>
<div
class="kuiInfoPanelBody__message"
ng-repeat="match in controller.existing.matches | orderBy:'toString()'| limitTo: controller.sampleCount"
>
{{match}}
</div>
<div class="kuiInfoPanelBody__message">
<button
ng-if="controller.sampleCount < controller.existing.matches.length"
ng-click="controller.moreSamples()"
type="button"
class="kuiButton kuiButton--basic"
>
<span translate="KIBANA-EXPAND_SEARCH"></span>
</button>
</div>
</div>
</div>
<div
class="kuiInfoPanel kuiInfoPanel--info kuiVerticalRhythmSmall"
ng-if="controller.existing.failures.length"
>
<div class="kuiInfoPanelHeader">
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--info fa-info"></span>
<span
class="kuiInfoPanelHeader__title"
translate="KIBANA-NON_MATCHING_INDICES_AND_ALIASES"
></span>
</div>
<div class="kuiInfoPanelBody">
<div
class="kuiInfoPanelBody__message"
ng-repeat="match in controller.existing.failures | limitTo: controller.sampleCount"
>
{{match}}
</div>
</div>
<div class="kuiInfoPanelBody__message">
<a
class="kuiLink"
ng-if="controller.sampleCount < controller.existing.matches.length"
ng-click="controller.moreSamples()"
>
<span translate="KIBANA-MORE"></span>
</a>
</div>
</div>
</div>
<!-- Form actions -->
<button
data-test-subj="createIndexPatternCreateButton"

View file

@ -6,7 +6,6 @@ import { RefreshKibanaIndex } from '../refresh_kibana_index';
import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import template from './create_index_pattern.html';
import { getDefaultPatternForInterval } from './get_default_pattern_for_interval';
import { sendCreateIndexPatternRequest } from './send_create_index_pattern_request';
import { pickCreateButtonText } from './pick_create_button_text';
@ -19,41 +18,27 @@ uiModules.get('apps/management')
.controller('managementIndicesCreate', function ($scope, kbnUrl, Private, Notifier, indexPatterns, es, config, Promise, $translate) {
const notify = new Notifier();
const refreshKibanaIndex = Private(RefreshKibanaIndex);
const intervals = indexPatterns.intervals;
let loadingCount = 0;
// Configure the new index pattern we're going to create.
this.formValues = {
name: config.get('indexPattern:placeholder'),
nameIsPattern: false,
expandWildcard: false,
nameInterval: _.find(intervals, { name: 'daily' }),
timeFieldOption: null,
};
// UI state.
this.timeFieldOptions = [];
this.timeFieldOptionsError = null;
this.sampleCount = 5;
this.samples = null;
this.existing = null;
this.nameIntervalOptions = intervals;
this.patternErrors = [];
const getTimeFieldOptions = () => {
loadingCount += 1;
return Promise.resolve()
.then(() => {
const { nameIsPattern, name } = this.formValues;
const { name } = this.formValues;
if (!name) {
return [];
}
if (nameIsPattern) {
return indexPatterns.fieldsFetcher.fetchForTimePattern(name);
}
return indexPatterns.fieldsFetcher.fetchForWildcard(name);
})
.then(fields => {
@ -126,51 +111,6 @@ uiModules.get('apps/management')
return nonFieldOptions[0];
};
const resetIndex = () => {
this.patternErrors = [];
this.samples = null;
this.existing = null;
};
const updateSamples = () => {
const patternErrors = [];
if (!this.formValues.nameInterval || !this.formValues.name) {
return Promise.resolve();
}
loadingCount += 1;
return indexPatterns.fieldsFetcher.testTimePattern(this.formValues.name)
.then(existing => {
const all = _.get(existing, 'all', []);
const matches = _.get(existing, 'matches', []);
if (all.length) {
return this.existing = {
all,
matches,
matchPercent: Math.round((matches.length / all.length) * 100) + '%',
failures: _.difference(all, matches)
};
}
patternErrors.push($translate.instant('KIBANA-PATTERN_DOES_NOT_MATCH_EXIST_INDICES'));
const radius = Math.round(this.sampleCount / 2);
const samples = intervals.toIndexList(this.formValues.name, this.formValues.nameInterval, -radius, radius);
if (_.uniq(samples).length !== samples.length) {
patternErrors.push($translate.instant('KIBANA-INVALID_NON_UNIQUE_INDEX_NAME_CREATED'));
} else {
this.samples = samples;
}
throw patternErrors;
})
.finally(() => {
loadingCount -= 1;
});
};
this.isTimeBased = () => {
if (!this.formValues.timeFieldOption) {
// if they haven't choosen a time field, assume they will
@ -187,7 +127,6 @@ uiModules.get('apps/management')
return (
this.isTimeBased() &&
!this.isCrossClusterName() &&
!this.formValues.nameIsPattern &&
_.includes(this.formValues.name, '*')
);
};
@ -199,14 +138,6 @@ uiModules.get('apps/management')
);
};
this.canUseTimePattern = () => {
return (
this.isTimeBased() &&
!this.isExpandWildcardEnabled() &&
!this.isCrossClusterName()
);
};
this.isCrossClusterName = () => {
return (
this.formValues.name &&
@ -264,8 +195,6 @@ uiModules.get('apps/management')
const {
name,
timeFieldOption,
nameIsPattern,
nameInterval,
} = this.formValues;
const id = name;
@ -278,16 +207,10 @@ uiModules.get('apps/management')
? undefined
: true;
// Only event-time-based index patterns set an intervalName.
const intervalName = (this.canUseTimePattern() && nameIsPattern && nameInterval)
? nameInterval.name
: undefined;
loadingCount += 1;
sendCreateIndexPatternRequest(indexPatterns, {
id,
timeFieldName,
intervalName,
notExpandable,
}).then(createdId => {
if (!createdId) {
@ -316,65 +239,7 @@ uiModules.get('apps/management')
});
};
$scope.$watchMulti([
'controller.formValues.nameIsPattern',
'controller.formValues.nameInterval.name',
], (newVal, oldVal) => {
const nameIsPattern = newVal[0];
const newDefault = getDefaultPatternForInterval(newVal[1]);
const oldDefault = getDefaultPatternForInterval(oldVal[1]);
if (this.formValues.name === oldDefault) {
this.formValues.name = newDefault;
}
if (!nameIsPattern) {
delete this.formValues.nameInterval;
} else {
this.formValues.nameInterval = this.formValues.nameInterval || intervals.byName.days;
this.formValues.name = this.formValues.name || getDefaultPatternForInterval(this.formValues.nameInterval);
}
});
this.moreSamples = andUpdate => {
this.sampleCount += 5;
if (andUpdate) updateSamples();
};
let latestUpdateSampleId = -1;
$scope.$watchMulti([
'controller.formValues.name',
'controller.formValues.nameInterval'
], () => {
resetIndex();
// track the latestUpdateSampleId at the time we started
// so that we can avoid mutating the controller if the
// watcher triggers again before we finish (which would
// cause latestUpdateSampleId to increment and the
// id === latestUpdateSampleId checks below to fail)
const id = (++latestUpdateSampleId);
updateSamples()
.then(() => {
if (latestUpdateSampleId === id) {
this.samples = null;
this.patternErrors = [];
}
})
.catch(errors => {
if (latestUpdateSampleId === id) {
this.existing = null;
this.patternErrors = errors;
}
})
.finally(() => {
this.refreshTimeFieldOptions();
});
});
$scope.$watchMulti([
'controller.sampleCount'
], () => {
$scope.$watch('controller.formValues.name', () => {
this.refreshTimeFieldOptions();
});

View file

@ -1,7 +1,6 @@
export function sendCreateIndexPatternRequest(indexPatterns, {
id,
timeFieldName,
intervalName,
notExpandable,
}) {
// get an empty indexPattern to start
@ -12,7 +11,6 @@ export function sendCreateIndexPatternRequest(indexPatterns, {
Object.assign(indexPattern, {
timeFieldName,
intervalName,
notExpandable,
});

View file

@ -32,19 +32,21 @@
<!-- Alerts -->
<div
ng-if="indexPattern.timeFieldName && indexPattern.intervalName"
class="kuiInfoPanel kuiInfoPanel--warning kuiVerticalRhythm"
ng-if="indexPattern.isUnsupportedTimePattern()"
class="kuiInfoPanel kuiInfoPanel--error kuiVerticalRhythm"
>
<div class="kuiInfoPanelHeader">
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--warning fa-bolt"></span>
<span class="kuiInfoPanelHeader__icon kuiIcon kuiIcon--error fa-warning"></span>
<span class="kuiInfoPanelHeader__title">
Repeating index pattern
Support for repeating index patterns removed
</span>
</div>
<div class="kuiInfoPanelBody">
<div class="kuiInfoPanelBody__message">
This index uses a <strong>time-based index pattern</strong> which repeats <span ng-bind="::indexPattern.getInterval().display"></span>
Support for time-interval based index patterns has been removed! In the next major
version of Kibana this index patterns will stop working. Migrate saved objects that
use this index pattern to a wildcard pattern and delete this one.
</div>
</div>
</div>

View file

@ -182,6 +182,11 @@ export function getUiSettingDefaults() {
description: 'For index patterns containing timestamps in their names, look for this many recent matching ' +
'patterns from which to query the field mapping'
},
'indexPatterns:warnAboutUnsupportedTimePatterns': {
value: false,
description: 'When an index pattern is using the now unsupported "time pattern" format, a warning will ' +
'be displayed once per session that is using this pattern. Set this to false to disable that warning.'
},
'format:defaultTypeMap': {
type: 'json',
value:

View file

@ -1,7 +1,6 @@
import { IndexPatternsService } from './service';
import {
createTestTimePatternRoute,
createFieldsForWildcardRoute,
createFieldsForTimePatternRoute,
} from './routes';
@ -25,7 +24,6 @@ export function indexPatternsMixin(kbnServer, server) {
}
};
server.route(createTestTimePatternRoute(pre));
server.route(createFieldsForWildcardRoute(pre));
server.route(createFieldsForTimePatternRoute(pre));
}

View file

@ -1,3 +1,2 @@
export { createTestTimePatternRoute } from './test_time_pattern_route';
export { createFieldsForWildcardRoute } from './fields_for_wildcard_route';
export { createFieldsForTimePatternRoute } from './fields_for_time_pattern_route';

View file

@ -1,22 +0,0 @@
import Joi from 'joi';
export const createTestTimePatternRoute = pre => ({
path: '/api/index_patterns/_test_time_pattern',
method: 'GET',
config: {
pre: [pre.getIndexPatternsService],
validate: {
query: Joi.object().keys({
pattern: Joi.string().required()
}).default()
},
handler(req, reply) {
const { indexPatterns } = req.pre;
const { pattern } = req.query;
reply(indexPatterns.testTimePattern({
pattern,
}));
}
}
});

View file

@ -2,7 +2,6 @@ import {
getFieldCapabilities,
resolveTimePattern,
createNoMatchingIndicesError,
isNoMatchingIndicesError,
} from './lib';
export class IndexPatternsService {
@ -24,33 +23,6 @@ export class IndexPatternsService {
return await getFieldCapabilities(this._callDataCluster, pattern, metaFields);
}
/**
* Convert a time pattern into a list of indexes it could
* have matched and ones it did match.
*
* @param {Object} [options={}]
* @property {String} options.pattern The moment compatible time pattern
* @return {Promise<Object>} object that lists the indices that match based
* on a wildcard version of the time pattern (all)
* and the indices that actually match the time
* pattern (matches);
*/
async testTimePattern(options = {}) {
const { pattern } = options;
try {
return await resolveTimePattern(this._callDataCluster, pattern);
} catch (err) {
if (isNoMatchingIndicesError(err)) {
return {
all: [],
matches: []
};
}
throw err;
}
}
/**
* Get a list of field objects for a time pattern
*

View file

@ -1,3 +1,3 @@
export { getFieldCapabilities } from './field_capabilities';
export { resolveTimePattern } from './resolve_time_pattern';
export { createNoMatchingIndicesError, isNoMatchingIndicesError } from './errors';
export { createNoMatchingIndicesError } from './errors';

View file

@ -1,7 +1,7 @@
import _ from 'lodash';
import sinon from 'sinon';
import Promise from 'bluebird';
import { IndexPatternProvider } from 'ui/index_patterns/_index_pattern';
import { IndexPatternProvider, getRoutes } from 'ui/index_patterns/_index_pattern';
import { formatHit } from 'ui/index_patterns/_format_hit';
import { getComputedFields } from 'ui/index_patterns/_get_computed_fields';
import { RegistryFieldFormatsProvider } from 'ui/registry/field_formats';
@ -23,7 +23,7 @@ export default function (Private) {
this.getSourceFiltering = sinon.stub();
this.metaFields = ['_id', '_type', '_source'];
this.fieldFormatMap = {};
this.routes = IndexPatternProvider.routes;
this.routes = getRoutes();
this.toIndexList = _.constant(Promise.resolve(pattern.split(',')));
this.toDetailedIndexList = _.constant(Promise.resolve(pattern.split(',').map(index => ({

View file

@ -12,11 +12,15 @@ import UtilsMappingSetupProvider from 'ui/utils/mapping_setup';
import { IndexPatternsIntervalsProvider } from 'ui/index_patterns/_intervals';
import { IndexPatternProvider } from 'ui/index_patterns/_index_pattern';
import NoDigestPromises from 'test_utils/no_digest_promises';
import { Notifier } from 'ui/notify';
import { FieldsFetcherProvider } from '../fields_fetcher_provider';
import { StubIndexPatternsApiClientModule } from './stub_index_patterns_api_client';
import { IndexPatternsApiClientProvider } from '../index_patterns_api_client_provider';
import { IndexPatternsCalculateIndicesProvider } from '../_calculate_indices';
import { IsUserAwareOfUnsupportedTimePatternProvider } from '../unsupported_time_patterns';
const MARKDOWN_LINK_RE = /\[(.+?)\]\((.+?)\)/;
describe('index pattern', function () {
NoDigestPromises.activateForSuite();
@ -33,6 +37,7 @@ describe('index pattern', function () {
let intervals;
let indexPatternsApiClient;
let defaultTimeField;
let isUserAwareOfUnsupportedTimePattern;
beforeEach(ngMock.module('kibana', StubIndexPatternsApiClientModule, (PrivateProvider) => {
PrivateProvider.swap(IndexPatternsCalculateIndicesProvider, () => {
@ -46,6 +51,11 @@ describe('index pattern', function () {
return calculateIndices;
});
isUserAwareOfUnsupportedTimePattern = sinon.stub().returns(false);
PrivateProvider.swap(IsUserAwareOfUnsupportedTimePatternProvider, () => {
return isUserAwareOfUnsupportedTimePattern;
});
}));
beforeEach(ngMock.inject(function (Private) {
@ -488,4 +498,46 @@ describe('index pattern', function () {
expect(indexPattern.isWildcard()).to.be(false);
});
});
describe('#isUnsupportedTimePattern()', () => {
it('returns true when intervalName is set', () => {
indexPattern.intervalName = 'something';
expect(indexPattern.isUnsupportedTimePattern()).to.be(true);
});
it('returns false otherwise', () => {
delete indexPattern.intervalName;
expect(indexPattern.isUnsupportedTimePattern()).to.be(false);
});
});
describe('unsupported time pattern warning', () => {
async function createUnsupportedTimePattern() {
return await create('pattern-id', {
_source: {
timeFieldName: '@timestamp',
intervalName: 'days',
fields: '[]'
}
});
}
it('logs a warning when the index pattern source includes `intervalName`', async () => {
const indexPattern = await createUnsupportedTimePattern();
expect(Notifier.prototype._notifs).to.have.length(1);
const notif = Notifier.prototype._notifs.shift();
expect(notif).to.have.property('type', 'warning');
expect(notif.content).to.match(MARKDOWN_LINK_RE);
const [,text,url] = notif.content.match(MARKDOWN_LINK_RE);
expect(text).to.contain(indexPattern.id);
expect(url).to.contain(indexPattern.id);
expect(url).to.contain('management/kibana/indices');
});
it('does not notify if isUserAwareOfUnsupportedTimePattern() returns true', async () => {
isUserAwareOfUnsupportedTimePattern.returns(true);
await createUnsupportedTimePattern();
expect(Notifier.prototype._notifs).to.have.length(0);
});
});
});

View file

@ -15,10 +15,6 @@ export function StubIndexPatternsApiClientModule(PrivateProvider) {
class StubIndexPatternsApiClient {
getFieldsForTimePattern = sinon.spy(() => Promise.resolve(nonScriptedFields));
getFieldsForWildcard = sinon.spy(() => Promise.resolve(nonScriptedFields));
testTimePattern = sinon.spy(() => Promise.resolve({
all: [],
matches: []
}))
swapStubNonScriptedFields = (newNonScriptedFields) => {
nonScriptedFields = newNonScriptedFields;

View file

@ -0,0 +1,76 @@
import ngMock from 'ng_mock';
import expect from 'expect.js';
import Chance from 'chance';
import { Storage } from 'ui/storage';
import StubBrowserStorage from 'test_utils/stub_browser_storage';
import StubIndexPatternProvider from 'test_utils/stub_index_pattern';
import { IsUserAwareOfUnsupportedTimePatternProvider } from '../unsupported_time_patterns';
const chance = new Chance();
const CONFIG_KEY = 'indexPatterns:warnAboutUnsupportedTimePatterns';
describe('isUserAwareOfUnsupportedTimePattern()', () => {
let setup;
beforeEach(function () {
setup = () => {
const store = new StubBrowserStorage();
const sessionStorage = new Storage(store);
// stub some core services
ngMock.module('kibana', $provide => {
$provide.value('sessionStorage', sessionStorage);
});
// trigger creation of the injector
ngMock.inject();
const { $injector } = this;
const Private = $injector.get('Private');
const StubIndexPattern = Private(StubIndexPatternProvider);
const isUserAwareOfUnsupportedTimePattern = Private(IsUserAwareOfUnsupportedTimePatternProvider);
const config = $injector.get('config');
config.set(CONFIG_KEY, true); // enable warnings
return {
config,
createIndexPattern: () => new StubIndexPattern(chance.word(), null, []),
isUserAwareOfUnsupportedTimePattern,
};
};
});
it('only warns once per index pattern', () => {
const {
createIndexPattern,
isUserAwareOfUnsupportedTimePattern,
} = setup();
const indexPattern1 = createIndexPattern();
const indexPattern2 = createIndexPattern();
expect(isUserAwareOfUnsupportedTimePattern(indexPattern1)).to.be(false);
expect(isUserAwareOfUnsupportedTimePattern(indexPattern1)).to.be(true);
expect(isUserAwareOfUnsupportedTimePattern(indexPattern2)).to.be(false);
expect(isUserAwareOfUnsupportedTimePattern(indexPattern1)).to.be(true);
expect(isUserAwareOfUnsupportedTimePattern(indexPattern2)).to.be(true);
expect(isUserAwareOfUnsupportedTimePattern(indexPattern1)).to.be(true);
});
describe('ui config', () => {
it('respects setting', () => {
const {
config,
isUserAwareOfUnsupportedTimePattern,
createIndexPattern,
} = setup();
config.set(CONFIG_KEY, false);
expect(isUserAwareOfUnsupportedTimePattern(createIndexPattern())).to.be(true);
config.set(CONFIG_KEY, true);
expect(isUserAwareOfUnsupportedTimePattern(createIndexPattern())).to.be(false);
});
});
});

View file

@ -15,8 +15,19 @@ import { IndexPatternsFlattenHitProvider } from './_flatten_hit';
import { IndexPatternsCalculateIndicesProvider } from './_calculate_indices';
import { IndexPatternsPatternCacheProvider } from './_pattern_cache';
import { FieldsFetcherProvider } from './fields_fetcher_provider';
import { IsUserAwareOfUnsupportedTimePatternProvider } from './unsupported_time_patterns';
export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, confirmModalPromise) {
export function getRoutes() {
return {
edit: '/management/kibana/indices/{{id}}',
addField: '/management/kibana/indices/{{id}}/create-field',
indexedFields: '/management/kibana/indices/{{id}}?_a=(tab:indexedFields)',
scriptedFields: '/management/kibana/indices/{{id}}?_a=(tab:scriptedFields)',
sourceFilters: '/management/kibana/indices/{{id}}?_a=(tab:sourceFilters)'
};
}
export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise, confirmModalPromise, kbnUrl) {
const fieldformats = Private(RegistryFieldFormatsProvider);
const getIds = Private(IndexPatternsGetIdsProvider);
const fieldsFetcher = Private(FieldsFetcherProvider);
@ -27,17 +38,12 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise,
const flattenHit = Private(IndexPatternsFlattenHitProvider);
const calculateIndices = Private(IndexPatternsCalculateIndicesProvider);
const patternCache = Private(IndexPatternsPatternCacheProvider);
const isUserAwareOfUnsupportedTimePattern = Private(IsUserAwareOfUnsupportedTimePatternProvider);
const type = 'index-pattern';
const notify = new Notifier();
const configWatchers = new WeakMap();
const docSources = new WeakMap();
const getRoutes = () => ({
edit: '/management/kibana/indices/{{id}}',
addField: '/management/kibana/indices/{{id}}/create-field',
indexedFields: '/management/kibana/indices/{{id}}?_a=(tab:indexedFields)',
scriptedFields: '/management/kibana/indices/{{id}}?_a=(tab:scriptedFields)',
sourceFilters: '/management/kibana/indices/{{id}}?_a=(tab:sourceFilters)'
});
const mapping = mappingSetup.expandShorthand({
title: 'text',
@ -86,6 +92,18 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise,
// give index pattern all of the values in _source
_.assign(indexPattern, response._source);
if (indexPattern.isUnsupportedTimePattern()) {
if (!isUserAwareOfUnsupportedTimePattern(indexPattern)) {
const warning = (
'Support for time-intervals has been removed. ' +
`View the ["${indexPattern.id}" index pattern in management](` +
kbnUrl.getRouteHref(indexPattern, 'edit') +
') for more information.'
);
notify.warning(warning, { lifetime: Infinity });
}
}
const promise = indexFields(indexPattern);
// any time index pattern in ES is updated, update index pattern object
@ -319,6 +337,10 @@ export function IndexPatternProvider(Private, $http, config, kbnIndex, Promise,
return this.isTimeBased() && !!this.getInterval();
}
isUnsupportedTimePattern() {
return !!this.intervalName;
}
isTimeBasedWildcard() {
return this.isTimeBased() && this.isWildcard();
}

View file

@ -9,12 +9,6 @@ export function createFieldsFetcher(apiClient, config) {
return this.fetchForWildcard(indexPattern.id);
}
testTimePattern(indexPatternId) {
return apiClient.testTimePattern({
pattern: indexPatternId
});
}
fetchForTimePattern(indexPatternId) {
return apiClient.getFieldsForTimePattern({
pattern: indexPatternId,

View file

@ -81,20 +81,6 @@ export function createIndexPatternsApiClient($http, basePath) {
request('GET', url).then(resp => resp.fields)
));
}
testTimePattern(options = {}) {
const {
pattern
} = options;
const url = getUrl(['_test_time_pattern'], {
pattern,
});
return notify.event(`testTimePattern(${pattern})`, () => (
request('GET', url)
));
}
}
return new IndexPatternsApiClient();

View file

@ -0,0 +1,28 @@
import { BoundToConfigObjProvider } from 'ui/bound_to_config_obj';
export function IsUserAwareOfUnsupportedTimePatternProvider(Private, $injector) {
const BoundToConfigObj = Private(BoundToConfigObjProvider);
const sessionStorage = $injector.get('sessionStorage');
const HISTORY_STORAGE_KEY = 'indexPatterns:warnAboutUnsupportedTimePatterns:history';
const FLAGS = new BoundToConfigObj({
enabled: '=indexPatterns:warnAboutUnsupportedTimePatterns'
});
return function isUserAwareOfUnsupportedTimePattern(indexPattern) {
// The user's disabled the notification. They know about it.
if (!FLAGS.enabled) {
return true;
}
// We've already told the user.
const previousIds = sessionStorage.get(HISTORY_STORAGE_KEY) || [];
if (previousIds.includes(indexPattern.id)) {
return true;
}
// Let's store this for later, so we don't tell the user multiple times.
sessionStorage.set(HISTORY_STORAGE_KEY, [...previousIds, indexPattern.id]);
return false;
};
}

View file

@ -1,7 +1,7 @@
import { uiModules } from 'ui/modules';
import angular from 'angular';
function Storage(store) {
export function Storage(store) {
const self = this;
self.store = store;

View file

@ -3,6 +3,5 @@ export default function ({ loadTestFile }) {
loadTestFile(require.resolve('./es_errors'));
loadTestFile(require.resolve('./fields_for_time_pattern_route'));
loadTestFile(require.resolve('./fields_for_wildcard_route'));
loadTestFile(require.resolve('./test_time_pattern_route'));
});
}

View file

@ -1,53 +0,0 @@
export default function ({ getService }) {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
const chance = getService('chance');
describe('index_patterns/_test_time_pattern', () => {
before(() => esArchiver.load('index_patterns/time_based_indices'));
after(() => esArchiver.unload('index_patterns/time_based_indices'));
it('returns all and matching fields for tested pattern', () =>
supertest
.get('/api/index_patterns/_test_time_pattern')
.query({ pattern: '[yearly-]YYYY' })
.expect(200, {
all: [
'yearly-2018',
'yearly-2017',
'yearly-2016',
],
matches: [
'yearly-2018',
'yearly-2017',
'yearly-2016',
]
})
);
it('returns all and matching fields for tested pattern', () =>
supertest
.get('/api/index_patterns/_test_time_pattern')
.query({ pattern: '[monthly-]YYYY.MM' })
.expect(200, {
all: [
'monthly-2017.01',
'monthly-invalid',
],
matches: [
'monthly-2017.01',
]
})
);
it('returns an empty response if it does not match any indexes', () =>
supertest
.get('/api/index_patterns/_test_time_pattern')
.query({ pattern: `[${chance.word({ length: 12 })}]-YYYY` })
.expect(200, {
all: [],
matches: []
})
);
});
}

View file

@ -17,13 +17,6 @@ export default function ({ getService, getPageObjects }) {
});
});
it('should load with name pattern unchecked', function () {
return PageObjects.settings.getTimeBasedIndexPatternCheckbox().isSelected()
.then(function (selected) {
expect(selected).to.not.be.ok();
});
});
it('should contain default index pattern', function () {
const defaultPattern = 'logstash-*';

View file

@ -62,11 +62,6 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await PageObjects.common.navigateToApp('settings');
}
getTimeBasedIndexPatternCheckbox() {
// fail faster since we're sometimes checking that it doesn't exist
return testSubjects.find('createIndexPatternNameIsPatternCheckBox');
}
getIndexPatternField() {
return testSubjects.find('createIndexPatternNameInput');
}