Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Data - CSV Upload UI #6845

Merged
merged 23 commits into from
Jun 1, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
57c391a
[Wizard] Creates a new CSV Add Data Wizard
Bargs Mar 17, 2016
ed5e4e3
Remove pipeline creation step from CSV wizard
Bargs May 10, 2016
d6bac79
Merge branch 'feature/ingest' into ingest/uploadUI
Bargs May 17, 2016
e23f54a
[Wizard] Improved index pattern input validation
Bargs May 17, 2016
a32364f
[Wizard] Default index pattern input value on pattern review step is now
Bargs May 19, 2016
e11b270
Refresh index pattern cache after saving or deleting via ingest service
Bargs May 19, 2016
271e78f
Enhance kbnUrl to allow setting app state on target url
Bargs May 19, 2016
c080b3d
[Wizard] Done button now sends you to the correct index pattern on di…
Bargs May 19, 2016
4140db8
[Wizard] code cleanup
Bargs May 20, 2016
f604b12
[Wizard] Ensure row values are ordered correctly
Bargs May 20, 2016
625ca75
[Wizard] Fix test errors caused by async task, refreshing kibana inde…
Bargs May 20, 2016
8205f4b
Remove default index name and add a placeholder
Bargs May 25, 2016
526d5d4
Update help text so it doesn't imply that you can add data to existin…
Bargs May 25, 2016
2228e86
Limit the number of errors initially displayed on upload data step wi…
Bargs May 25, 2016
873ff3f
Update id parsing logic since the API changed the ID format
Bargs May 25, 2016
a2f1e17
Increment papaparse error row by 1 so the reported row number matches…
Bargs May 25, 2016
001ff8b
Fail early if CSV contains empty column headers
Bargs May 25, 2016
900a4ee
Extract complex error formatting logic into its own function
Bargs May 25, 2016
3143c06
Added API test so we don't forget that the ID format is a part of the…
Bargs May 25, 2016
f237ec0
Don't allow duplicate headers in CSVs
Bargs May 26, 2016
d04aea5
[FileUpload] Correctly detect the existence of directive attributes
Bargs May 31, 2016
66880a3
Improve parsing performance
Bargs May 31, 2016
b925554
Truncate columns to 20 because we don't really need more, and perform…
Bargs Jun 1, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ <h4>Time-interval based index patterns are deprecated!</h4>
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"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<file-upload ng-if="!wizard.file" on-locate="wizard.file = file" upload-selector="button.upload">
<h2><em>Pick a CSV file to get started.</em>
Please follow the instructions below.
</h2>

<div class="upload-wizard-file-upload-container">
<div class="upload-instructions">Drop your file here</div>
<div class="upload-instructions-separator">or</div>
<button class="btn btn-primary btn-lg controls upload" ng-click>
Select File
</button>
<div>Maximum upload file size: 1 GB</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this value available from config, etc so that it doesn't get out of sync with the actual limit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately it's only available server side.

</div>
</file-upload>

<div class="upload-wizard-file-preview-container" ng-if="wizard.file">
<h2><em>Review the sample below.</em>
Click next if it looks like we parsed your file correctly.
</h2>

<div ng-if="!!wizard.formattedErrors.length" class="alert alert-danger parse-error">
<ul>
<li ng-repeat="error in wizard.formattedErrors track by $index">{{ error }}</li>
</ul>
</div>

<div ng-if="!!wizard.formattedWarnings.length" class="alert alert-warning">
<ul>
<li ng-repeat="warning in wizard.formattedWarnings track by $index">{{ warning }}</li>
</ul>
</div>

<div class="advanced-options form-inline">
<span class="form-group">
<label>Delimiter</label>
<select ng-model="wizard.parseOptions.delimiter"
ng-options="option.value as option.label for option in wizard.delimiterOptions"
class="form-control">
</select>
</span>
<span class="form-group">
<label>Filename:</label>
{{ wizard.file.name }}
</span>
</div>

<div class="preview">
<table class="table table-condensed">
<thead>
<tr>
<th ng-repeat="col in wizard.columns track by $index">
<span title="{{ col }}">{{ col | limitTo:12 }}{{ col.length > 12 ? '...' : '' }}</span>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in wizard.rows">
<td ng-repeat="cell in row track by $index">{{ cell }}</td>
Copy link
Contributor

@BigFunger BigFunger May 16, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we limiting the length of the column headers to 12 characters, but not limiting the data (which in theory could be significantly longer than 12 characters)? Is that intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the data can wrap but the columns shouldn't. At least that's my thinking. The column limit idea came from @alt74, what do you think Jurgen?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can wrap, but the css as it is now will not force it to wrap if it gets big.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, right now it's left up to the browser to figure out the best sizing for each column. Is there a better way to handle it?

</tr>
</tbody>
</table>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be wrapped in a try/catch? I don't know enough about Papaparse, but I assume this would be a good place to wrap.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know papaparse doesn't throw any errors, it returns them as a part of the results.

};

$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();
});
}
};
});
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,48 @@ <h2><em>Review the index pattern.</em>
fields can be changed if we got it wrong!
</h2>

<div class="pattern-review form-inline">
<div ng-show="reviewStep.errors.length" class="alert alert-danger">
<div ng-repeat="error in reviewStep.errors">{{ error }}</div>
</div>
<label>Index name or pattern</label>
<span id="pattern-help" class="help-block">Patterns allow you to define dynamic index names using * as a wildcard. Example: filebeat-*</span>
<input ng-model="reviewStep.indexPattern.id" class="pattern-input form-control" aria-describedby="pattern-help"/>
<label>
<input ng-model="reviewStep.isTimeBased" type="checkbox"/>
time based
</label>
<label ng-if="reviewStep.isTimeBased" class="time-field-input">
Time Field
<select ng-model="reviewStep.indexPattern.timeFieldName" name="time_field_name" class="form-control">
<option ng-repeat="field in reviewStep.dateFields" value="{{field}}">
{{field}}
</option>
</select>
</label>
</div>
<form name="reviewStep.form">
<div class="pattern-review form-inline">
<div ng-show="reviewStep.errors.length" class="alert alert-danger">
<div ng-repeat="error in reviewStep.errors">{{ error }}</div>
</div>
<div class="alert alert-danger"
ng-show="reviewStep.form.pattern.$dirty && reviewStep.form.pattern.$error.lowercase">
Index names must be all lowercase
</div>
<div class="alert alert-danger"
ng-show="reviewStep.form.pattern.$dirty && reviewStep.form.pattern.$error.indexNameInput">
An index name must not be empty and cannot contain whitespace or any of the following characters: ", *, \, <, |, ,, >, /, ?
</div>

<paginated-table
class="pattern-review-field-table"
columns="reviewStep.columns"
rows="reviewStep.rows"
per-page="10">
</paginated-table>
<label>{{ reviewStep.patternInput.label }}</label>
<span id="pattern-help" class="help-block">{{ reviewStep.patternInput.helpText }}</span>
<input name="pattern" ng-model="reviewStep.indexPattern.id"
class="pattern-input form-control"
novalidate
required
validate-index-name
validate-lowercase
placeholder="{{reviewStep.patternInput.placeholder}}"
aria-describedby="pattern-help"/>
<label>
<input ng-model="reviewStep.isTimeBased" type="checkbox"/>
time based
</label>
<label ng-if="reviewStep.isTimeBased" class="time-field-input">
Time Field
<select ng-model="reviewStep.indexPattern.timeFieldName" name="time_field_name" class="form-control">
<option ng-repeat="field in reviewStep.dateFields" value="{{field}}">
{{field}}
</option>
</select>
</label>
</div>

<paginated-table
class="pattern-review-field-table"
columns="reviewStep.columns"
rows="reviewStep.rows"
per-page="10">
</paginated-table>
</form>
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -26,14 +27,26 @@ modules.get('apps/settings')
scope: {
indexPattern: '=',
pipeline: '=',
sampleDoc: '='
sampleDoc: '=',
defaultIndexInput: '='
},
controllerAs: 'reviewStep',
bindToController: true,
controller: function ($scope, Private) {
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 = {};
}
Expand Down Expand Up @@ -62,7 +75,7 @@ modules.get('apps/settings')
});

_.defaults(this.indexPattern, {
id: 'filebeat-*',
id: this.patternInput.defaultValue,
title: 'filebeat-*',
fields: _(sampleFields)
.map((field, fieldName) => {
Expand Down
Loading