mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
"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:
parent
4bbd127626
commit
9175137c57
40 changed files with 1259 additions and 660 deletions
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
|
||||
<kbn-info info="This field will be used to filter events with the global time filter"></kbn-info>
|
||||
|
||||
<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>
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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-*';
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
import './create_index_pattern';
|
|
@ -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';
|
||||
}
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
import './create_index_pattern_wizard';
|
|
@ -0,0 +1 @@
|
|||
import './matching_indices_list';
|
|
@ -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>
|
|
@ -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();
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
.matchingIndicesListLoadingPrompt {
|
||||
min-height: 60px;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
import './step_index_pattern';
|
|
@ -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>
|
|
@ -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';
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
.createIndexPatternInputContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.createIndexPatternInputField.ng-untouched {
|
||||
border-color: #dedede !important;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
import './step_time_field';
|
|
@ -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>
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* 1. Match select width.
|
||||
*/
|
||||
.timeFieldNameLabel {
|
||||
width: 400px; /* 1 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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'));
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
|
@ -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`,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
|
|
|
@ -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;
|
||||
|
|
41
ui_framework/dist/ui_framework.css
vendored
41
ui_framework/dist/ui_framework.css
vendored
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.kuiInfoButton {
|
||||
font-size: 16px;
|
||||
line-height: 0;
|
||||
background-color: transparent;
|
||||
color: $globalLinkColor;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -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 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue