"Create index pattern" wizard. (#13454)

* Create Index Pattern Creation wizard.
- Create a directive for each step in the wizard.
- Reorganize files into conventional folder structure.
- Rename files with more conventional and consistent naming patterns.
- Display indices, partial matches, exact matches.
- Add loading, empty, and success states.
- Add option to include system indices.

* Temp: comment out and mock getIndices functionality.

* Hook up data seaching

* Add for/id connections for form inputs and labels

* Automatically append a wildcard

* Highlight the index pattern in the results

* Ensure we only remove the last character if it's a `*`

* Auto hide index pattern id controls

* Ensure this link is tabbable

* Move the advanced fields down

* Use toggle button

* This shouldn't ever be required

* Revert "This shouldn't ever be required"

This reverts commit b6e31e79308271e7f04ea1d42ce66e32e7aa0612.

* Update based on comments in PR

* Ffew more changes

* Port changes from Tyler's PR, https://github.com/elastic/kibana/pull/13353

* Remove unnecessary file

* Fix broken functional tests

* Copy changes

* Fix functional tests

* Remove loading from the main select, and move to an additional select

* Show help text when loading

* Fix sorting

* Fixing broken functional tests

* Couple changes from PR review

* Ensure input field does not show a red border until touched

* More descriptive and consistent copy
This commit is contained in:
Chris Roberson 2017-08-24 09:04:28 -04:00 committed by GitHub
parent 4bbd127626
commit 9175137c57
40 changed files with 1259 additions and 660 deletions

View file

@ -1,124 +0,0 @@
import angular from 'angular';
import ngMock from 'ng_mock';
import jQuery from 'jquery';
import expect from 'expect.js';
import sinon from 'sinon';
import createIndexPatternTemplate from '../create_index_pattern.html';
import { StubIndexPatternsApiClientModule } from 'ui/index_patterns/__tests__/stub_index_patterns_api_client';
import { IndexPatternsApiClientProvider } from 'ui/index_patterns';
import MockLogstashFieldsProvider from 'fixtures/logstash_fields';
describe('createIndexPattern UI', () => {
let setup;
const trash = [];
beforeEach(ngMock.module('kibana', StubIndexPatternsApiClientModule, ($provide) => {
$provide.constant('buildSha', 'abc1234');
$provide.constant('$route', {
current: {
params: {},
locals: {
indexPatterns: []
}
}
});
}));
beforeEach(ngMock.inject(($injector) => {
setup = function () {
const Private = $injector.get('Private');
const $compile = $injector.get('$compile');
const $rootScope = $injector.get('$rootScope');
const fields = Private(MockLogstashFieldsProvider);
const indexPatternsApiClient = Private(IndexPatternsApiClientProvider);
const $scope = $rootScope.$new();
const $view = jQuery($compile(angular.element('<div>').html(createIndexPatternTemplate))($scope));
trash.push(() => $scope.$destroy());
$scope.$apply();
const setNameTo = (name) => {
$view.findTestSubject('createIndexPatternNameInput')
.val(name)
.change()
.blur();
// ensure that name successfully applied
const form = $view.find('form').scope().form;
expect(form.name).to.have.property('$viewValue', name);
};
return {
$view,
$scope,
setNameTo,
indexPatternsApiClient,
fields
};
};
}));
afterEach(() => {
trash.forEach(fn => fn());
trash.length = 0;
});
describe('defaults', () => {
it('renders `logstash-*` into the name input', () => {
const { $view } = setup();
const $name = $view.findTestSubject('createIndexPatternNameInput');
expect($name).to.have.length(1);
expect($name.val()).to.be('logstash-*');
});
it('attempts to getFieldsForWildcard for `logstash-*`', () => {
const { indexPatternsApiClient } = setup();
const { getFieldsForWildcard } = indexPatternsApiClient;
sinon.assert.called(getFieldsForWildcard);
const calledWithPattern = getFieldsForWildcard.getCalls().some(call => {
const [params] = call.args;
return (
params &&
params.pattern &&
params.pattern === 'logstash-*'
);
});
if (!calledWithPattern) {
throw new Error('expected indexPatternsApiClient.getFieldsForWildcard to be called with pattern = logstash-*');
}
});
it('loads the time fields into the select box', () => {
const { $view, fields } = setup();
const timeFieldOptions = $view.findTestSubject('createIndexPatternTimeFieldSelect')
.find('option')
.toArray()
.map(option => option.innerText);
fields.forEach((field) => {
if (!field.scripted && field.type === 'date') {
expect(timeFieldOptions).to.contain(field.name);
} else {
expect(timeFieldOptions).to.not.contain(field.name);
}
});
});
});
describe('cross cluster pattern', () => {
it('name input accepts `cluster2:logstash-*` pattern', () => {
const { $view, setNameTo } = setup();
setNameTo('cluster2:logstash-*');
const $name = $view.findTestSubject('createIndexPatternNameInput');
const classes = [...$name.get(0).classList];
expect(classes).to.contain('ng-valid');
expect(classes).to.not.contain('ng-invalid');
});
});
});

View file

@ -1,144 +0,0 @@
<kbn-management-app section="kibana">
<kbn-management-indices>
<div
ng-controller="managementIndicesCreate as controller"
data-test-subj="createIndexPatternContainer"
class="kuiViewContent"
>
<h1 class="kuiTitle kuiVerticalRhythm">
Configure an index pattern
</h1>
<p class="kuiText kuiVerticalRhythm">
In order to use Kibana you must configure at least one index pattern.
Index patterns are used to identify the Elasticsearch index to run
search and analytics against. They are also used to configure fields.
</p>
<div class="kuiVerticalRhythm">
<!-- Form -->
<form
name="form"
role="form"
class="kuiVerticalRhythm"
ng-submit="controller.createIndexPattern()"
>
<!-- Index pattern input -->
<div class="kuiVerticalRhythm">
<label class="kuiLabel kuiVerticalRhythmSmall">
<span>Index pattern</span>
<small>
<a
class="kuiLink"
ng-click="controller.toggleAdvancedIndexOptions();"
>
advanced options
</a>
</small>
</label>
<div class="kuiVerticalRhythm kuiVerticalRhythmSmall">
<input
class="kuiTextInput kuiTextInput--large"
data-test-subj="createIndexPatternNameInput"
ng-model="controller.formValues.name"
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"
validate-index-name
allow-wildcard
name="name"
required
type="text"
>
</div>
<!-- Input error text -->
<div
class="kuiVerticalRhythm"
ng-if="controller.timeFieldOptionsError"
>
<p class="kuiText">
<span class="kuiStatusText kuiStatusText--error">
<span class="kuiStatusText__icon kuiIcon fa-warning"></span>
{{controller.timeFieldOptionsError}}
</span>
</p>
</div>
<!-- Input help text -->
<div class="kuiVerticalRhythm">
<p class="kuiSubText kuiVerticalRhythmSmall">
Patterns allow you to define dynamic index names using * as a wildcard. Example: logstash-*
</p>
</div>
</div>
<!-- Index pattern id input -->
<div class="kuiVerticalRhythm" ng-if="controller.showAdvancedOptions">
<label class="kuiLabel kuiVerticalRhythmSmall">
Index pattern ID
</label>
<div class="kuiVerticalRhythm kuiVerticalRhythmSmall">
<input
class="kuiTextInput kuiTextInput--large"
data-test-subj="createIndexPatternIdInput"
ng-model="controller.formValues.id"
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"
validate-index-name
allow-wildcard
name="id"
type="text"
>
</div>
<!-- ID help text -->
<div class="kuiVerticalRhythm">
<p class="kuiSubText kuiVerticalRhythmSmall">
Creates the index pattern with the specified ID.
</p>
</div>
</div>
<!-- Time field select -->
<div class="kuiVerticalRhythm">
<label class="kuiLabel kuiVerticalRhythmSmall">
<span>Time Filter field name</span>
&nbsp;
<kbn-info info="This field will be used to filter events with the global time filter"></kbn-info>
&nbsp;
<small>
<a
class="kuiLink"
ng-click="controller.refreshTimeFieldOptions();"
>refresh fields</a>
</small>
</label>
<div class="kuiVerticalRhythmSmall">
<select
class="kuiSelect kuiSelect--large kuiVerticalRhythmSmall"
data-test-subj="createIndexPatternTimeFieldSelect"
ng-disabled="controller.isLoading() || controller.timeFieldOptionsError || controller.timeFieldOptions.length === 1"
ng-required="controller.timeFieldOptions.length"
ng-options="option.display for option in controller.timeFieldOptions"
ng-model="controller.formValues.timeFieldOption"
></select>
</div>
</div>
<!-- Form actions -->
<button
data-test-subj="createIndexPatternCreateButton"
ng-disabled="form.$invalid || controller.timeFieldOptionsError || controller.isLoading()"
class="kuiButton kuiButton--primary kuiVerticalRhythm"
type="submit"
>
{{controller.createButtonText}}
</button>
</form>
</div>
</div>
</kbn-management-indices>
</kbn-management-app>

View file

@ -1,252 +0,0 @@
import { IndexPatternMissingIndices } from 'ui/errors';
import 'ui/directives/validate_index_name';
import 'ui/directives/auto_select_if_only_one';
import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import template from './create_index_pattern.html';
import { sendCreateIndexPatternRequest } from './send_create_index_pattern_request';
import { pickCreateButtonText } from './pick_create_button_text';
uiRoutes
.when('/management/kibana/index', {
template,
});
uiModules.get('apps/management')
.controller('managementIndicesCreate', function (
$scope,
$routeParams,
kbnUrl,
Private,
Notifier,
indexPatterns,
es,
config,
Promise,
$translate
) {
const notify = new Notifier();
let loadingCount = 0;
// Configure the new index pattern we're going to create.
this.formValues = {
name: config.get('indexPattern:placeholder'),
timeFieldOption: null,
};
// UI state.
this.timeFieldOptions = [];
this.timeFieldOptionsError = null;
this.showAdvancedOptions = false;
// fills index-pattern ID based on query param.
if ($routeParams.id) {
this.formValues.id = decodeURIComponent($routeParams.id);
this.formValues.name = '';
this.showAdvancedOptions = true;
}
const getTimeFieldOptions = () => {
loadingCount += 1;
return Promise.resolve()
.then(() => {
const { name } = this.formValues;
if (!name) {
return [];
}
return indexPatterns.fieldsFetcher.fetchForWildcard(name);
})
.then(fields => {
const dateFields = fields.filter(field => field.type === 'date');
if (dateFields.length === 0) {
return {
options: [
{
display: `The indices which match this index pattern don't contain any time fields.`
}
]
};
}
return {
options: [
{
display: `I don't want to use the Time Filter`
},
...dateFields.map(field => ({
display: field.name,
fieldName: field.name
})),
]
};
})
.catch(err => {
if (err instanceof IndexPatternMissingIndices) {
return {
error: 'Unable to fetch mapping. Do you have indices matching the pattern?'
};
}
throw err;
})
.finally(() => {
loadingCount -= 1;
});
};
const findTimeFieldOption = match => {
if (!match) return;
return this.timeFieldOptions.find(option => (
// comparison is not done with _.isEqual() because options get a unique
// `$$hashKey` tag attached to them by ng-repeat
option.fieldName === match.fieldName &&
option.display === match.display
));
};
const pickDefaultTimeFieldOption = () => {
const noOptions = this.timeFieldOptions.length === 0;
// options that represent a time field
const fieldOptions = this.timeFieldOptions.filter(option => !!option.fieldName);
// options like "I don't want the time filter" or "There are no date fields"
const nonFieldOptions = this.timeFieldOptions.filter(option => !option.fieldName);
// if there are multiple field or non-field options then we can't select a default, the user must choose
const tooManyOptions = fieldOptions.length > 1 || nonFieldOptions.length > 1;
if (noOptions || tooManyOptions) {
return null;
}
if (fieldOptions.length === 1) {
return fieldOptions[0];
}
return nonFieldOptions[0];
};
this.isTimeBased = () => {
if (!this.formValues.timeFieldOption) {
// if they haven't choosen a time field, assume they will
return true;
}
// if timeFieldOption has a fieldName it's a time field, otherwise
// it's a way to opt-out of the time field or an indication that there
// are no fields available
return Boolean(this.formValues.timeFieldOption.fieldName);
};
this.isCrossClusterName = () => {
return (
this.formValues.name &&
this.formValues.name.includes(':')
);
};
this.isLoading = () => {
return loadingCount > 0;
};
let activeRefreshTimeFieldOptionsCall;
this.refreshTimeFieldOptions = () => {
// if there is an active refreshTimeFieldOptions() call then we use
// their prevOption, allowing the previous selection to persist
// across simultaneous calls to refreshTimeFieldOptions()
const prevOption = activeRefreshTimeFieldOptionsCall
? activeRefreshTimeFieldOptionsCall.prevOption
: this.formValues.timeFieldOption;
// `thisCall` is our unique "token" to verify that we are still the
// most recent call. When we are not the most recent call we don't
// modify the controller in any way to prevent race conditions
const thisCall = activeRefreshTimeFieldOptionsCall = { prevOption };
loadingCount += 1;
this.timeFieldOptions = [];
this.timeFieldOptionsError = null;
this.formValues.timeFieldOption = null;
getTimeFieldOptions()
.then(({ options, error }) => {
if (thisCall !== activeRefreshTimeFieldOptionsCall) return;
this.timeFieldOptions = options;
this.timeFieldOptionsError = error;
if (!this.timeFieldOptions) {
return;
}
// Restore the preivously selected state, or select the default option in the UI
const restoredOption = findTimeFieldOption(prevOption);
const defaultOption = pickDefaultTimeFieldOption();
this.formValues.timeFieldOption = restoredOption || defaultOption;
})
.catch(notify.error)
.finally(() => {
loadingCount -= 1;
if (thisCall === activeRefreshTimeFieldOptionsCall) {
activeRefreshTimeFieldOptionsCall = null;
}
});
};
this.toggleAdvancedIndexOptions = () => {
this.showAdvancedOptions = !!!this.showAdvancedOptions;
};
this.createIndexPattern = () => {
const {
id,
name,
timeFieldOption,
} = this.formValues;
const timeFieldName = timeFieldOption
? timeFieldOption.fieldName
: undefined;
loadingCount += 1;
sendCreateIndexPatternRequest(indexPatterns, {
id,
name,
timeFieldName,
}).then(createdId => {
if (!createdId) {
return;
}
if (!config.get('defaultIndex')) {
config.set('defaultIndex', createdId);
}
indexPatterns.cache.clear(createdId);
kbnUrl.change(`/management/kibana/indices/${createdId}`);
// force loading while kbnUrl.change takes effect
loadingCount = Infinity;
}).catch(err => {
if (err instanceof IndexPatternMissingIndices) {
return notify.error('Could not locate any indices matching that pattern. Please add the index to Elasticsearch');
}
notify.fatal(err);
}).finally(() => {
loadingCount -= 1;
});
};
$scope.$watch('controller.formValues.name', () => {
this.refreshTimeFieldOptions();
});
$scope.$watchMulti([
'controller.isLoading()',
'form.name.$error.indexNameInput',
'controller.formValues.timeFieldOption'
], ([loading, invalidIndexName, timeFieldOption]) => {
const state = { loading, invalidIndexName, timeFieldOption };
this.createButtonText = pickCreateButtonText($translate, state);
});
});

View file

@ -1,17 +0,0 @@
const intervalToDefaultPatternMap = {
hours: '[logstash-]YYYY.MM.DD.HH',
days: '[logstash-]YYYY.MM.DD',
weeks: '[logstash-]GGGG.WW',
months: '[logstash-]YYYY.MM',
years: '[logstash-]YYYY',
};
export function getDefaultPatternForInterval(interval) {
const defaultPattern = intervalToDefaultPatternMap[interval];
if (defaultPattern) {
return defaultPattern;
}
return 'logstash-*';
}

View file

@ -1 +0,0 @@
import './create_index_pattern';

View file

@ -1,21 +0,0 @@
export function pickCreateButtonText($translate, state) {
const {
loading,
invalidIndexName,
timeFieldOption
} = state;
if (loading) {
return 'Loading';
}
if (invalidIndexName) {
return 'Invalid index name pattern.';
}
if (!timeFieldOption) {
return 'Time Filter field name is required';
}
return 'Create';
}

View file

@ -0,0 +1,154 @@
<kbn-management-app section="kibana">
<kbn-management-indices>
<div
ng-controller="managementIndicesCreate as controller"
data-test-subj="createIndexPatternContainer"
class="kuiViewContent"
>
<!-- Intro -->
<div class="kuiVerticalRhythm">
<div class="kuiBar">
<h1 class="kuiTitle">
Create index pattern
</h1>
<div class="kuiBarSection">
<!-- Include system indices -->
<label
ng-if="controller.isSystemIndicesCheckBoxVisible()"
class="kuiCheckBoxLabel"
for="indexPatternCreationIncludeSystemIndices"
>
<input
type="checkbox"
id="indexPatternCreationIncludeSystemIndices"
class="kuiCheckBox"
ng-model="controller.doesIncludeSystemIndices"
ng-change="controller.onIncludeSystemIndicesChange()"
>
<span class="kuiCheckBoxLabel__text">
Include system indices
</span>
</label>
</div>
</div>
<p class="kuiText kuiSubduedText">
Kibana uses index patterns to retrieve data from Elasticsearch indices for things like visualizations.
</p>
</div>
<!-- User has no data -->
<div
class="kuiPanel kuiVerticalRhythm"
ng-if="!controller.hasIndices()"
>
<div class="kuiPanelBody">
<!-- Fetching state -->
<div
ng-if="controller.isFetchingExistingIndices"
class="kuiPanel kuiPanel--prompt kuiPanel--noBorder kuiVerticalRhythm"
>
<div class="kuiPanelBody">
<h2 class="kuiSubTitle kuiSubduedText kuiVerticalRhythm">
Checking for Elasticsearch data
</h2>
<div class="kuiVerticalRhythm">
<p class="kuiText kuiSubduedText kuiVerticalRhythmSmall">
<span class="kuiStatusText">
<span
aria-hidden="true"
class="kuiStatusText__icon kuiIcon fa-spinner fa-spin"
></span>
<span>
Reticulating splines...
</span>
</span>
</p>
</div>
</div>
</div>
<!-- Empty state -->
<div ng-if="!controller.isFetchingExistingIndices">
<div class="kuiPanel kuiPanel--prompt kuiPanel--noBorder kuiVerticalRhythm">
<div class="kuiPanelBody">
<h1 class="kuiSubTitle kuiSubduedText kuiVerticalRhythm">
Couldn't find any Elasticsearch data
</h1>
<div class="kuiVerticalRhythm">
<p class="kuiText kuiSubduedText kuiVerticalRhythmSmall">
<span>
You'll need to index some data into Elasticsearch before you can create an index pattern.
</span>
<a
class="kuiLink"
aria-label="Learn how to index data into Elasticsearch"
ng-href="{{ controller.documentationLinks.indexPatterns.loadingData }}"
target="_blank"
>
Learn how.
</a>
</p>
</div>
<button
class="kuiButton kuiButton--secondary kuiButton--iconText kuiVerticalRhythm"
ng-click="controller.fetchExistingIndices()"
>
<span class="kuiButton__inner">
<span
aria-hidden="true"
class="kuiButton__icon kuiIcon fa-refresh"
></span>
<span>
Check for new data
</span>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- User has data -->
<div
class="kuiVerticalRhythm"
ng-if="controller.hasIndices()"
ng-switch="controller.wizardStep"
>
<!-- Specify index pattern -->
<step-index-pattern
ng-switch-when="indexPattern"
fetch-existing-indices="controller.fetchExistingIndices()"
is-fetching-existing-indices="controller.isFetchingExistingIndices"
fetch-matching-indices="controller.fetchMatchingIndices()"
is-fetching-matching-indices="controller.isFetchingMatchingIndices"
has-indices="controller.hasIndices()"
index-pattern-name="controller.formValues.name"
all-indices="controller.allIndices"
partial-matching-indices="controller.partialMatchingIndices"
matching-indices="controller.matchingIndices"
go-to-next-step="controller.goToTimeFieldStep()"
></step-index-pattern>
<!-- Specify optional time field -->
<step-time-field
ng-switch-when="timeField"
index-pattern-id="controller.formValues.id"
index-pattern-name="controller.formValues.name"
time-field-options="controller.timeFieldOptions"
selected-time-field-option="controller.formValues.timeFieldOption"
fetch-time-field-options="controller.fetchTimeFieldOptions()"
is-fetching-time-field-options="controller.isFetchingTimeFieldOptions"
go-to-previous-step="controller.goToIndexPatternStep()"
create-index-pattern="controller.createIndexPattern()"
></step-time-field>
</div>
</div>
</kbn-management-indices>
</kbn-management-app>

View file

@ -0,0 +1,282 @@
import _ from 'lodash';
import { IndexPatternMissingIndices } from 'ui/errors';
import 'ui/directives/validate_index_pattern';
import 'ui/directives/auto_select_if_only_one';
import { documentationLinks } from 'ui/documentation_links/documentation_links';
import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';
import template from './create_index_pattern_wizard.html';
import { sendCreateIndexPatternRequest } from './send_create_index_pattern_request';
import './step_index_pattern';
import './step_time_field';
import './matching_indices_list';
uiRoutes
.when('/management/kibana/index', {
template,
});
uiModules.get('apps/management')
.controller('managementIndicesCreate', function (
$routeParams,
$scope,
$timeout,
config,
es,
indexPatterns,
kbnUrl,
Notifier,
Promise
) {
const MAX_NUMBER_OF_MATCHING_INDICES = 20;
const notify = new Notifier();
const disabledDividerOption = {
isDisabled: true,
display: '───',
};
const noTimeFieldOption = {
display: `I don't want to use the Time Filter`,
};
this.documentationLinks = documentationLinks;
// Configure the new index pattern we're going to create.
this.formValues = {
id: $routeParams.id ? decodeURIComponent($routeParams.id) : undefined,
name: '',
expandWildcard: false,
timeFieldOption: undefined,
};
// UI state.
this.timeFieldOptions = [];
this.wizardStep = 'indexPattern';
this.isFetchingExistingIndices = true;
this.isFetchingMatchingIndices = false;
this.isFetchingTimeFieldOptions = false;
this.isCreatingIndexPattern = false;
this.doesIncludeSystemIndices = false;
let allIndices = [];
let matchingIndices = [];
let partialMatchingIndices = [];
this.allIndices = [];
this.matchingIndices = [];
this.partialMatchingIndices = [];
function createReasonableWait() {
return new Promise(resolve => {
// Make every fetch take a set amount of time so the user gets some feedback that something
// is happening.
$timeout(() => {
resolve();
}, 500);
});
}
function getIndices(pattern, limit = MAX_NUMBER_OF_MATCHING_INDICES) {
const params = {
index: pattern,
ignore: [404],
body: {
size: 0, // no hits
aggs: {
indices: {
terms: {
field: '_index',
size: limit,
}
}
}
}
};
return es.search(params)
.then(response => {
if (!response || response.error || !response.aggregations) {
return [];
}
return _.sortBy(response.aggregations.indices.buckets.map(bucket => {
return {
name: bucket.key
};
}), 'name');
});
}
const whiteListIndices = indices => {
if (!indices) {
return indices;
}
if (this.doesIncludeSystemIndices) {
return indices;
}
// All system indices begin with a period.
return indices.filter(index => !index.name.startsWith('.'));
};
const updateWhiteListedIndices = () => {
this.allIndices = whiteListIndices(allIndices);
this.matchingIndices = whiteListIndices(matchingIndices);
this.partialMatchingIndices = whiteListIndices(partialMatchingIndices);
};
this.onIncludeSystemIndicesChange = () => {
updateWhiteListedIndices();
};
let mostRecentFetchMatchingIndicesRequest;
this.fetchMatchingIndices = () => {
this.isFetchingMatchingIndices = true;
// Default to searching for all indices.
const exactSearchQuery = this.formValues.name;
let partialSearchQuery = this.formValues.name;
if (!_.endsWith(partialSearchQuery, '*')) {
partialSearchQuery = `${partialSearchQuery}*`;
}
if (!_.startsWith(partialSearchQuery, '*')) {
partialSearchQuery = `*${partialSearchQuery}`;
}
const thisFetchMatchingIndicesRequest = mostRecentFetchMatchingIndicesRequest = Promise.all([
getIndices(exactSearchQuery),
getIndices(partialSearchQuery),
createReasonableWait()
])
.then(([
matchingIndicesResponse,
partialMatchingIndicesResponse
]) => {
if (thisFetchMatchingIndicesRequest === mostRecentFetchMatchingIndicesRequest) {
matchingIndices = matchingIndicesResponse;
partialMatchingIndices = partialMatchingIndicesResponse;
updateWhiteListedIndices();
this.isFetchingMatchingIndices = false;
}
}).catch(error => {
notify.error(error);
});
};
this.fetchExistingIndices = () => {
this.isFetchingExistingIndices = true;
const allExistingLocalAndRemoteIndicesPattern = '*,*:*';
Promise.all([
getIndices(allExistingLocalAndRemoteIndicesPattern),
createReasonableWait()
])
.then(([allIndicesResponse]) => {
// Cache all indices.
allIndices = allIndicesResponse;
updateWhiteListedIndices();
this.isFetchingExistingIndices = false;
}).catch(error => {
notify.error(error);
});
};
this.isSystemIndicesCheckBoxVisible = () => (
this.wizardStep === 'indexPattern'
);
this.goToIndexPatternStep = () => {
this.wizardStep = 'indexPattern';
};
this.goToTimeFieldStep = () => {
// Re-initialize this step.
this.formValues.timeFieldOption = undefined;
this.fetchTimeFieldOptions();
this.wizardStep = 'timeField';
};
this.hasIndices = () => (
this.allIndices.length
);
const extractTimeFieldsFromFields = fields => {
const dateFields = fields.filter(field => field.type === 'date');
if (dateFields.length === 0) {
return [{
display: `The indices which match this index pattern don't contain any time fields.`,
}];
}
return [
...dateFields.map(field => ({
display: field.name,
fieldName: field.name
})),
disabledDividerOption,
noTimeFieldOption,
];
};
this.fetchTimeFieldOptions = () => {
this.isFetchingTimeFieldOptions = true;
this.formValues.timeFieldOption = undefined;
this.timeFieldOptions = [];
Promise.all([
indexPatterns.fieldsFetcher.fetchForWildcard(this.formValues.name),
createReasonableWait(),
])
.then(([fields]) => {
this.timeFieldOptions = extractTimeFieldsFromFields(fields);
})
.catch(error => {
notify.error(error);
})
.finally(() => {
this.isFetchingTimeFieldOptions = false;
});
};
this.createIndexPattern = () => {
this.isCreatingIndexPattern = true;
const {
id,
name,
timeFieldOption,
} = this.formValues;
const timeFieldName = timeFieldOption
? timeFieldOption.fieldName
: undefined;
sendCreateIndexPatternRequest(indexPatterns, {
id,
name,
timeFieldName,
}).then(createdId => {
if (!createdId) {
return;
}
if (!config.get('defaultIndex')) {
config.set('defaultIndex', createdId);
}
indexPatterns.cache.clear(createdId);
kbnUrl.change(`/management/kibana/indices/${createdId}`);
}).catch(err => {
if (err instanceof IndexPatternMissingIndices) {
return notify.error(`Couldn't locate any indices matching that pattern. Please add the index to Elasticsearch`);
}
notify.fatal(err);
}).finally(() => {
this.isCreatingIndexPattern = false;
});
};
this.fetchExistingIndices();
});

View file

@ -0,0 +1 @@
import './create_index_pattern_wizard';

View file

@ -0,0 +1,61 @@
<div>
<div
ng-if="matchingIndicesList.isLoading"
class="kuiPanel kuiPanel--prompt kuiVerticalRhythm matchingIndicesListLoadingPrompt"
>
<div class="kuiPanelBody">
<p class="kuiSubTitle kuiSubduedText kuiVerticalRhythm">
Looking for matching indices
</p>
<div class="kuiVerticalRhythm">
<p class="kuiText kuiSubduedText kuiVerticalRhythmSmall">
Just a sec...
</p>
</div>
</div>
</div>
<div
ng-if="!matchingIndicesList.isLoading"
class="kuiControlledTable"
>
<!-- ToolBar -->
<div class="kuiToolBar">
<div class="kuiToolBarSection">
<p class="kuiText">
<ng-transclude></ng-transclude>
</p>
</div>
<div class="kuiToolBarSection">
<!-- Pagination -->
<tool-bar-pager-text
start-item="matchingIndicesList.pager.startItem"
end-item="matchingIndicesList.pager.endItem"
total-items="matchingIndicesList.pager.totalItems"
ng-if="matchingIndicesList.hasMultiplePages()"
></tool-bar-pager-text>
<tool-bar-pager-buttons
has-previous-page="matchingIndicesList.pager.hasPreviousPage"
has-next-page="matchingIndicesList.pager.hasNextPage"
on-page-next="matchingIndicesList.onPageNext"
on-page-previous="matchingIndicesList.onPagePrevious"
ng-if="matchingIndicesList.hasMultiplePages()"
></tool-bar-pager-buttons>
</div>
</div>
<!-- Indices list -->
<ul class="kuiMenu kuiMenu--contained">
<li
class="kuiMenuItem"
ng-repeat="index in matchingIndicesList.pageOfIndices"
>
<p class="kuiText" ng-bind-html="index.name | highlight:matchingIndicesList.formattedPattern">
</p>
</li>
</ul>
</div>
</div>

View file

@ -0,0 +1,62 @@
import 'ui/pager_control';
import 'ui/pager';
import { last } from 'lodash';
import { uiModules } from 'ui/modules';
import './matching_indices_list.less';
import template from './matching_indices_list.html';
const module = uiModules.get('apps/management');
module.directive('matchingIndicesList', function ($filter, pagerFactory) {
return {
restrict: 'E',
replace: true,
template,
transclude: true,
controllerAs: 'matchingIndicesList',
bindToController: true,
scope: {
indices: '=',
pattern: '=',
isLoading: '=',
},
link: function (scope) {
scope.$watch('matchingIndicesList.indices', () => {
scope.matchingIndicesList.calculateItemsOnPage();
});
scope.$watch('matchingIndicesList.pattern', () => {
if (last(scope.matchingIndicesList.pattern) === '*') {
const end = scope.matchingIndicesList.pattern.length - 1;
scope.matchingIndicesList.formattedPattern = scope.matchingIndicesList.pattern.substring(0, end);
} else {
scope.matchingIndicesList.formattedPattern = scope.matchingIndicesList.pattern;
}
});
},
controller: function () {
this.pageOfIndices = [];
this.calculateItemsOnPage = () => {
const limitTo = $filter('limitTo');
this.pager.setTotalItems(this.indices.length);
this.pageOfIndices = limitTo(this.indices, this.pager.pageSize, this.pager.startIndex);
};
this.pager = pagerFactory.create(this.indices.length, 10, 1);
this.hasMultiplePages = () => {
return this.indices.length > this.pager.pageSize;
};
this.onPageNext = () => {
this.pager.nextPage();
this.calculateItemsOnPage();
};
this.onPagePrevious = () => {
this.pager.previousPage();
this.calculateItemsOnPage();
};
},
};
});

View file

@ -0,0 +1,3 @@
.matchingIndicesListLoadingPrompt {
min-height: 60px;
}

View file

@ -0,0 +1,168 @@
<div class="kuiPanel">
<div class="kuiPanelHeader">
<div class="kuiPanelHeaderSection">
<h2 class="kuiPanelHeader__title">
Step 1 of 2: Define index pattern
</h2>
</div>
</div>
<div class="kuiPanelBody">
<div>
<form
name="stepIndexPattern.indexPatternNameForm"
role="form"
ng-submit="stepIndexPattern.goToNextStep()"
class="kuiVerticalRhythm"
>
<div class="createIndexPatternInputContainer kuiVerticalRhythm">
<!-- Index pattern input -->
<div>
<label
for="indexPatternNameField"
class="kuiLabel kuiVerticalRhythmSmall"
>
Index pattern
</label>
<div class="kuiVerticalRhythmSmall kuiVerticalRhythm">
<input
id="indexPatternNameField"
class="kuiTextInput kuiTextInput--large createIndexPatternInputField"
data-test-subj="createIndexPatternNameInput"
ng-model="stepIndexPattern.indexPatternName"
placeholder="index-name-*"
validate-index-pattern
validate-index-pattern-allow-wildcard
name="name"
required
type="text"
aria-describedby="indexPatternNameFieldHelp1 indexPatternNameFieldHelp2"
>
</div>
<p
id="indexPatternNameFieldHelp1"
class="kuiText kuiSubduedText kuiVerticalRhythm"
>
You can use a <strong>*</strong> as a wildcard in your index pattern.
</p>
<p
id="indexPatternNameFieldHelp2"
class="kuiText kuiSubduedText"
>
You can't use empty spaces or the characters <strong>\ / ? " < > , |</strong>.
</p>
</div>
<!-- Action -->
<button
data-test-subj="createIndexPatternGoToStep2Button"
class="kuiButton kuiButton--primary kuiButton--iconText kuiVerticalRhythmSmall"
ng-click="stepIndexPattern.goToNextStep()"
ng-disabled="!stepIndexPattern.canGoToNextStep()"
>
<span class="kuiButton__inner">
<span>
Next step
</span>
<span
aria-hidden="true"
class="kuiButton__icon kuiIcon fa-chevron-right"
></span>
</span>
</button>
</div>
</form>
<!-- List of matching indices -->
<div
class="kuiVerticalRhythm"
ng-switch="stepIndexPattern.matchingIndicesListType"
>
<div ng-switch-when="invalidIndexPattern">
<matching-indices-list
is-loading="stepIndexPattern.isFetchingMatchingIndices"
indices="stepIndexPattern.allIndices"
pattern="stepIndexPattern.indexPatternName"
>
<span class="kuiStatusText kuiStatusText--error">
<span
aria-hidden="true"
class="kuiStatusText__icon kuiIcon fa-hand-stop-o"
></span>
<span>
You've entered an invalid index pattern. Please adjust it to match any of your <strong>{{stepIndexPattern.allIndices.length}} indices</strong>, below.
</span>
</span>
</matching-indices-list>
</div><div ng-switch-when="noInput">
<matching-indices-list
is-loading="stepIndexPattern.isFetchingMatchingIndices"
indices="stepIndexPattern.allIndices"
pattern="stepIndexPattern.indexPatternName"
>
<span class="kuiStatusText kuiStatusText--info">
<span ng-if="!stepIndexPattern.allIndices.length">
You only have a single index. You can create an index pattern to match it.
</span>
<span ng-if="stepIndexPattern.allIndices.length">
Your index pattern can match any of your <strong>{{stepIndexPattern.allIndices.length}} indices</strong>, below.
</span>
</span>
</matching-indices-list>
</div>
<div ng-switch-when="noMatches">
<matching-indices-list
is-loading="stepIndexPattern.isFetchingMatchingIndices"
indices="stepIndexPattern.allIndices"
pattern="stepIndexPattern.indexPatternName"
>
<span class="kuiStatusText kuiStatusText--info">
<span>
The index pattern you've entered doesn't match any indices. You can match any of your <strong>{{stepIndexPattern.allIndices.length}} indices</strong>, below.
</span>
</span>
</matching-indices-list>
</div>
<div ng-switch-when="partialMatches">
<matching-indices-list
is-loading="stepIndexPattern.isFetchingMatchingIndices"
indices="stepIndexPattern.partialMatchingIndices"
pattern="stepIndexPattern.indexPatternName"
>
<span class="kuiStatusText kuiStatusText--info">
<span>
Your index pattern doesn't match any indices, but you have <strong>{{stepIndexPattern.partialMatchingIndices.length}} {{stepIndexPattern.partialMatchingIndices.length > 1 ? 'indices' : 'index'}}</strong> which {{stepIndexPattern.partialMatchingIndices.length > 1 ? 'look' : 'looks'}} similar.
</span>
</span>
</matching-indices-list>
</div>
<div ng-switch-when="exactMatches">
<matching-indices-list
is-loading="stepIndexPattern.isFetchingMatchingIndices"
indices="stepIndexPattern.matchingIndices"
pattern="stepIndexPattern.indexPatternName"
>
<span class="kuiStatusText kuiStatusText--success">
<span
aria-hidden="true"
class="kuiStatusText__icon kuiIcon fa-check"
></span>
<span>
<strong>Success!</strong> Your index pattern matches <strong>{{stepIndexPattern.matchingIndices.length}} {{stepIndexPattern.matchingIndices.length > 1 ? 'indices' : 'index'}}</strong>.
</span>
</span>
</matching-indices-list>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,110 @@
import { uiModules } from 'ui/modules';
import './step_index_pattern.less';
import template from './step_index_pattern.html';
import { documentationLinks } from 'ui/documentation_links/documentation_links';
const module = uiModules.get('apps/management');
module.directive('stepIndexPattern', function () {
return {
restrict: 'E',
template,
replace: true,
controllerAs: 'stepIndexPattern',
bindToController: true,
scope: {
fetchExistingIndices: '&',
isFetchingExistingIndices: '=',
fetchMatchingIndices: '&',
isFetchingMatchingIndices: '=',
hasIndices: '&',
indexPatternName: '=',
allIndices: '=',
partialMatchingIndices: '=',
matchingIndices: '=',
goToNextStep: '&',
},
link: function (scope, element) {
scope.stepIndexPattern.appendedWildcard = false;
scope.$watch('stepIndexPattern.allIndices', scope.stepIndexPattern.updateList);
scope.$watch('stepIndexPattern.matchingIndices', scope.stepIndexPattern.updateList);
scope.$watch('stepIndexPattern.indexPatternName', () => {
if (scope.stepIndexPattern.indexPatternName && scope.stepIndexPattern.indexPatternName.length === 1) {
if (scope.stepIndexPattern.indexPatternName === '*') {
if (scope.stepIndexPattern.appendedWildcard) {
scope.stepIndexPattern.indexPatternName = '';
scope.stepIndexPattern.appendedWildcard = false;
}
} else {
scope.stepIndexPattern.indexPatternName += '*';
scope.stepIndexPattern.appendedWildcard = true;
setTimeout(() => element.find('#indexPatternNameField')[0].setSelectionRange(1, 1));
}
}
// Only send the request if there's valid input.
if (scope.stepIndexPattern.indexPatternNameForm && scope.stepIndexPattern.indexPatternNameForm.$valid) {
scope.stepIndexPattern.fetchMatchingIndices();
}
// If the index pattern name is invalid, we should reflect that state in the list.
scope.stepIndexPattern.updateList();
});
scope.$watchCollection('stepIndexPattern.indexPatternNameForm.$error', () => {
// If we immediately replace the input with an invalid string, then only the form state
// changes, but not the `indexPatternName` value, so we need to watch both.
scope.stepIndexPattern.updateList();
});
},
controller: function () {
this.matchingIndicesListType = 'noMatches';
this.documentationLinks = documentationLinks;
this.canGoToNextStep = () => (
!this.isFetchingMatchingIndices
&& !this.indexPatternNameForm.$invalid
&& this.hasExactMatches()
);
const hasInvalidIndexPattern = () => (
this.indexPatternNameForm
&& !this.indexPatternNameForm.$error.required
&& this.indexPatternNameForm.$error.indexPattern
);
const hasNoInput = () => (
!this.indexPatternName
|| !this.indexPatternName.trim()
);
this.hasExactMatches = () => (
this.matchingIndices.length
);
const hasPartialMatches = () => (
!this.matchingIndices.length
&& this.partialMatchingIndices.length
);
this.updateList = () => {
if (hasInvalidIndexPattern()) {
return this.matchingIndicesListType = 'invalidIndexPattern';
}
if (hasNoInput()) {
return this.matchingIndicesListType = 'noInput';
}
if (this.hasExactMatches()) {
return this.matchingIndicesListType = 'exactMatches';
}
if (hasPartialMatches()) {
return this.matchingIndicesListType = 'partialMatches';
}
this.matchingIndicesListType = 'noMatches';
};
},
};
});

View file

@ -0,0 +1,9 @@
.createIndexPatternInputContainer {
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.createIndexPatternInputField.ng-untouched {
border-color: #dedede !important;
}

View file

@ -0,0 +1,174 @@
<form
name="stepTimeField.form"
role="form"
ng-submit="stepTimeField.createIndexPattern()"
class="kuiPanel"
>
<div class="kuiPanelHeader">
<div class="kuiPanelHeaderSection">
<h2 class="kuiPanelHeader__title">
Step 2 of 2: Configure settings
</h2>
</div>
</div>
<div class="kuiPanelBody">
<p class="kuiText kuiSubduedText kuiVerticalRhythm">
You've defined <strong>{{stepTimeField.indexPatternName}}</strong> as your index pattern. Now you can specify some settings before we create it.
</p>
<!-- Time field select -->
<div class="kuiVerticalRhythm">
<div class="kuiVerticalRhythmSmall timeFieldNameLabel">
<label
id="timeFilterFieldSelectLabel"
class="kuiLabel"
for="timeFilterFieldSelect"
>
Time Filter field name
</label>
<a
ng-if="!stepTimeField.isFetchingTimeFieldOptions"
class="kuiLink kuiSubText"
ng-click="stepTimeField.fetchTimeFieldOptions()"
kbn-accessible-click
aria-describedby="timeFilterFieldSelectLabel"
>
Refresh
</a>
<p
ng-if="stepTimeField.isFetchingTimeFieldOptions"
class="kuiText kuiSubduedText"
aria-label="Please wait while we fetch your time field options"
aria-describedby="timeFilterFieldSelectLabel"
>
<span
aria-hidden="true"
class="kuiIcon fa-spinner fa-spin"
></span>
</p>
</div>
<div class="kuiVerticalRhythmSmall kuiVerticalRhythm">
<select
ng-show="stepTimeField.canShowMainSelect()"
id="timeFilterFieldSelect"
aria-describedby="timeFilterFieldSelectHelpText"
class="kuiSelect kuiSelect--large"
data-test-subj="createIndexPatternTimeFieldSelect"
ng-disabled="stepTimeField.isTimeFieldSelectDisabled()"
ng-required="stepTimeField.hasTimeFieldOptions()"
ng-options="option as option.display disable when option.isDisabled for option in stepTimeField.timeFieldOptions"
ng-model="stepTimeField.selectedTimeFieldOption"
></select>
<select
ng-show="stepTimeField.canShowLoadingSelect()"
id="timeFilterFieldSelect"
aria-describedby="timeFilterFieldSelectHelpText"
class="kuiSelect kuiSelect--large"
data-test-subj="createIndexPatternTimeFieldSelect"
disabled="disabled"
>
<option>Loading...</option>
</select>
<p
ng-if="stepTimeField.canShowNoTimeBasedFieldsMessage()"
class="kuiText kuiSubduedText"
>
The indices which match this index pattern don't contain any time fields.
</p>
</div>
<p
ng-if="stepTimeField.canShowHelpText()"
id="timeFilterFieldSelectHelpText"
class="kuiText kuiSubduedText kuiVerticalRhythm"
>
The Time Filter will use this field to filter your data by time.
You can choose not to have a time field, but you will not be able to narrow down your data by a time range.
</p>
</div>
<div class="kuiVerticalRhythm">
<button
class="kuiToggleButton"
data-id="toggleButton"
ng-click="stepTimeField.toggleAdvancedOptions()"
>
<span
class="kuiToggleButton__icon kuiIcon"
ng-class="{
'fa-caret-right': !stepTimeField.showAdvancedOptions,
'fa-caret-down': stepTimeField.showAdvancedOptions
}"
data-id="toggleButtonIcon"
>
</span>
<span ng-if="stepTimeField.showAdvancedOptions">Hide advanced options</span>
<span ng-if="!stepTimeField.showAdvancedOptions">Show advanced options</span>
</button>
</div>
<!-- Index pattern id input -->
<div
class="kuiVerticalRhythm"
ng-if="stepTimeField.showAdvancedOptions"
>
<label
class="kuiLabel kuiVerticalRhythmSmall"
for="indexPatternCreationId"
>
Custom index pattern ID
</label>
<div class="kuiVerticalRhythm kuiVerticalRhythmSmall">
<input
class="kuiTextInput kuiTextInput--large"
id="indexPatternCreationId"
data-test-subj="createIndexPatternIdInput"
ng-model="stepTimeField.indexPatternId"
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"
validate-index-pattern
allow-wildcard
name="id"
type="text"
>
</div>
<p
class="kuiText kuiSubduedText kuiVerticalRhythm"
>
Kibana will provide a unique identifier for each index pattern.
If you do not want to use this unique ID, enter a custom one.
</p>
</div>
<!-- Actions -->
<div class="kuiBar kuiVerticalRhythm">
<div class="kuiBarSection">
<div class="kuiButtonGroup">
<button
class="kuiButton kuiButton--secondary kuiButton--iconText"
ng-click="stepTimeField.goToPreviousStep()"
>
<span class="kuiButton__inner">
<span aria-hidden="true" class="kuiButton__icon kuiIcon fa-chevron-left"></span>
<span>Back</span>
</span>
</button>
<button
data-test-subj="createIndexPatternCreateButton"
ng-disabled="!stepTimeField.canCreateIndexPattern()"
class="kuiButton kuiButton--primary"
type="submit"
>
Create index pattern
</button>
</div>
</div>
</div>
</div>
</form>

View file

@ -0,0 +1,68 @@
import 'ui/toggle_panel';
import { uiModules } from 'ui/modules';
import './step_time_field.less';
import template from './step_time_field.html';
const module = uiModules.get('apps/management');
module.directive('stepTimeField', function () {
return {
restrict: 'E',
template,
replace: true,
controllerAs: 'stepTimeField',
bindToController: true,
scope: {
indexPatternId: '=',
indexPatternName: '=',
timeFieldOptions: '=',
selectedTimeFieldOption: '=',
fetchTimeFieldOptions: '&',
isFetchingTimeFieldOptions: '=',
goToPreviousStep: '&',
createIndexPattern: '&',
},
controller: function () {
this.isTimeFieldSelectDisabled = () => (
this.isFetchingTimeFieldOptions
|| this.timeFieldOptionsError
);
this.isFormValid = () => (
this.form.$valid
);
this.hasTimeFieldOptions = () => (
this.timeFieldOptions.length > 1
);
this.canCreateIndexPattern = () => (
!this.timeFieldOptionsError
&& !this.isFetchingTimeFieldOptions
&& this.isFormValid()
);
this.canShowMainSelect = () => (
!this.isFetchingTimeFieldOptions && this.hasTimeFieldOptions()
);
this.canShowLoadingSelect = () => (
this.isFetchingTimeFieldOptions
);
this.canShowNoTimeBasedFieldsMessage = () => (
!this.isFetchingTimeFieldOptions && !this.hasTimeFieldOptions()
);
this.canShowHelpText = () => (
this.isFetchingTimeFieldOptions || this.hasTimeFieldOptions()
);
this.toggleAdvancedOptions = () => {
this.showAdvancedOptions = !this.showAdvancedOptions;
};
this.showAdvancedOptions = !!this.indexPatternId;
},
};
});

View file

@ -0,0 +1,9 @@
/**
* 1. Match select width.
*/
.timeFieldNameLabel {
width: 400px; /* 1 */
display: flex;
align-items: center;
justify-content: space-between;
}

View file

@ -15,7 +15,8 @@
<p ng-if="indexPattern.timeFieldName" class="kuiText kuiVerticalRhythm">
<span class="label label-success">
<i class="fa fa-clock-o"></i><span>Time Filter field name</span>: {{indexPattern.timeFieldName}}
<span class="kuiIcon fa-clock-o"></span>
<span>Time Filter field name</span>: {{indexPattern.timeFieldName}}
</span>
</p>

View file

@ -6,7 +6,6 @@
ng-if="editingId"
href="#/management/kibana/index"
class="kuiButton kuiButton--primary kuiButton--small"
aria-label="Create Index Pattern"
>
<span aria-hidden="true" class="kuiIcon fa-plus"></span>
<span>Create Index Pattern</span>
@ -20,7 +19,9 @@
>
<div class="sidebar-item-title full-title">
<span class="label label-warning">Warning</span>
<p>No default index pattern. You must select or create one to continue.</p>
<p>
No default index pattern. You must select or create one to continue.
</p>
</div>
</li>

View file

@ -1,5 +1,5 @@
import { management } from 'ui/management';
import './create_index_pattern';
import './create_index_pattern_wizard';
import './edit_index_pattern';
import uiRoutes from 'ui/routes';
import { uiModules } from 'ui/modules';

View file

@ -1,15 +1,15 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import 'ui/directives/validate_index_name';
import 'ui/directives/validate_index_pattern';
// Load the kibana app dependencies.
describe('Validate index name directive', function () {
describe('Validate index pattern directive', function () {
let $compile;
let $rootScope;
const noWildcardHtml = '<input type="text" ng-model="indexName" validate-index-name />';
const requiredHtml = '<input type="text" ng-model="indexName" validate-index-name required />';
const allowWildcardHtml = '<input type="text" ng-model="indexName" allow-wildcard validate-index-name />';
const noWildcardHtml = '<input type="text" ng-model="indexName" validate-index-pattern />';
const requiredHtml = '<input type="text" ng-model="indexName" validate-index-pattern required />';
const allowWildcardHtml = '<input type="text" ng-model="indexName" validate-index-pattern validate-index-pattern-allow-wildcard />';
beforeEach(ngMock.module('kibana'));

View file

@ -1,4 +1,4 @@
import html from 'ui/partials/info.html';
import template from 'ui/partials/info.html';
import { uiModules } from 'ui/modules';
uiModules
@ -10,7 +10,7 @@ uiModules
info: '@',
placement: '@'
},
template: html,
template,
link: function ($scope) {
$scope.placement = $scope.placement || 'top';
}

View file

@ -4,13 +4,17 @@ import { uiModules } from 'ui/modules';
uiModules
.get('kibana')
.directive('validateIndexName', function () {
.directive('validateIndexPattern', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, elem, attr, ngModel) {
const illegalCharacters = ['\\', '/', '?', '"', '<', '>', '|', ' ', ','];
const allowWildcard = !_.isUndefined(attr.allowWildcard) && attr.allowWildcard !== 'false';
const allowWildcard =
!_.isUndefined(attr.validateIndexPatternAllowWildcard)
&& attr.validateIndexPatternAllowWildcard !== 'false';
if (!allowWildcard) {
illegalCharacters.push('*');
}
@ -26,7 +30,7 @@ uiModules
return !match;
};
ngModel.$validators.indexNameInput = function (modelValue, viewValue) {
ngModel.$validators.indexPattern = function (modelValue, viewValue) {
return isValid(viewValue);
};
}

View file

@ -21,6 +21,10 @@ export const documentationLinks = {
painlessSyntax: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/modules-scripting-painless-syntax.html`,
luceneExpressions: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/modules-scripting-expression.html`
},
indexPatterns: {
loadingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/tutorial-load-dataset.html`,
introduction: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/index-patterns.html`,
},
query: {
luceneQuerySyntax:
`${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/query-dsl-query-string-query.html#query-string-syntax`,

View file

@ -1,4 +1,10 @@
<i aria-label="{{info}}" class="fa fa-info-circle"
tooltip="{{info}}"
tooltip-placement="{{placement}}"
tooltip-popup-delay="250"></i>
<button
aria-label="{{info}}"
class="kuiInfoButton"
tooltip="{{info}}"
tooltip-placement="{{placement}}"
tooltip-popup-delay="250"
tooltip-trigger="focus"
>
<span class="kuiIcon fa-info-circle"></span>
</button>

View file

@ -0,0 +1,35 @@
import expect from 'expect.js';
export default function ({ getService, getPageObjects }) {
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects(['settings', 'common']);
describe('"Create Index Pattern" wizard', function () {
beforeEach(function () {
// delete .kibana index and then wait for Kibana to re-create it
return kibanaServer.uiSettings.replace({})
.then(function () {
return PageObjects.settings.navigateTo();
})
.then(function () {
return PageObjects.settings.clickKibanaIndices();
});
});
describe('step 1 next button', function () {
it('is disabled by default', async function () {
const btn = await PageObjects.settings.getCreateIndexPatternGoToStep2Button();
const isEnabled = await btn.isEnabled();
expect(isEnabled).not.to.be.ok();
});
it('is enabled once an index pattern with matching indices has been entered', async function () {
await PageObjects.settings.setIndexPatternField();
await PageObjects.common.sleep(1000);
const btn = await PageObjects.settings.getCreateIndexPatternGoToStep2Button();
const isEnabled = await btn.isEnabled();
expect(isEnabled).to.be.ok();
});
});
});
}

View file

@ -1,29 +0,0 @@
import expect from 'expect.js';
export default function ({ getService, getPageObjects }) {
const kibanaServer = getService('kibanaServer');
const screenshots = getService('screenshots');
const PageObjects = getPageObjects(['settings', 'common']);
describe('user input reactions', function () {
beforeEach(function () {
// delete .kibana index and then wait for Kibana to re-create it
return kibanaServer.uiSettings.replace({})
.then(function () {
return PageObjects.settings.navigateTo();
})
.then(function () {
return PageObjects.settings.clickKibanaIndices();
});
});
it('should enable creation after selecting time field', async function () {
// select a time field and check that Create button is enabled
await PageObjects.settings.selectTimeFieldOption('@timestamp');
const createButton = await PageObjects.settings.getCreateButton();
const enabled = await createButton.isEnabled();
screenshots.take('Settings-indices-enable-creation');
expect(enabled).to.be.ok();
});
});
}

View file

@ -84,7 +84,7 @@ export default function ({ getService, getPageObjects }) {
it('should return to index pattern creation page', function returnToPage() {
return retry.try(function tryingForTime() {
return PageObjects.settings.getCreateButton();
return PageObjects.settings.getCreateIndexPatternGoToStep2Button();
});
});

View file

@ -1,41 +0,0 @@
import expect from 'expect.js';
export default function ({ getService, getPageObjects }) {
const kibanaServer = getService('kibanaServer');
const log = getService('log');
const PageObjects = getPageObjects(['settings', 'common']);
describe('initial state', function () {
before(function () {
// delete .kibana index and then wait for Kibana to re-create it
return kibanaServer.uiSettings.replace({})
.then(function () {
return PageObjects.settings.navigateTo();
})
.then(function () {
return PageObjects.settings.clickKibanaIndices();
});
});
it('should contain default index pattern', async function () {
const defaultPattern = 'logstash-*';
const indexPatternField = await PageObjects.settings.getIndexPatternField();
const pattern = await indexPatternField.getProperty('value');
expect(pattern).to.be(defaultPattern);
});
it('should not select the time field', async function () {
const timeFieldNameField = await PageObjects.settings.getTimeFieldNameField();
const timeFieldIsSelected = await timeFieldNameField.isSelected();
log.debug('timeField isSelected = ' + timeFieldIsSelected);
expect(timeFieldIsSelected).to.not.be.ok();
});
it('should not enable creation', async function () {
const createIndexPatternButton = await PageObjects.settings.getCreateIndexPatternButton();
const enabled = await createIndexPatternButton.isEnabled();
expect(enabled).to.not.be.ok();
});
});
}

View file

@ -15,8 +15,7 @@ export default function ({ getService, loadTestFile }) {
await esArchiver.unload('empty_kibana');
});
loadTestFile(require.resolve('./_initial_state'));
loadTestFile(require.resolve('./_creation_form_changes'));
loadTestFile(require.resolve('./_create_index_pattern_wizard'));
loadTestFile(require.resolve('./_index_pattern_create_delete'));
loadTestFile(require.resolve('./_index_pattern_results_sort'));
loadTestFile(require.resolve('./_index_pattern_popularity'));

View file

@ -286,14 +286,16 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
await PageObjects.header.waitUntilLoadingHasFinished();
}
async createIndexPattern(indexPatternName = 'logstash-*', timefield = '@timestamp') {
async createIndexPattern(indexPatternName, timefield = '@timestamp') {
await retry.try(async () => {
await this.navigateTo();
await this.clickKibanaIndices();
await this.setIndexPatternField(indexPatternName);
await PageObjects.common.sleep(2000);
await (await this.getCreateIndexPatternGoToStep2Button()).click();
await PageObjects.common.sleep(2000);
await this.selectTimeFieldOption(timefield);
const createButton = await this.getCreateButton();
await createButton.click();
await (await this.getCreateIndexPatternCreateButton()).click();
});
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.try(async () => {
@ -318,11 +320,20 @@ export function SettingsPageProvider({ getService, getPageObjects }) {
return indexPatternId;
}
async setIndexPatternField(pattern) {
log.debug(`setIndexPatternField(${pattern})`);
return await testSubjects.setValue('createIndexPatternNameInput', pattern);
async setIndexPatternField(indexPatternName = 'logstash-') {
log.debug(`setIndexPatternField(${indexPatternName})`);
const field = await this.getIndexPatternField();
await field.clearValue();
field.type(indexPatternName);
}
async getCreateIndexPatternGoToStep2Button() {
return await testSubjects.find('createIndexPatternGoToStep2Button');
}
async getCreateIndexPatternCreateButton() {
return await testSubjects.find('createIndexPatternCreateButton');
}
async removeIndexPattern() {
let alertText;

View file

@ -1469,6 +1469,7 @@ main {
.kuiInfoButton {
font-size: 16px;
line-height: 0;
background-color: transparent;
color: #0079a5;
cursor: pointer;
@ -2623,6 +2624,33 @@ main {
border: 1px solid #D9D9D9;
border-radius: 4px; }
.kuiPanel--prompt {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
text-align: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
min-height: 300px; }
.kuiPanel--prompt .kuiPanelBody {
padding: 30px;
max-width: 500px; }
.kuiPanel--noBorder {
border: none; }
.kuiPanel--withToolBar {
border-top: none;
border-radius: 0; }
@ -2699,9 +2727,14 @@ main {
outline: none;
border-color: #0079a5; }
/**
* 1. This way we can use h1, h2, etc.
*/
.kuiPanelHeader__title {
font-size: 18px;
line-height: 1.5; }
line-height: 1.5;
margin: 0;
/* 1 */ }
/**
* 1. Undo what barSection mixin does.
@ -2885,7 +2918,7 @@ main {
/* 2 */ }
/**
* 1. Make seamless transition from ToolBar to Table header.
* 1. Make seamless transition from ToolBar to Table header and contained Menu.
* 1. Make seamless transition from Table to ToolBarFooter header.
*/
.kuiControlledTable .kuiTable {
@ -2896,6 +2929,10 @@ main {
border-top: none;
/* 2 */ }
.kuiControlledTable .kuiMenu--contained {
border-top: none;
/* 1 */ }
/**
* 1. Prevent cells from expanding based on content size. This substitutes for table-layout: fixed.
*/

View file

@ -1,5 +1,6 @@
.kuiInfoButton {
font-size: 16px;
line-height: 0;
background-color: transparent;
color: $globalLinkColor;
cursor: pointer;

View file

@ -3,12 +3,29 @@
border-radius: $globalBorderRadius;
}
.kuiPanel--prompt {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
justify-content: center;
min-height: 300px;
.kuiPanelBody {
padding: 30px;
max-width: 500px;
}
}
.kuiPanel--noBorder {
border: none;
}
.kuiPanel--withToolBar {
border-top: none;
border-radius: 0;
}
.kuiPanel--centered {
display: flex;
justify-content: center;
@ -25,9 +42,13 @@
border-bottom: $globalBorderThin;
}
/**
* 1. This way we can use h1, h2, etc.
*/
.kuiPanelHeader__title {
font-size: $globalTitleFontSize;
line-height: $globalLineHeight;
margin: 0; /* 1 */
}
/**

View file

@ -1,5 +1,5 @@
/**
* 1. Make seamless transition from ToolBar to Table header.
* 1. Make seamless transition from ToolBar to Table header and contained Menu.
* 1. Make seamless transition from Table to ToolBarFooter header.
*/
.kuiControlledTable {
@ -10,4 +10,8 @@
.kuiToolBarFooter {
border-top: none; /* 2 */
}
.kuiMenu--contained {
border-top: none; /* 1 */
}
}