diff --git a/package.json b/package.json
index 99b14a072b12..373cfa9c26b7 100644
--- a/package.json
+++ b/package.json
@@ -132,6 +132,7 @@
"moment": "2.10.6",
"moment-timezone": "0.4.1",
"node-uuid": "1.4.7",
+ "papaparse": "4.1.2",
"raw-loader": "0.5.1",
"request": "2.61.0",
"rimraf": "2.4.3",
diff --git a/src/plugins/kibana/public/settings/sections/indices/_create.html b/src/plugins/kibana/public/settings/sections/indices/_create.html
index 7cf592c69441..fa65016ff9f2 100644
--- a/src/plugins/kibana/public/settings/sections/indices/_create.html
+++ b/src/plugins/kibana/public/settings/sections/indices/_create.html
@@ -62,6 +62,7 @@
ng-attr-placeholder="{{index.defaultName}}"
ng-model-options="{ updateOn: 'default blur', debounce: {'default': 2500, 'blur': 0} }"
validate-index-name
+ allow-wildcard
name="name"
required
type="text"
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/parse_csv_step/parse_csv_step.html b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/parse_csv_step/parse_csv_step.html
new file mode 100644
index 000000000000..8c0a0d7812b4
--- /dev/null
+++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/parse_csv_step/parse_csv_step.html
@@ -0,0 +1,63 @@
+
+ Pick a CSV file to get started.
+ Please follow the instructions below.
+
+
+
+
Drop your file here
+
or
+
+
Maximum upload file size: 1 GB
+
+
+
+
+
Review the sample below.
+ Click next if it looks like we parsed your file correctly.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ wizard.file.name }}
+
+
+
+
+
+
+
+
+ {{ col | limitTo:12 }}{{ col.length > 12 ? '...' : '' }}
+ |
+
+
+
+
+ {{ cell }} |
+
+
+
+
+
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/parse_csv_step/parse_csv_step.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/parse_csv_step/parse_csv_step.js
new file mode 100644
index 000000000000..7699cb1d89e7
--- /dev/null
+++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/parse_csv_step/parse_csv_step.js
@@ -0,0 +1,132 @@
+import _ from 'lodash';
+import Papa from 'papaparse';
+import modules from 'ui/modules';
+import template from './parse_csv_step.html';
+import './styles/_add_data_parse_csv_step.less';
+
+modules.get('apps/settings')
+ .directive('parseCsvStep', function () {
+ return {
+ restrict: 'E',
+ template: template,
+ scope: {
+ file: '=',
+ parseOptions: '=',
+ samples: '='
+ },
+ bindToController: true,
+ controllerAs: 'wizard',
+ controller: function ($scope) {
+ const maxSampleRows = 10;
+ const maxSampleColumns = 20;
+
+ this.delimiterOptions = [
+ {
+ label: 'comma',
+ value: ','
+ },
+ {
+ label: 'tab',
+ value: '\t'
+ },
+ {
+ label: 'space',
+ value: ' '
+ },
+ {
+ label: 'semicolon',
+ value: ';'
+ },
+ {
+ label: 'pipe',
+ value: '|'
+ }
+ ];
+
+ this.parse = () => {
+ if (!this.file) return;
+ let row = 1;
+ let rows = [];
+ let data = [];
+
+ const config = _.assign(
+ {
+ header: true,
+ dynamicTyping: true,
+ step: (results, parser) => {
+ if (row > maxSampleRows) {
+ parser.abort();
+
+ // The complete callback isn't automatically called if parsing is manually aborted
+ config.complete();
+ return;
+ }
+ if (row === 1) {
+ // Collect general information on the first pass
+ if (results.meta.fields.length > _.uniq(results.meta.fields).length) {
+ this.formattedErrors.push('Column names must be unique');
+ }
+ _.forEach(results.meta.fields, (field) => {
+ if (_.isEmpty(field)) {
+ this.formattedErrors.push('Column names must not be blank');
+ }
+ });
+ if (results.meta.fields.length > maxSampleColumns) {
+ this.formattedWarnings.push(`Preview truncated to ${maxSampleColumns} columns`);
+ }
+
+ this.columns = results.meta.fields.slice(0, maxSampleColumns);
+ this.parseOptions = _.defaults({}, this.parseOptions, {delimiter: results.meta.delimiter});
+ }
+
+ this.formattedErrors = _.map(results.errors, (error) => {
+ return `${error.type} at line ${row + 1} - ${error.message}`;
+ });
+
+ data = data.concat(results.data);
+
+ rows = rows.concat(_.map(results.data, (row) => {
+ return _.map(this.columns, (columnName) => {
+ return row[columnName];
+ });
+ }));
+
+ ++row;
+ },
+ complete: () => {
+ $scope.$apply(() => {
+ this.rows = rows;
+
+ if (_.isUndefined(this.formattedErrors) || _.isEmpty(this.formattedErrors)) {
+ this.samples = data;
+ }
+ else {
+ delete this.samples;
+ }
+ });
+ }
+ },
+ this.parseOptions
+ );
+
+ Papa.parse(this.file, config);
+ };
+
+ $scope.$watch('wizard.parseOptions', (newValue, oldValue) => {
+ // Delimiter is auto-detected in the first run of the parse function, so we don't want to
+ // re-parse just because it's being initialized.
+ if (!_.isUndefined(oldValue)) {
+ this.parse();
+ }
+ }, true);
+
+ $scope.$watch('wizard.file', () => {
+ delete this.rows;
+ delete this.columns;
+ delete this.formattedErrors;
+ this.formattedWarnings = [];
+ this.parse();
+ });
+ }
+ };
+ });
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/parse_csv_step/styles/_add_data_parse_csv_step.less b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/parse_csv_step/styles/_add_data_parse_csv_step.less
new file mode 100644
index 000000000000..036c4f940e43
--- /dev/null
+++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/parse_csv_step/styles/_add_data_parse_csv_step.less
@@ -0,0 +1,64 @@
+@import (reference) "../../../styles/_add_data_wizard";
+
+.upload-wizard-file-upload-container {
+ min-height: 300px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ background-color: @settings-add-data-wizard-form-control-bg;
+ border: @settings-add-data-wizard-parse-csv-container-border 1px dashed;
+ text-align: center;
+
+ .upload-instructions {
+ font-size: 2em;
+ }
+
+ .upload-instructions-separator {
+ margin: 15px 0;
+ }
+
+ button {
+ width: inherit;
+ }
+
+ button.upload {
+ align-self: center;
+ margin-bottom: 15px;
+ }
+}
+
+.upload-wizard-file-preview-container {
+ .preview {
+ overflow: auto;
+ max-height: 500px;
+ border: @settings-add-data-wizard-parse-csv-container-border 1px solid;
+
+ table {
+ margin-bottom: 0;
+
+ .table-striped()
+ }
+ }
+
+ .parse-error {
+ margin-top: 2em;
+ }
+
+ .advanced-options {
+ display: flex;
+ align-items: center;
+
+ .form-group {
+ display: flex;
+ align-items: center;
+ padding-right: 15px;
+
+ label {
+ padding-right: 8px;
+ margin-bottom: 0;
+ }
+ }
+
+ padding-bottom: 10px;
+ }
+}
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pattern_review_step/pattern_review_step.html b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pattern_review_step/pattern_review_step.html
index e10e8bf1d690..4816d7c9ac0f 100644
--- a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pattern_review_step/pattern_review_step.html
+++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pattern_review_step/pattern_review_step.html
@@ -3,31 +3,48 @@
fields can be changed if we got it wrong!
-
-
-
-
-
+
+
+
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pattern_review_step/pattern_review_step.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pattern_review_step/pattern_review_step.js
index b607c0b22026..75eedc0624d6 100644
--- a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pattern_review_step/pattern_review_step.js
+++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/pattern_review_step/pattern_review_step.js
@@ -6,6 +6,7 @@ import isGeoPointObject from './lib/is_geo_point_object';
import forEachField from './lib/for_each_field';
import './styles/_add_data_pattern_review_step.less';
import moment from 'moment';
+import '../../../../../../../../ui/public/directives/validate_lowercase';
function pickDefaultTimeFieldName(dateFields) {
if (_.isEmpty(dateFields)) {
@@ -26,7 +27,8 @@ modules.get('apps/settings')
scope: {
indexPattern: '=',
pipeline: '=',
- sampleDoc: '='
+ sampleDoc: '=',
+ defaultIndexInput: '='
},
controllerAs: 'reviewStep',
bindToController: true,
@@ -34,6 +36,17 @@ modules.get('apps/settings')
this.errors = [];
const sampleFields = {};
+ this.patternInput = {
+ label: 'Index name',
+ helpText: 'The name of the Elasticsearch index you want to create for your data.',
+ defaultValue: '',
+ placeholder: 'Name'
+ };
+
+ if (this.defaultIndexInput) {
+ this.patternInput.defaultValue = this.defaultIndexInput;
+ }
+
if (_.isUndefined(this.indexPattern)) {
this.indexPattern = {};
}
@@ -62,7 +75,7 @@ modules.get('apps/settings')
});
_.defaults(this.indexPattern, {
- id: 'filebeat-*',
+ id: this.patternInput.defaultValue,
title: 'filebeat-*',
fields: _(sampleFields)
.map((field, fieldName) => {
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/upload_data_step/styles/_add_data_upload_data_step.less b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/upload_data_step/styles/_add_data_upload_data_step.less
new file mode 100644
index 000000000000..e8e472de50c8
--- /dev/null
+++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/upload_data_step/styles/_add_data_upload_data_step.less
@@ -0,0 +1,22 @@
+@import (reference) "../../../styles/_add_data_wizard";
+
+@add-data-upload-step-multi-alert-padding: 2px;
+
+.bulk-results {
+ .alert-warning {
+ padding: @add-data-upload-step-multi-alert-padding;
+ }
+
+ ul.errors {
+ background-color: white;
+ color: @text-color;
+ padding: @alert-padding - @add-data-upload-step-multi-alert-padding;
+ list-style-position: inside;
+ }
+
+ .alert-title {
+ display: flex;
+ padding: @alert-padding - @add-data-upload-step-multi-alert-padding;
+ justify-content: space-between;
+ }
+}
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/upload_data_step/upload_data_step.html b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/upload_data_step/upload_data_step.html
new file mode 100644
index 000000000000..d06bf536d0b5
--- /dev/null
+++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/upload_data_step/upload_data_step.html
@@ -0,0 +1,29 @@
+
+
Sit back, relax, we'll take it from here.
+
+
+ We're loading your data now. This may take some time if you selected a large file.
+
+
+
+
+
+
Upload complete. Let's take a look:
+
+
+ Created {{ uploadStep.created }} documents!
+
+
+
diff --git a/src/plugins/kibana/public/settings/sections/indices/add_data_steps/upload_data_step/upload_data_step.js b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/upload_data_step/upload_data_step.js
new file mode 100644
index 000000000000..93317c913447
--- /dev/null
+++ b/src/plugins/kibana/public/settings/sections/indices/add_data_steps/upload_data_step/upload_data_step.js
@@ -0,0 +1,65 @@
+import modules from 'ui/modules';
+import template from './upload_data_step.html';
+import _ from 'lodash';
+import IngestProvider from 'ui/ingest';
+import './styles/_add_data_upload_data_step.less';
+
+function formatIndexError(errorDoc) {
+ const lineNumber = errorDoc._id.substr(errorDoc._id.lastIndexOf(':') + 1);
+ const errorType = errorDoc.error.type;
+ const errorReason = errorDoc.error.reason;
+
+ return `Line ${lineNumber}: ${errorType} - ${errorReason}`;
+}
+
+modules.get('apps/settings')
+.directive('uploadDataStep', function () {
+ return {
+ template: template,
+ scope: {
+ results: '='
+ },
+ bindToController: true,
+ controllerAs: 'uploadStep',
+ controller: function (Notifier, $window, Private, $scope) {
+ const ingest = Private(IngestProvider);
+ const notify = new Notifier({
+ location: 'Add Data'
+ });
+
+ const usePipeline = !_.isEmpty(_.get(this.results, 'pipeline.processors'));
+ ingest.uploadCSV(this.results.file, this.results.indexPattern.id, this.results.parseOptions.delimiter, usePipeline)
+ .then(
+ (res) => {
+ this.created = 0;
+ this.formattedErrors = [];
+ _.forEach(res.data, (response) => {
+ this.created += response.created;
+ this.formattedErrors = this.formattedErrors.concat(_.map(_.get(response, 'errors.index'), formatIndexError));
+ if (!_.isEmpty(_.get(response, 'errors.other'))) {
+ this.formattedErrors = this.formattedErrors.concat(response.errors.other);
+ }
+ });
+ },
+ (err) => {
+ notify.error(err);
+ $window.scrollTo(0, 0);
+ }
+ );
+
+ this.showAllErrors = false;
+ this.defaultErrorLimit = 10;
+ this.displayErrors = [];
+ $scope.$watchGroup(['uploadStep.formattedErrors', 'uploadStep.showAllErrors'], (newValues) => {
+ const [formattedErrors, showAllErrors] = newValues;
+
+ if (showAllErrors && formattedErrors) {
+ this.displayErrors = formattedErrors;
+ }
+ else if (formattedErrors) {
+ this.displayErrors = formattedErrors.slice(0, this.defaultErrorLimit + 1);
+ }
+ });
+ }
+ };
+});
diff --git a/src/plugins/kibana/public/settings/sections/indices/filebeat/directives/filebeat_wizard.js b/src/plugins/kibana/public/settings/sections/indices/filebeat/directives/filebeat_wizard.js
index 6de8287171bf..315bb94182ff 100644
--- a/src/plugins/kibana/public/settings/sections/indices/filebeat/directives/filebeat_wizard.js
+++ b/src/plugins/kibana/public/settings/sections/indices/filebeat/directives/filebeat_wizard.js
@@ -16,7 +16,7 @@ modules.get('apps/settings')
scope: {},
bindToController: true,
controllerAs: 'wizard',
- controller: function ($scope, AppState, safeConfirm, kbnUrl, $http, Notifier, $window, config, Private) {
+ controller: function ($scope, AppState, safeConfirm, kbnUrl, Notifier, $window, Private) {
const ingest = Private(IngestProvider);
const $state = this.state = new AppState();
diff --git a/src/plugins/kibana/public/settings/sections/indices/index.html b/src/plugins/kibana/public/settings/sections/indices/index.html
index a7a1c40b6c4d..2fec61cf7d74 100644
--- a/src/plugins/kibana/public/settings/sections/indices/index.html
+++ b/src/plugins/kibana/public/settings/sections/indices/index.html
@@ -13,6 +13,13 @@
Pick this option if you already have data in Elasticsearch.
+
+
+
+ Got CSVs? Upload them here. No pain, all gain.
+
diff --git a/src/plugins/kibana/public/settings/sections/indices/index.js b/src/plugins/kibana/public/settings/sections/indices/index.js
index e9b0012ace70..0d4c1f58a1b3 100644
--- a/src/plugins/kibana/public/settings/sections/indices/index.js
+++ b/src/plugins/kibana/public/settings/sections/indices/index.js
@@ -5,6 +5,7 @@ import 'plugins/kibana/settings/sections/indices/_create';
import 'plugins/kibana/settings/sections/indices/_edit';
import 'plugins/kibana/settings/sections/indices/_field_editor';
import 'plugins/kibana/settings/sections/indices/filebeat/index';
+import 'plugins/kibana/settings/sections/indices/upload/index';
import uiRoutes from 'ui/routes';
import uiModules from 'ui/modules';
import indexTemplate from 'plugins/kibana/settings/sections/indices/index.html';
diff --git a/src/plugins/kibana/public/settings/sections/indices/styles/_add_data_wizard.less b/src/plugins/kibana/public/settings/sections/indices/styles/_add_data_wizard.less
index e8354811ae02..c0ebea3e4186 100644
--- a/src/plugins/kibana/public/settings/sections/indices/styles/_add_data_wizard.less
+++ b/src/plugins/kibana/public/settings/sections/indices/styles/_add_data_wizard.less
@@ -52,6 +52,11 @@
}
}
+ .btn-lg {
+ padding: 6px 35px;
+ font-size: 1.2em;
+ }
+
.form-group {
margin-bottom: 5px;
}
diff --git a/src/plugins/kibana/public/settings/sections/indices/upload/directives/upload_wizard.html b/src/plugins/kibana/public/settings/sections/indices/upload/directives/upload_wizard.html
new file mode 100644
index 000000000000..a36d8eb4fd05
--- /dev/null
+++ b/src/plugins/kibana/public/settings/sections/indices/upload/directives/upload_wizard.html
@@ -0,0 +1,90 @@
+
+
+
+ 1. Select
+
+
+ 2. Review
+
+
+ 3. Upload
+
+
+
+
+
diff --git a/src/plugins/kibana/public/settings/sections/indices/upload/directives/upload_wizard.js b/src/plugins/kibana/public/settings/sections/indices/upload/directives/upload_wizard.js
new file mode 100644
index 000000000000..a485ba28ce39
--- /dev/null
+++ b/src/plugins/kibana/public/settings/sections/indices/upload/directives/upload_wizard.js
@@ -0,0 +1,95 @@
+import modules from 'ui/modules';
+import template from 'plugins/kibana/settings/sections/indices/upload/directives/upload_wizard.html';
+import IngestProvider from 'ui/ingest';
+import 'plugins/kibana/settings/sections/indices/add_data_steps/pattern_review_step';
+import 'plugins/kibana/settings/sections/indices/add_data_steps/parse_csv_step';
+import 'plugins/kibana/settings/sections/indices/add_data_steps/upload_data_step';
+import '../../styles/_add_data_wizard.less';
+
+modules.get('apps/settings')
+ .directive('uploadWizard', function () {
+ return {
+ restrict: 'E',
+ template: template,
+ scope: {},
+ bindToController: true,
+ controllerAs: 'wizard',
+ controller: function ($scope, AppState, safeConfirm, kbnUrl, Notifier, $window, Private) {
+ const ingest = Private(IngestProvider);
+ const $state = this.state = new AppState();
+
+ var notify = new Notifier({
+ location: 'Add Data'
+ });
+
+ var totalSteps = 3;
+ this.stepResults = {};
+
+ this.setCurrentStep = (step) => {
+ if (!this.complete) {
+ $state.currentStep = step;
+ $state.save();
+ }
+ };
+ this.setCurrentStep(0);
+
+ this.nextStep = () => {
+ if ($state.currentStep + 1 < totalSteps) {
+ this.setCurrentStep($state.currentStep + 1);
+ }
+ else if ($state.currentStep + 1 === totalSteps) {
+ kbnUrl.change('/discover', null, {index: this.stepResults.indexPattern.id});
+ }
+ };
+
+ this.prevStep = () => {
+ if ($state.currentStep > 0) {
+ this.setCurrentStep($state.currentStep - 1);
+ }
+ };
+
+ this.save = () => {
+ return ingest.save(this.stepResults.indexPattern)
+ .then(
+ () => {
+ this.nextStep();
+ },
+ (err) => {
+ notify.error(err);
+ $window.scrollTo(0,0);
+ }
+ );
+ };
+
+ $scope.$watch('wizard.state.currentStep', (newValue, oldValue) => {
+ if (this.complete) {
+ $state.currentStep = totalSteps - 1;
+ $state.save();
+ return;
+ }
+ if (newValue + 1 === totalSteps) {
+ this.complete = true;
+ }
+ if (newValue < oldValue) {
+ return safeConfirm('Going back will reset any changes you\'ve made to this step, do you want to continue?')
+ .then(
+ () => {
+ if ($state.currentStep < 1) {
+ delete this.stepResults.indexPattern;
+ }
+ this.currentStep = newValue;
+ },
+ () => {
+ $state.currentStep = oldValue;
+ $state.save();
+ }
+ );
+ }
+ else {
+ this.currentStep = newValue;
+ }
+ });
+ }
+ };
+ });
+
diff --git a/src/plugins/kibana/public/settings/sections/indices/upload/index.html b/src/plugins/kibana/public/settings/sections/indices/upload/index.html
new file mode 100644
index 000000000000..123fc4b91b11
--- /dev/null
+++ b/src/plugins/kibana/public/settings/sections/indices/upload/index.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/plugins/kibana/public/settings/sections/indices/upload/index.js b/src/plugins/kibana/public/settings/sections/indices/upload/index.js
new file mode 100644
index 000000000000..473dac7e7eb4
--- /dev/null
+++ b/src/plugins/kibana/public/settings/sections/indices/upload/index.js
@@ -0,0 +1,7 @@
+import routes from 'ui/routes';
+import template from 'plugins/kibana/settings/sections/indices/upload/index.html';
+import './directives/upload_wizard';
+
+routes.when('/settings/indices/create/upload', {
+ template: template
+});
diff --git a/src/plugins/kibana/public/settings/styles/main.less b/src/plugins/kibana/public/settings/styles/main.less
index 002ea4c2f085..ab760fb42f87 100644
--- a/src/plugins/kibana/public/settings/styles/main.less
+++ b/src/plugins/kibana/public/settings/styles/main.less
@@ -204,4 +204,3 @@ kbn-settings-indices {
.kbn-settings-indices-create {
.time-and-pattern > div {}
}
-
diff --git a/src/ui/public/directives/__tests__/validate_index_name.js b/src/ui/public/directives/__tests__/validate_index_name.js
index f792f65c8174..3deb454077e1 100644
--- a/src/ui/public/directives/__tests__/validate_index_name.js
+++ b/src/ui/public/directives/__tests__/validate_index_name.js
@@ -8,7 +8,8 @@ import 'ui/directives/validate_index_name';
describe('Validate index name directive', function () {
let $compile;
let $rootScope;
- let html = '';
+ let noWildcardHtml = '';
+ let allowWildcardHtml = '';
beforeEach(ngMock.module('kibana'));
@@ -17,14 +18,14 @@ describe('Validate index name directive', function () {
$rootScope = _$rootScope_;
}));
- function checkPattern(input) {
+ function checkPattern(input, html) {
$rootScope.indexName = input;
let element = $compile(html)($rootScope);
$rootScope.$digest();
return element;
}
- let badPatterns = [
+ const badPatterns = [
null,
undefined,
'',
@@ -41,19 +42,22 @@ describe('Validate index name directive', function () {
'foo,bar',
];
- let goodPatterns = [
+ const goodPatterns = [
'...',
'foo',
'foo.bar',
+ '[foo-]YYYY-MM-DD',
+ ];
+
+ const wildcardPatterns = [
'foo*',
'foo.bar*',
- 'foo.*',
- '[foo-]YYYY-MM-DD',
+ 'foo.*'
];
badPatterns.forEach(function (pattern) {
it('should not accept index pattern: ' + pattern, function () {
- let element = checkPattern(pattern);
+ let element = checkPattern(pattern, noWildcardHtml);
expect(element.hasClass('ng-invalid')).to.be(true);
expect(element.hasClass('ng-valid')).to.not.be(true);
});
@@ -61,7 +65,23 @@ describe('Validate index name directive', function () {
goodPatterns.forEach(function (pattern) {
it('should accept index pattern: ' + pattern, function () {
- let element = checkPattern(pattern);
+ let element = checkPattern(pattern, noWildcardHtml);
+ expect(element.hasClass('ng-invalid')).to.not.be(true);
+ expect(element.hasClass('ng-valid')).to.be(true);
+ });
+ });
+
+ it('should disallow wildcards by default', function () {
+ wildcardPatterns.forEach(function (pattern) {
+ let element = checkPattern(pattern, noWildcardHtml);
+ expect(element.hasClass('ng-invalid')).to.be(true);
+ expect(element.hasClass('ng-valid')).to.not.be(true);
+ });
+ });
+
+ it('should allow wildcards if the allow-wildcard attribute is present', function () {
+ wildcardPatterns.forEach(function (pattern) {
+ let element = checkPattern(pattern, allowWildcardHtml);
expect(element.hasClass('ng-invalid')).to.not.be(true);
expect(element.hasClass('ng-valid')).to.be(true);
});
diff --git a/src/ui/public/directives/file_upload.js b/src/ui/public/directives/file_upload.js
index 03fb7137806d..06c4752b456b 100644
--- a/src/ui/public/directives/file_upload.js
+++ b/src/ui/public/directives/file_upload.js
@@ -22,7 +22,7 @@ module.directive('fileUpload', function () {
const handleFile = (file) => {
if (_.isUndefined(file)) return;
- if ($scope.onRead) {
+ if (_.has(attrs, 'onRead')) {
let reader = new FileReader();
reader.onload = function (e) {
$scope.$apply(function () {
@@ -32,8 +32,10 @@ module.directive('fileUpload', function () {
reader.readAsText(file);
}
- if ($scope.onLocate) {
- $scope.onLocate({ file });
+ if (_.has(attrs, 'onLocate')) {
+ $scope.$apply(function () {
+ $scope.onLocate({ file });
+ });
}
};
diff --git a/src/ui/public/directives/validate_index_name.js b/src/ui/public/directives/validate_index_name.js
index 1082616ee0e7..10b51c4dfb8f 100644
--- a/src/ui/public/directives/validate_index_name.js
+++ b/src/ui/public/directives/validate_index_name.js
@@ -8,11 +8,13 @@ uiModules
return {
restrict: 'A',
require: 'ngModel',
- scope: {
- 'ngModel': '='
- },
link: function ($scope, elem, attr, ngModel) {
- let illegalCharacters = ['\\', '/', '?', '"', '<', '>', '|', ' ', ','];
+ const illegalCharacters = ['\\', '/', '?', '"', '<', '>', '|', ' ', ','];
+ const allowWildcard = !_.isUndefined(attr.allowWildcard) && attr.allowWildcard !== 'false';
+ if (!allowWildcard) {
+ illegalCharacters.push('*');
+ }
+
let isValid = function (input) {
if (input == null || input === '' || input === '.' || input === '..') return false;
@@ -22,19 +24,9 @@ uiModules
return !match;
};
- // From User
- ngModel.$parsers.unshift(function (value) {
- let valid = isValid(value);
- ngModel.$setValidity('indexNameInput', valid);
- return valid ? value : undefined;
- });
-
- // To user
- ngModel.$formatters.unshift(function (value) {
- ngModel.$setValidity('indexNameInput', isValid(value));
- return value;
- });
-
+ ngModel.$validators.indexNameInput = function (modelValue, viewValue) {
+ return isValid(viewValue);
+ };
}
};
});
diff --git a/src/ui/public/directives/validate_lowercase.js b/src/ui/public/directives/validate_lowercase.js
new file mode 100644
index 000000000000..fb37841ee631
--- /dev/null
+++ b/src/ui/public/directives/validate_lowercase.js
@@ -0,0 +1,21 @@
+import uiModules from 'ui/modules';
+
+uiModules
+.get('kibana')
+.directive('validateLowercase', function () {
+ return {
+ restrict: 'A',
+ require: 'ngModel',
+ link: function ($scope, elem, attr, ctrl) {
+ ctrl.$validators.lowercase = function (modelValue, viewValue) {
+ if (ctrl.$isEmpty(modelValue)) {
+ // consider empty models to be valid per lowercase rules
+ return true;
+ }
+
+ return viewValue.toLowerCase() === viewValue;
+ };
+ }
+ };
+});
+
diff --git a/src/ui/public/ingest/__tests__/ingest.js b/src/ui/public/ingest/__tests__/ingest.js
index 16dbb7ebf1c4..4ca53e5e9db3 100644
--- a/src/ui/public/ingest/__tests__/ingest.js
+++ b/src/ui/public/ingest/__tests__/ingest.js
@@ -26,7 +26,7 @@ describe('Ingest Service', function () {
it('Sets the default index if there isn\'t one already', function () {
$httpBackend
- .when('POST', '../api/kibana/ingest')
+ .when('POST', '/api/kibana/ingest')
.respond('ok');
expect(config.get('defaultIndex')).to.be(null);
@@ -38,7 +38,7 @@ describe('Ingest Service', function () {
it('Returns error from ingest API if there is one', function (done) {
$httpBackend
- .expectPOST('../api/kibana/ingest')
+ .expectPOST('/api/kibana/ingest')
.respond(400);
ingest.save({id: 'foo'})
@@ -57,7 +57,7 @@ describe('Ingest Service', function () {
it('Broadcasts an ingest:updated event on the rootScope upon succesful save', function () {
$httpBackend
- .when('POST', '../api/kibana/ingest')
+ .when('POST', '/api/kibana/ingest')
.respond('ok');
ingest.save({id: 'foo'});
@@ -75,7 +75,7 @@ describe('Ingest Service', function () {
it('Calls the DELETE endpoint of the ingest API with the given id', function () {
$httpBackend
- .expectDELETE('../api/kibana/ingest/foo')
+ .expectDELETE('/api/kibana/ingest/foo')
.respond('ok');
ingest.delete('foo');
@@ -84,7 +84,7 @@ describe('Ingest Service', function () {
it('Returns error from ingest API if there is one', function (done) {
$httpBackend
- .expectDELETE('../api/kibana/ingest/foo')
+ .expectDELETE('/api/kibana/ingest/foo')
.respond(404);
ingest.delete('foo')
@@ -103,7 +103,7 @@ describe('Ingest Service', function () {
it('Broadcasts an ingest:updated event on the rootScope upon succesful save', function () {
$httpBackend
- .when('DELETE', '../api/kibana/ingest/foo')
+ .when('DELETE', '/api/kibana/ingest/foo')
.respond('ok');
ingest.delete('foo');
@@ -114,11 +114,53 @@ describe('Ingest Service', function () {
});
});
+ describe('uploadCSV', function () {
+ it('throws an error if file and index pattern are not provided', function () {
+ expect(ingest.uploadCSV).to.throwException(/file is required/);
+ expect(ingest.uploadCSV).withArgs('foo').to.throwException(/index pattern is required/);
+ });
+
+ it('POSTs to the kibana _data endpoint with the correct params and the file attached as multipart/form-data', function () {
+ $httpBackend
+ .expectPOST('/api/kibana/foo/_data?csv_delimiter=;&pipeline=true', function (data) {
+ // The assertions we can do here are limited because of poor browser support for FormData methods
+ return data instanceof FormData;
+ })
+ .respond('ok');
+
+ const file = new Blob(['foo,bar'], {type : 'text/csv'});
+
+ ingest.uploadCSV(file, 'foo', ';', true);
+ $httpBackend.flush();
+ });
+
+ it('Returns error from the data API if there is one', function (done) {
+ $httpBackend
+ .expectPOST('/api/kibana/foo/_data?csv_delimiter=;&pipeline=true')
+ .respond(404);
+
+ const file = new Blob(['foo,bar'], {type : 'text/csv'});
+
+ ingest.uploadCSV(file, 'foo', ';', true)
+ .then(
+ () => {
+ throw new Error('expected an error response');
+ },
+ (error) => {
+ expect(error.status).to.be(404);
+ done();
+ }
+ );
+
+ $httpBackend.flush();
+ });
+ });
+
describe('getProcessors', () => {
it('Calls the processors GET endpoint of the ingest API', function () {
$httpBackend
- .expectGET('../api/kibana/ingest/processors')
+ .expectGET('/api/kibana/ingest/processors')
.respond('ok');
ingest.getProcessors();
@@ -127,7 +169,7 @@ describe('Ingest Service', function () {
it('Throws user-friendly error when there is an error in the request', function (done) {
$httpBackend
- .when('GET', '../api/kibana/ingest/processors')
+ .when('GET', '/api/kibana/ingest/processors')
.respond(404);
ingest.getProcessors()
diff --git a/src/ui/public/ingest/ingest.js b/src/ui/public/ingest/ingest.js
index bee1cb564d3a..4d31c8271827 100644
--- a/src/ui/public/ingest/ingest.js
+++ b/src/ui/public/ingest/ingest.js
@@ -1,10 +1,13 @@
+import PluginsKibanaSettingsSectionsIndicesRefreshKibanaIndexProvider from 'plugins/kibana/settings/sections/indices/_refresh_kibana_index';
import { keysToCamelCaseShallow, keysToSnakeCaseShallow } from '../../../plugins/kibana/common/lib/case_conversion';
import _ from 'lodash';
import angular from 'angular';
+import chrome from 'ui/chrome';
-export default function IngestProvider($rootScope, $http, config, $q) {
+export default function IngestProvider($rootScope, $http, config, $q, Private, indexPatterns) {
- const ingestAPIPrefix = '../api/kibana/ingest';
+ const ingestAPIPrefix = chrome.addBasePath('/api/kibana/ingest');
+ const refreshKibanaIndex = Private(PluginsKibanaSettingsSectionsIndicesRefreshKibanaIndexProvider);
this.save = function (indexPattern, pipeline) {
if (_.isEmpty(indexPattern)) {
@@ -24,6 +27,7 @@ export default function IngestProvider($rootScope, $http, config, $q) {
config.set('defaultIndex', indexPattern.id);
}
+ indexPatterns.getIds.clearCache();
$rootScope.$broadcast('ingest:updated');
});
};
@@ -35,6 +39,7 @@ export default function IngestProvider($rootScope, $http, config, $q) {
return $http.delete(`${ingestAPIPrefix}/${ingestId}`)
.then(() => {
+ indexPatterns.getIds.clearCache();
$rootScope.$broadcast('ingest:updated');
});
};
@@ -71,4 +76,30 @@ export default function IngestProvider($rootScope, $http, config, $q) {
});
};
+ this.uploadCSV = function (file, indexPattern, delimiter, pipeline) {
+ if (_.isUndefined(file)) {
+ throw new Error('file is required');
+ }
+ if (_.isUndefined(indexPattern)) {
+ throw new Error('index pattern is required');
+ }
+
+ const formData = new FormData();
+ formData.append('csv', file);
+
+ const params = {};
+ if (!_.isUndefined(delimiter)) {
+ params.csv_delimiter = delimiter;
+ }
+ if (!_.isUndefined(pipeline)) {
+ params.pipeline = pipeline;
+ }
+
+ return $http.post(chrome.addBasePath(`/api/kibana/${indexPattern}/_data`), formData, {
+ params: params,
+ transformRequest: angular.identity,
+ headers: {'Content-Type': undefined}
+ });
+ };
+
}
diff --git a/src/ui/public/styles/variables/for-theme.less b/src/ui/public/styles/variables/for-theme.less
index d28a7af4cc14..5e55659e9dea 100644
--- a/src/ui/public/styles/variables/for-theme.less
+++ b/src/ui/public/styles/variables/for-theme.less
@@ -138,6 +138,9 @@
@settings-filebeat-wizard-processor-container-overlay-bg: fade(#000, 10%);
+// Settings - Add Data Wizard - Parse CSV
+@settings-add-data-wizard-parse-csv-container-border: @kibanaBlue3;
+
// Visualize ===================================================================
@visualize-show-spy-border: @gray-lighter;
@visualize-show-spy-bg: @white;
diff --git a/src/ui/public/url/__tests__/url.js b/src/ui/public/url/__tests__/url.js
index 10319796c244..e6be67b527a6 100644
--- a/src/ui/public/url/__tests__/url.js
+++ b/src/ui/public/url/__tests__/url.js
@@ -272,6 +272,27 @@ describe('kbnUrl', function () {
expect($location.search()).to.eql({});
expect($location.hash()).to.be('');
});
+
+ it('should allow setting app state on the target url', function () {
+ let path = '/test/path';
+ let search = {search: 'test'};
+ let hash = 'hash';
+ let newPath = '/new/location';
+
+ $location.path(path).search(search).hash(hash);
+
+ // verify the starting state
+ expect($location.path()).to.be(path);
+ expect($location.search()).to.eql(search);
+ expect($location.hash()).to.be(hash);
+
+ kbnUrl.change(newPath, null, {foo: 'bar'});
+
+ // verify the ending state
+ expect($location.path()).to.be(newPath);
+ expect($location.search()).to.eql({_a: '(foo:bar)'});
+ expect($location.hash()).to.be('');
+ });
});
describe('changePath', function () {
@@ -319,6 +340,27 @@ describe('kbnUrl', function () {
expect($location.hash()).to.be('');
});
+ it('should allow setting app state on the target url', function () {
+ let path = '/test/path';
+ let search = {search: 'test'};
+ let hash = 'hash';
+ let newPath = '/new/location';
+
+ $location.path(path).search(search).hash(hash);
+
+ // verify the starting state
+ expect($location.path()).to.be(path);
+ expect($location.search()).to.eql(search);
+ expect($location.hash()).to.be(hash);
+
+ kbnUrl.redirect(newPath, null, {foo: 'bar'});
+
+ // verify the ending state
+ expect($location.path()).to.be(newPath);
+ expect($location.search()).to.eql({_a: '(foo:bar)'});
+ expect($location.hash()).to.be('');
+ });
+
it('should replace the current history entry', function () {
sinon.stub($location, 'replace');
$location.url('/some/path');
diff --git a/src/ui/public/url/url.js b/src/ui/public/url/url.js
index cd3ed891e28f..e226166b97cf 100644
--- a/src/ui/public/url/url.js
+++ b/src/ui/public/url/url.js
@@ -2,6 +2,7 @@ import _ from 'lodash';
import 'ui/filters/uriescape';
import 'ui/filters/rison';
import uiModules from 'ui/modules';
+import rison from 'rison-node';
uiModules.get('kibana/url')
@@ -17,8 +18,8 @@ function KbnUrlProvider($route, $location, $rootScope, globalState, $parse, getA
* @param {Object} [paramObj] - optional set of parameters for the url template
* @return {undefined}
*/
- self.change = function (url, paramObj) {
- self._changeLocation('url', url, paramObj);
+ self.change = function (url, paramObj, appState) {
+ self._changeLocation('url', url, paramObj, false, appState);
};
/**
@@ -40,8 +41,8 @@ function KbnUrlProvider($route, $location, $rootScope, globalState, $parse, getA
* @param {Object} [paramObj] - optional set of parameters for the url template
* @return {undefined}
*/
- self.redirect = function (url, paramObj) {
- self._changeLocation('url', url, paramObj, true);
+ self.redirect = function (url, paramObj, appState) {
+ self._changeLocation('url', url, paramObj, true, appState);
};
/**
@@ -142,7 +143,7 @@ function KbnUrlProvider($route, $location, $rootScope, globalState, $parse, getA
/////
let reloading;
- self._changeLocation = function (type, url, paramObj, replace) {
+ self._changeLocation = function (type, url, paramObj, replace, appState) {
let prev = {
path: $location.path(),
search: $location.search()
@@ -152,6 +153,10 @@ function KbnUrlProvider($route, $location, $rootScope, globalState, $parse, getA
$location[type](url);
if (replace) $location.replace();
+ if (appState) {
+ $location.search('_a', rison.encode(appState));
+ }
+
let next = {
path: $location.path(),
search: $location.search()
diff --git a/test/unit/api/ingest/_data.js b/test/unit/api/ingest/_data.js
index f8fbdd14b620..4d2a4e5249f1 100644
--- a/test/unit/api/ingest/_data.js
+++ b/test/unit/api/ingest/_data.js
@@ -103,6 +103,16 @@ define(function (require) {
});
});
+ bdd.it('should use the filename and line numbers as document IDs', function () {
+ return request.post('/kibana/names/_data')
+ .attach('csv', 'test/unit/fixtures/fake_names_with_mapping_errors.csv')
+ .expect(200)
+ .then((dataResponse) => {
+ const id = dataResponse.body[0].errors.index[0]._id;
+ expect(id).to.be('fake_names_with_mapping_errors.csv:2');
+ });
+ });
+
bdd.it('should report any csv parsing errors under an "errors.other" key', function () {
return request.post('/kibana/names/_data')
.attach('csv', 'test/unit/fixtures/fake_names_with_parse_errors.csv')