[ML] Adds File Data Visualizer feature (#24423)

* [ML] File datavisualizer initial commit (#22828)

* [ML] File datavisualizer initial commit

* removing mocked data and adding initial stats

* adding card styling to fields

* Revert "". accidentally added with no commit message

This reverts commit d762d20b706e6a770e631f863b9e7d8879bb7ee6.

* adding date type to timestamp field

* renaming FileStats to FieldsStats

* code clean up

* changes based on review

* changes to error handling

* [ML] Adding file datavisualizer overrides (#23194)

* [ML] Adding file datavisualizer overrides

* improvements to overrides

* removing comment

* small refactor

* removing accidentally added file

* updates based on review

* fixing broken test

* adding missing grok pattern override

* fixing test

* [ML] Refactoring override option lists (#23424)

* [ML] Refactoring override option lists

* moving lists out of class

* updating test snapshot

* [ML] Fixing field editing (#23500)

* [ML] Changes to timestamp formats (#23498)

* [ML] Changes to timestamp formats

* updating test snapshot

* [ML] Allow Datavisualizer use on basic license (#23748)

* [ML] Allow ML use on basic license

* removing timeout change

* adding permission checks

* updating tests

* removing unnecessary checks

* [ML] Adds new page for choosing file or index based data visualizer (#23763)

* [ML] Adding license check to datavisualizer landing page (#23809)

* [ML] Adding license check to datavisualizer landing page

* removing comments

* updating redirect to landing page

* [ML] Adding ability to upload data to elasticsearch from datavisualizer  (#24042)

* [ML] Initial work for delimited file upload

* adding results links cards

* adding nav menu

* removing accidental debugger

* initial work for importing semi structured text

* using ingest pipeline for import

* adding json importer and better error reporting

* better progress steps

* time range added to results links

* first import only creates index and pipeline

* adding status constants

* using status constants

* adding explanation comment

* updating yarn.lock

* changes based on review

* fixing space

* fixing space again, stort it out git

* removing oversized background container causing constant scrollbar

* [ML] Adding basic license check when loading privileges (#24173)

* [ML] Adding basic license check

* missing import

* [ML] Adds an About panel to the file data visualizer landing page (#24260)

* [ML] Adds an About panel to the file data visualizer landing page

* [ML] Remove unnecessary style from file data visualizer scss

* [ML] Adding better error reporting for reading and importing data (#24269)

* [ML] Adding better error reporting for reading and importing data

* changes to endpoint errors

* displaying errors

* step logic refactor

* removing log statements

* [ML] Switch file data visualizer to use Papa Parse for CSV parsing (#24329)

* [ML] Fixes layout of Data Visualizer selector page for IE (#24387)

* [ML] Adding ability to override various settings when importing data (#24346)

* [ML] Adding ability to override various settings when importing data

* second commit with most of the outstanding code

* improving index pattern name validation

* better index pattern matching

* adding comments

* adding empty index pattern check

* changes based on review

* fixing test
This commit is contained in:
James Gowdy 2018-10-23 20:58:05 +01:00 committed by GitHub
parent b981546290
commit 25d35fac27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
117 changed files with 5290 additions and 137 deletions

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const MAX_BYTES = 104857600;

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const LICENSE_TYPE = {
BASIC: 0,
FULL: 1,
};

View file

@ -24,6 +24,7 @@ import { filtersRoutes } from './server/routes/filters';
import { resultsServiceRoutes } from './server/routes/results_service';
import { jobServiceRoutes } from './server/routes/job_service';
import { jobAuditMessagesRoutes } from './server/routes/job_audit_messages';
import { fileDataVisualizerRoutes } from './server/routes/file_data_visualizer';
export const ml = (kibana) => {
return new kibana.Plugin({
@ -40,8 +41,9 @@ export const ml = (kibana) => {
euiIconType: 'machineLearningApp',
main: 'plugins/ml/app',
},
styleSheetPaths: `${__dirname}/public/index.scss`,
hacks: ['plugins/ml/hacks/toggle_app_link_in_nav'],
home: ['plugins/ml/register_feature']
home: ['plugins/ml/register_feature'],
},
@ -73,7 +75,7 @@ export const ml = (kibana) => {
const config = server.config();
return {
kbnIndex: config.get('kibana.index'),
esServerUrl: config.get('elasticsearch.url')
esServerUrl: config.get('elasticsearch.url'),
};
});
@ -91,6 +93,7 @@ export const ml = (kibana) => {
resultsServiceRoutes(server, commonRouteConfig);
jobServiceRoutes(server, commonRouteConfig);
jobAuditMessagesRoutes(server, commonRouteConfig);
fileDataVisualizerRoutes(server, commonRouteConfig);
}
});

View file

@ -32,6 +32,7 @@ import 'plugins/ml/components/confirm_modal';
import 'plugins/ml/components/nav_menu';
import 'plugins/ml/components/loading_indicator';
import 'plugins/ml/settings';
import 'plugins/ml/file_datavisualizer';
import uiRoutes from 'ui/routes';

View file

@ -0,0 +1 @@
@import 'nav_menu'

View file

@ -0,0 +1,4 @@
.disabled-nav-link {
color: $euiColorMediumShade;
pointer-events: none;
}

View file

@ -15,17 +15,25 @@
<div data-transclude-slot="bottomRow">
<div ng-if="showTabs" class="kuiLocalTabs" role="tablist">
<a kbn-href="#/jobs" class="kuiLocalTab" role="tab"
ng-class="{'kuiLocalTab-isSelected': isActiveTab('jobs')}">
Job Management</a>
ng-class="{'kuiLocalTab-isSelected': isActiveTab('jobs'), 'disabled-nav-link': disableLinks}">
Job Management
</a>
<a kbn-href="#/explorer" class="kuiLocalTab" role="tab"
ng-class="{'kuiLocalTab-isSelected': isActiveTab('explorer')}">
Anomaly Explorer</a>
ng-class="{'kuiLocalTab-isSelected': isActiveTab('explorer'), 'disabled-nav-link': disableLinks}">
Anomaly Explorer
</a>
<a kbn-href="#/timeseriesexplorer" class="kuiLocalTab" role="tab"
ng-class="{'kuiLocalTab-isSelected': isActiveTab('timeseriesexplorer')}">
Single Metric Viewer</a>
ng-class="{'kuiLocalTab-isSelected': isActiveTab('timeseriesexplorer'), 'disabled-nav-link': disableLinks}">
Single Metric Viewer
</a>
<a kbn-href="#/datavisualizer" class="kuiLocalTab" role="tab"
ng-class="{'kuiLocalTab-isSelected': isActiveTab('datavisualizer')}">
Data Visualizer
</a>
<a kbn-href="#/settings" class="kuiLocalTab" role="tab"
ng-class="{'kuiLocalTab-isSelected': isActiveTab('settings')}">
Settings</a>
ng-class="{'kuiLocalTab-isSelected': isActiveTab('settings'), 'disabled-nav-link': disableLinks}">
Settings
</a>
</div>
</div>
</div>

View file

@ -10,6 +10,7 @@ import _ from 'lodash';
import $ from 'jquery';
import template from './nav_menu.html';
import uiRouter from 'ui/routes';
import { isFullLicense } from '../../license/check_license';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
@ -21,12 +22,16 @@ module.directive('mlNavMenu', function (breadcrumbState, config) {
template,
link: function (scope, el, attrs) {
// Tabs
scope.name = attrs.name;
scope.showTabs = false;
if (scope.name === 'jobs' ||
scope.name === 'settings' ||
scope.name === 'datavisualizer' ||
scope.name === 'filedatavisualizer' ||
scope.name === 'timeseriesexplorer' ||
scope.name === 'explorer') {
scope.showTabs = true;
@ -35,6 +40,8 @@ module.directive('mlNavMenu', function (breadcrumbState, config) {
return scope.name === path;
};
scope.disableLinks = (isFullLicense() === false);
// Breadcrumbs
const crumbNames = {
jobs: { label: 'Job Management', url: '#/jobs' },
@ -44,6 +51,7 @@ module.directive('mlNavMenu', function (breadcrumbState, config) {
population: { label: 'Population job', url: '' },
advanced: { label: 'Advanced Job Configuration', url: '' },
datavisualizer: { label: 'Data Visualizer', url: '' },
filedatavisualizer: { label: 'File Data Visualizer', url: '' },
explorer: { label: 'Anomaly Explorer', url: '#/explorer' },
timeseriesexplorer: { label: 'Single Metric Viewer', url: '#/timeseriesexplorer' },
settings: { label: 'Settings', url: '#/settings' },

View file

@ -0,0 +1 @@
@import './selector/index';

View file

@ -11,7 +11,10 @@
</div>
</div>
<div class="main-container">
<div
class="main-container"
ng-class="{'no-sidebar': showSidebar===false}"
>
<div class="kuiPanel kuiVerticalRhythm datavisualizer-panel">
@ -170,7 +173,7 @@
</div>
<div class="datavisualizer-sidebar">
<div ng-if="showSidebar" class="datavisualizer-sidebar">
<ng-include src="urlBasePath+'/plugins/ml/datavisualizer/datavisualizer_sidebar.html'"></ng-include>
</div>

View file

@ -24,7 +24,7 @@ import { decorateQuery, luceneStringToDsl } from 'ui/courier';
import { ML_JOB_FIELD_TYPES, KBN_FIELD_TYPES } from 'plugins/ml/../common/constants/field_types';
import { kbnTypeToMLJobType } from 'plugins/ml/util/field_types_utils';
import { IntervalHelperProvider } from 'plugins/ml/util/ml_time_buckets';
import { checkLicenseExpired } from 'plugins/ml/license/check_license';
import { checkBasicLicense, isFullLicense } from 'plugins/ml/license/check_license';
import { checkGetJobsPrivilege } from 'plugins/ml/privilege/check_privilege';
import { createSearchItems } from 'plugins/ml/jobs/new_job/utils/new_job_utils';
import { loadCurrentIndexPattern, loadCurrentSavedSearch, timeBasedIndexCheck } from 'plugins/ml/util/index_utils';
@ -37,7 +37,7 @@ uiRoutes
.when('/jobs/new_job/datavisualizer', {
template,
resolve: {
CheckLicense: checkLicenseExpired,
CheckLicense: checkBasicLicense,
privileges: checkGetJobsPrivilege,
indexPattern: loadCurrentIndexPattern,
savedSearch: loadCurrentSavedSearch,
@ -93,6 +93,8 @@ module
$scope.fieldFilter = '';
$scope.recognizerResults = { count: 0 };
$scope.showSidebar = isFullLicense();
// Check for a saved query in the AppState or via a savedSearchId in the URL.
$scope.searchQueryText = '';
if (_.has($scope.appState, 'query')) {

View file

@ -7,6 +7,7 @@
import './styles/main.less';
import './selector';
import './datavisualizer_controller';
import 'plugins/ml/components/data_recognizer';
import 'plugins/ml/components/field_data_card';

View file

@ -0,0 +1 @@
@import 'selector';

View file

@ -0,0 +1,5 @@
.ml-datavisualizer-selector {
flex-grow: 1;
background-color: $euiColorLightestShade;
min-height: 100vh;
}

View file

@ -0,0 +1,124 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiButton,
EuiCard,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiPage,
EuiPageBody,
EuiSpacer,
EuiText,
EuiTitle,
} from '@elastic/eui';
import { isFullLicense } from '../../license/check_license';
function startTrialDescription() {
return (
<span>
To experience what the full Machine Learning features of a {' '}
<EuiLink
href="https://www.elastic.co/subscriptions"
target="_blank"
>
Platinum subscription
</EuiLink>{' '}
have to offer, start a 30-day trial from the license management page.
</span>
);
}
export function DatavisualizerSelector() {
const startTrialVisible = (isFullLicense() === false);
return (
<EuiPage restrictWidth={1000}>
<EuiPageBody>
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem grow={false}>
<EuiTitle size="l">
<h2>Data Visualizer</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xl" />
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem grow={false}>
<EuiText color="subdued">
The Machine Learning Data Visualizer tool helps you understand your data, by analyzing the metrics and fields in
a log file or an existing Elasticsearch index.
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="xl" />
<EuiFlexGroup justifyContent="spaceAround" gutterSize="xl">
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type="addDataApp" />}
title="Import data"
description="Visualize data from a log file. Supported for files up to 100MB in size."
betaBadgeLabel="Experimental"
betaBadgeTooltipContent="Experimental feature. We'd love to hear your feedback."
footer={
<EuiButton
target="_self"
href="#/filedatavisualizer"
>
Select file
</EuiButton>
}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type="dataVisualizer" />}
title="Pick index pattern"
description="Visualize data in an existing Elasticsearch index."
footer={
<EuiButton
target="_self"
href="#datavisualizer_index_select"
>
Select index
</EuiButton>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
{startTrialVisible === true &&
<React.Fragment>
<EuiSpacer size="xxl" />
<EuiSpacer size="xxl" />
<EuiFlexGroup justifyContent="spaceAround" gutterSize="xl">
<EuiFlexItem grow={false} style={{ width: '600px' }}>
<EuiCard
title="Start trial"
description={startTrialDescription()}
footer={
<EuiButton
target="_blank"
href="kibana#/management/elasticsearch/license_management/home"
>
Start trial
</EuiButton>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</React.Fragment>
}
</EuiPageBody>
</EuiPage>
);
}

View file

@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import 'ngreact';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { checkBasicLicense } from 'plugins/ml/license/check_license';
import { checkFindFileStructurePrivilege } from 'plugins/ml/privilege/check_privilege';
import { initPromise } from 'plugins/ml/util/promise';
import uiRoutes from 'ui/routes';
const template = `<ml-nav-menu name="datavisualizer" /><datavisualizer-selector class="ml-datavisualizer-selector"/>`;
uiRoutes
.when('/datavisualizer', {
template,
resolve: {
CheckLicense: checkBasicLicense,
privileges: checkFindFileStructurePrivilege,
initPromise: initPromise(false)
}
});
import { DatavisualizerSelector } from './datavisualizer_selector';
module.directive('datavisualizerSelector', function ($injector) {
const reactDirective = $injector.get('reactDirective');
return reactDirective(DatavisualizerSelector, undefined, { restrict: 'E' }, { });
});

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import './directive';

View file

@ -25,6 +25,9 @@
display: inline-block;
padding-right: 10px;
}
.no-sidebar {
width: 100%;
}
.datavisualizer-sidebar {
width: 290px;

View file

@ -28,7 +28,7 @@ import { initPromise } from 'plugins/ml/util/promise';
import template from './explorer.html';
import uiRoutes from 'ui/routes';
import { checkLicense } from 'plugins/ml/license/check_license';
import { checkFullLicense } from 'plugins/ml/license/check_license';
import { checkGetJobsPrivilege } from 'plugins/ml/privilege/check_privilege';
import { loadIndexPatterns, getIndexPatterns } from 'plugins/ml/util/index_utils';
import { refreshIntervalWatcher } from 'plugins/ml/util/refresh_interval_watcher';
@ -51,7 +51,7 @@ uiRoutes
.when('/explorer/?', {
template,
resolve: {
CheckLicense: checkLicense,
CheckLicense: checkFullLicense,
privileges: checkGetJobsPrivilege,
indexPatterns: loadIndexPatterns,
initPromise: initPromise(true)

View file

@ -0,0 +1,7 @@
@import 'components/index';
.file-datavisualizer-container {
padding: 20px;
background-color: $euiColorLightestShade;
min-height: calc(100vh - 70px);
}

View file

@ -0,0 +1 @@
@import 'file_datavisualizer';

View file

@ -0,0 +1,6 @@
@import 'file_datavisualizer_view/index';
@import 'results_view/index';
@import 'analysis_summary/index';
@import 'fields_stats/index';
@import 'about_panel/index';
@import 'import_summary/index';

View file

@ -0,0 +1,6 @@
.file-datavisualizer-about-panel__icon {
width: $euiSizeXL * 3;
height: $euiSizeXL * 3;
margin-left: $euiSizeXL;
margin-right: $euiSizeL;
}

View file

@ -0,0 +1 @@
@import 'about_panel'

View file

@ -0,0 +1,118 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLink,
EuiSpacer,
EuiPanel,
EuiText,
EuiTitle,
} from '@elastic/eui';
export function AboutPanel() {
return (
<EuiPanel paddingSize="l">
<EuiFlexGroup gutterSize="xl" alignItems="center">
<EuiFlexItem grow={false}>
<EuiIcon size="xxl" type="addDataApp" className="file-datavisualizer-about-panel__icon" />
</EuiFlexItem>
<EuiFlexItem>
<EuiTitle size="m">
<h3>
Visualize data from a log file
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<EuiText>
<p>
The Machine Learning Data Visualizer helps you understand the fields and metrics
in a log file as preparation for further analysis. After analyzing the data in the
file you can then choose to import the data into an elasticsearch index.
</p>
</EuiText>
<EuiSpacer size="s" />
<EuiText>
<p>
Select the file to visualize using the button at the top of the page,
and we will then attempt to analyze its structure.
</p>
</EuiText>
<EuiSpacer size="s" />
<EuiText>
<p>
The log file formats we currently support are:
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="l">
<EuiFlexItem grow={false}>
<EuiIcon size="l" type="document" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<p>
JSON
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="l">
<EuiFlexItem grow={false}>
<EuiIcon size="l" type="document" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<p>
Delimited text files such as CSV and TSV
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="s" />
<EuiFlexGroup gutterSize="l">
<EuiFlexItem grow={false}>
<EuiIcon size="l" type="document" />
</EuiFlexItem>
<EuiFlexItem>
<EuiText>
<p>
Log files consisting of semi-structured text with the timestamp in each message having a common format
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiText>
<p>
Files up to 100MB in size can be uploaded.
</p>
</EuiText>
<EuiSpacer size="s" />
<EuiText>
<p>
This is an experimental feature. For any feedback please create an issue in&nbsp;
<EuiLink
href="https://github.com/elastic/kibana/issues/new"
target="_blank"
>
GitHub
</EuiLink>.
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { AboutPanel } from './about_panel';

View file

@ -0,0 +1,11 @@
.analysis-summary-list.euiDescriptionList {
// adding overrides for title and desciption
// these have to be overridden here as they are not
// accessable as overrides in the EuiDescriptionList component
.euiDescriptionList__title {
flex-basis: 15%;
}
.euiDescriptionList__description {
flex-basis: 85%;
}
}

View file

@ -0,0 +1 @@
@import 'analysis_summary';

View file

@ -0,0 +1,91 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiTitle,
EuiSpacer,
EuiDescriptionList,
} from '@elastic/eui';
export function AnalysisSummary({ results }) {
const items = createDisplayItems(results);
return (
<React.Fragment>
<EuiTitle size="s">
<h3>Summary</h3>
</EuiTitle>
<EuiSpacer size="m" />
<EuiDescriptionList
type="column"
listItems={items}
className="analysis-summary-list"
/>
</React.Fragment>
);
}
function createDisplayItems(results) {
const items = [
{
title: 'Number of lines analyzed',
description: results.num_lines_analyzed,
},
// {
// title: 'Charset',
// description: results.charset,
// }
];
if (results.format !== undefined) {
items.push({
title: 'Format',
description: results.format,
});
if (results.format === 'delimited') {
items.push({
title: 'Delimiter',
description: results.delimiter,
});
items.push({
title: 'Has header row',
description: `${results.has_header_row}`,
});
}
}
if (results.grok_pattern !== undefined) {
items.push({
title: 'Grok pattern',
description: results.grok_pattern,
});
}
if (results.timestamp_field !== undefined) {
items.push({
title: 'Time field',
description: results.timestamp_field,
});
}
if (results.joda_timestamp_formats !== undefined) {
const s = (results.joda_timestamp_formats.length > 1) ? 's' : '';
items.push({
title: `Time format${s}`,
description: results.joda_timestamp_formats.join(', '),
});
}
return items;
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { AnalysisSummary } from './analysis_summary';

View file

@ -0,0 +1,465 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Overrides render overrides 1`] = `
<EuiForm>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Data format"
>
<EuiSuperSelect
onChange={[Function]}
options={
Array [
Object {
"inputDisplay": <span>
json
</span>,
"value": "json",
},
Object {
"inputDisplay": <span>
delimited
</span>,
"value": "delimited",
},
Object {
"inputDisplay": <span>
semi_structured_text
</span>,
"value": "semi_structured_text",
},
Object {
"inputDisplay": <span>
xml
</span>,
"value": "xml",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Timestamp format"
>
<EuiSuperSelect
onChange={[Function]}
options={
Array [
Object {
"inputDisplay": <span>
dd/MMM/YYYY:HH:mm:ss Z
</span>,
"value": "dd/MMM/YYYY:HH:mm:ss Z",
},
Object {
"inputDisplay": <span>
EEE MMM dd HH:mm zzz YYYY
</span>,
"value": "EEE MMM dd HH:mm zzz YYYY",
},
Object {
"inputDisplay": <span>
EEE MMM dd HH:mm:ss YYYY
</span>,
"value": "EEE MMM dd HH:mm:ss YYYY",
},
Object {
"inputDisplay": <span>
EEE MMM dd HH:mm:ss zzz YYYY
</span>,
"value": "EEE MMM dd HH:mm:ss zzz YYYY",
},
Object {
"inputDisplay": <span>
EEE MMM dd YYYY HH:mm zzz
</span>,
"value": "EEE MMM dd YYYY HH:mm zzz",
},
Object {
"inputDisplay": <span>
EEE MMM dd YYYY HH:mm:ss zzz
</span>,
"value": "EEE MMM dd YYYY HH:mm:ss zzz",
},
Object {
"inputDisplay": <span>
EEE, dd MMM YYYY HH:mm Z
</span>,
"value": "EEE, dd MMM YYYY HH:mm Z",
},
Object {
"inputDisplay": <span>
EEE, dd MMM YYYY HH:mm ZZ
</span>,
"value": "EEE, dd MMM YYYY HH:mm ZZ",
},
Object {
"inputDisplay": <span>
EEE, dd MMM YYYY HH:mm:ss Z
</span>,
"value": "EEE, dd MMM YYYY HH:mm:ss Z",
},
Object {
"inputDisplay": <span>
EEE, dd MMM YYYY HH:mm:ss ZZ
</span>,
"value": "EEE, dd MMM YYYY HH:mm:ss ZZ",
},
Object {
"inputDisplay": <span>
ISO8601
</span>,
"value": "ISO8601",
},
Object {
"inputDisplay": <span>
MMM dd HH:mm:ss
</span>,
"value": "MMM dd HH:mm:ss",
},
Object {
"inputDisplay": <span>
MMM dd HH:mm:ss,SSS
</span>,
"value": "MMM dd HH:mm:ss,SSS",
},
Object {
"inputDisplay": <span>
MMM dd HH:mm:ss,SSSSSS
</span>,
"value": "MMM dd HH:mm:ss,SSSSSS",
},
Object {
"inputDisplay": <span>
MMM dd HH:mm:ss,SSSSSSSSS
</span>,
"value": "MMM dd HH:mm:ss,SSSSSSSSS",
},
Object {
"inputDisplay": <span>
MMM dd HH:mm:ss.SSS
</span>,
"value": "MMM dd HH:mm:ss.SSS",
},
Object {
"inputDisplay": <span>
MMM dd HH:mm:ss.SSSSSS
</span>,
"value": "MMM dd HH:mm:ss.SSSSSS",
},
Object {
"inputDisplay": <span>
MMM dd HH:mm:ss.SSSSSSSSS
</span>,
"value": "MMM dd HH:mm:ss.SSSSSSSSS",
},
Object {
"inputDisplay": <span>
MMM dd HH:mm:ss:SSS
</span>,
"value": "MMM dd HH:mm:ss:SSS",
},
Object {
"inputDisplay": <span>
MMM dd HH:mm:ss:SSSSSS
</span>,
"value": "MMM dd HH:mm:ss:SSSSSS",
},
Object {
"inputDisplay": <span>
MMM dd HH:mm:ss:SSSSSSSSS
</span>,
"value": "MMM dd HH:mm:ss:SSSSSSSSS",
},
Object {
"inputDisplay": <span>
MMM dd YYYY HH:mm:ss
</span>,
"value": "MMM dd YYYY HH:mm:ss",
},
Object {
"inputDisplay": <span>
MMM dd, YYYY h:mm:ss a
</span>,
"value": "MMM dd, YYYY h:mm:ss a",
},
Object {
"inputDisplay": <span>
TAI64N
</span>,
"value": "TAI64N",
},
Object {
"inputDisplay": <span>
UNIX
</span>,
"value": "UNIX",
},
Object {
"inputDisplay": <span>
UNIX_MS
</span>,
"value": "UNIX_MS",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss
</span>,
"value": "YYYY-MM-dd HH:mm:ss",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss,SSS
</span>,
"value": "YYYY-MM-dd HH:mm:ss,SSS",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss,SSSSSS
</span>,
"value": "YYYY-MM-dd HH:mm:ss,SSSSSS",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss,SSSSSSSSS
</span>,
"value": "YYYY-MM-dd HH:mm:ss,SSSSSSSSS",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss.SSS
</span>,
"value": "YYYY-MM-dd HH:mm:ss.SSS",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss.SSSSSS
</span>,
"value": "YYYY-MM-dd HH:mm:ss.SSSSSS",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss.SSSSSSSSS
</span>,
"value": "YYYY-MM-dd HH:mm:ss.SSSSSSSSS",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss:SSS
</span>,
"value": "YYYY-MM-dd HH:mm:ss:SSS",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss:SSSSSS
</span>,
"value": "YYYY-MM-dd HH:mm:ss:SSSSSS",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss:SSSSSSSSS
</span>,
"value": "YYYY-MM-dd HH:mm:ss:SSSSSSSSS",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss,SSS Z
</span>,
"value": "YYYY-MM-dd HH:mm:ss,SSS Z",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss,SSSSSS Z
</span>,
"value": "YYYY-MM-dd HH:mm:ss,SSSSSS Z",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss,SSSSSSSSS Z
</span>,
"value": "YYYY-MM-dd HH:mm:ss,SSSSSSSSS Z",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss.SSS Z
</span>,
"value": "YYYY-MM-dd HH:mm:ss.SSS Z",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss.SSSSSS Z
</span>,
"value": "YYYY-MM-dd HH:mm:ss.SSSSSS Z",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss.SSSSSSSSS Z
</span>,
"value": "YYYY-MM-dd HH:mm:ss.SSSSSSSSS Z",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss:SSS Z
</span>,
"value": "YYYY-MM-dd HH:mm:ss:SSS Z",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss:SSSSSS Z
</span>,
"value": "YYYY-MM-dd HH:mm:ss:SSSSSS Z",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss:SSSSSSSSS Z
</span>,
"value": "YYYY-MM-dd HH:mm:ss:SSSSSSSSS Z",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss,SSSZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss,SSSZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss,SSSSSSZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss,SSSSSSZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss,SSSSSSSSSZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss,SSSSSSSSSZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss.SSSZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss.SSSZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss.SSSSSSZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss.SSSSSSZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss.SSSSSSSSSZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss.SSSSSSSSSZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss:SSSZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss:SSSZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss:SSSSSSZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss:SSSSSSZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss:SSSSSSSSSZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss:SSSSSSSSSZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss,SSSZZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss,SSSZZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss,SSSSSSZZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss,SSSSSSZZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss,SSSSSSSSSZZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss,SSSSSSSSSZZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss.SSSZZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss.SSSZZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss.SSSSSSZZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss.SSSSSSZZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss.SSSSSSSSSZZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss.SSSSSSSSSZZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss:SSSZZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss:SSSZZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss:SSSSSSZZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss:SSSSSSZZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ss:SSSSSSSSSZZ
</span>,
"value": "YYYY-MM-dd HH:mm:ss:SSSSSSSSSZZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ssZ
</span>,
"value": "YYYY-MM-dd HH:mm:ssZ",
},
Object {
"inputDisplay": <span>
YYYY-MM-dd HH:mm:ssZZ
</span>,
"value": "YYYY-MM-dd HH:mm:ssZZ",
},
Object {
"inputDisplay": <span>
YYYYMMddHHmmss
</span>,
"value": "YYYYMMddHHmmss",
},
]
}
/>
</EuiFormRow>
<EuiFormRow
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
label="Time field"
>
<EuiSuperSelect
onChange={[Function]}
options={Array []}
/>
</EuiFormRow>
</EuiForm>
`;

View file

@ -0,0 +1 @@
@import 'edit_flyout'

View file

@ -0,0 +1,138 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, {
Component,
} from 'react';
import {
EuiFlyout,
EuiFlyoutHeader,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
EuiButton,
EuiButtonEmpty,
} from '@elastic/eui';
import { Overrides } from './overrides';
export class EditFlyout extends Component {
constructor(props) {
super(props);
this.state = {
isFlyoutVisible: false,
};
this.applyOverrides = () => {};
}
componentDidMount() {
if (typeof this.props.setShowFunction === 'function') {
this.props.setShowFunction(this.showFlyout);
}
}
componentWillUnmount() {
if (typeof this.props.unsetShowFunction === 'function') {
this.props.unsetShowFunction();
}
}
closeFlyout = () => {
this.setState({ isFlyoutVisible: false });
}
showFlyout = () => {
this.setState({ isFlyoutVisible: true });
}
applyAndClose = () => {
this.applyOverrides();
this.closeFlyout();
}
setApplyOverrides = (applyOverrides) => {
this.applyOverrides = applyOverrides;
}
unsetApplyOverrides = () => {
this.applyOverrides = () => {};
}
render() {
const { isFlyoutVisible } = this.state;
const {
setOverrides,
overrides,
originalSettings,
fields,
} = this.props;
return (
<React.Fragment>
{ isFlyoutVisible &&
<EuiFlyout
// ownFocus
onClose={this.closeFlyout}
size="m"
>
<EuiFlyoutHeader>
<EuiTitle>
<h2>
Override settings
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<Overrides
setOverrides={setOverrides}
overrides={overrides}
originalSettings={originalSettings}
setApplyOverrides={this.setApplyOverrides}
fields={fields}
/>
{/* <EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[0]}
onTabClick={() => { }}
/> */}
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
iconType="cross"
onClick={this.closeFlyout}
flush="left"
>
Close
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={this.applyAndClose}
fill
// isDisabled={(isValidJobDetails === false) || (isValidJobCustomUrls === false)}
>
Apply
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
}
</React.Fragment>
);
}
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { EditFlyout } from './edit_flyout';

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export {
getCharsetOptions,
getFormatOptions,
getTimestampFormatOptions,
getDelimiterOptions,
getQuoteOptions,
} from './options';

View file

@ -0,0 +1,281 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const FORMAT_OPTIONS = [
'json',
'delimited',
'semi_structured_text',
'xml',
];
export const TIMESTAMP_OPTIONS = [
'dd/MMM/YYYY:HH:mm:ss Z',
'EEE MMM dd HH:mm zzz YYYY',
'EEE MMM dd HH:mm:ss YYYY',
'EEE MMM dd HH:mm:ss zzz YYYY',
'EEE MMM dd YYYY HH:mm zzz',
'EEE MMM dd YYYY HH:mm:ss zzz',
'EEE, dd MMM YYYY HH:mm Z',
'EEE, dd MMM YYYY HH:mm ZZ',
'EEE, dd MMM YYYY HH:mm:ss Z',
'EEE, dd MMM YYYY HH:mm:ss ZZ',
'ISO8601',
// 'MMM d HH:mm:ss',
// 'MMM d HH:mm:ss,SSS',
// 'MMM d HH:mm:ss,SSSSSS',
// 'MMM d HH:mm:ss,SSSSSSSSS',
// 'MMM d HH:mm:ss.SSS',
// 'MMM d HH:mm:ss.SSSSSS',
// 'MMM d HH:mm:ss.SSSSSSSSS',
// 'MMM d HH:mm:ss:SSS',
// 'MMM d HH:mm:ss:SSSSSS',
// 'MMM d HH:mm:ss:SSSSSSSSS',
// 'MMM d YYYY HH:mm:ss',
'MMM dd HH:mm:ss',
'MMM dd HH:mm:ss,SSS',
'MMM dd HH:mm:ss,SSSSSS',
'MMM dd HH:mm:ss,SSSSSSSSS',
'MMM dd HH:mm:ss.SSS',
'MMM dd HH:mm:ss.SSSSSS',
'MMM dd HH:mm:ss.SSSSSSSSS',
'MMM dd HH:mm:ss:SSS',
'MMM dd HH:mm:ss:SSSSSS',
'MMM dd HH:mm:ss:SSSSSSSSS',
'MMM dd YYYY HH:mm:ss',
'MMM dd, YYYY h:mm:ss a',
'TAI64N',
'UNIX',
'UNIX_MS',
'YYYY-MM-dd HH:mm:ss',
'YYYY-MM-dd HH:mm:ss,SSS',
'YYYY-MM-dd HH:mm:ss,SSSSSS',
'YYYY-MM-dd HH:mm:ss,SSSSSSSSS',
'YYYY-MM-dd HH:mm:ss.SSS',
'YYYY-MM-dd HH:mm:ss.SSSSSS',
'YYYY-MM-dd HH:mm:ss.SSSSSSSSS',
'YYYY-MM-dd HH:mm:ss:SSS',
'YYYY-MM-dd HH:mm:ss:SSSSSS',
'YYYY-MM-dd HH:mm:ss:SSSSSSSSS',
'YYYY-MM-dd HH:mm:ss,SSS Z',
'YYYY-MM-dd HH:mm:ss,SSSSSS Z',
'YYYY-MM-dd HH:mm:ss,SSSSSSSSS Z',
'YYYY-MM-dd HH:mm:ss.SSS Z',
'YYYY-MM-dd HH:mm:ss.SSSSSS Z',
'YYYY-MM-dd HH:mm:ss.SSSSSSSSS Z',
'YYYY-MM-dd HH:mm:ss:SSS Z',
'YYYY-MM-dd HH:mm:ss:SSSSSS Z',
'YYYY-MM-dd HH:mm:ss:SSSSSSSSS Z',
'YYYY-MM-dd HH:mm:ss,SSSZ',
'YYYY-MM-dd HH:mm:ss,SSSSSSZ',
'YYYY-MM-dd HH:mm:ss,SSSSSSSSSZ',
'YYYY-MM-dd HH:mm:ss.SSSZ',
'YYYY-MM-dd HH:mm:ss.SSSSSSZ',
'YYYY-MM-dd HH:mm:ss.SSSSSSSSSZ',
'YYYY-MM-dd HH:mm:ss:SSSZ',
'YYYY-MM-dd HH:mm:ss:SSSSSSZ',
'YYYY-MM-dd HH:mm:ss:SSSSSSSSSZ',
'YYYY-MM-dd HH:mm:ss,SSSZZ',
'YYYY-MM-dd HH:mm:ss,SSSSSSZZ',
'YYYY-MM-dd HH:mm:ss,SSSSSSSSSZZ',
'YYYY-MM-dd HH:mm:ss.SSSZZ',
'YYYY-MM-dd HH:mm:ss.SSSSSSZZ',
'YYYY-MM-dd HH:mm:ss.SSSSSSSSSZZ',
'YYYY-MM-dd HH:mm:ss:SSSZZ',
'YYYY-MM-dd HH:mm:ss:SSSSSSZZ',
'YYYY-MM-dd HH:mm:ss:SSSSSSSSSZZ',
'YYYY-MM-dd HH:mm:ssZ',
'YYYY-MM-dd HH:mm:ssZZ',
'YYYYMMddHHmmss',
];
export const DELIMITER_OPTIONS = [
'comma',
'tab',
'semicolon',
'pipe',
'space',
'other',
];
export const QUOTE_OPTIONS = [
'\'',
'"',
'`',
];
export const CHARSET_OPTIONS = [
'IBM00858',
'IBM437',
'IBM775',
'IBM850',
'IBM852',
'IBM855',
'IBM857',
'IBM862',
'IBM866',
'ISO-8859-1',
'ISO-8859-2',
'ISO-8859-4',
'ISO-8859-5',
'ISO-8859-7',
'ISO-8859-9',
'ISO-8859-13',
'ISO-8859-15',
'KOI8-R',
'KOI8-U',
'US-ASCII',
'UTF-8',
'UTF-16',
'UTF-16BE',
'UTF-16LE',
'UTF-32',
'UTF-32BE',
'UTF-32LE',
'x-UTF-32BE-BOM',
'x-UTF-32LE-BOM',
'windows-1250',
'windows-1251',
'windows-1252',
'windows-1253',
'windows-1254',
'windows-1257',
'Not available',
'x-IBM737',
'x-IBM874',
'x-UTF-16LE-BOM',
'Big5',
'Big5-HKSCS',
'EUC-JP',
'EUC-KR',
'GB18030',
'GB2312',
'GBK',
'IBM-Thai',
'IBM01140',
'IBM01141',
'IBM01142',
'IBM01143',
'IBM01144',
'IBM01145',
'IBM01146',
'IBM01147',
'IBM01148',
'IBM01149',
'IBM037',
'IBM1026',
'IBM1047',
'IBM273',
'IBM277',
'IBM278',
'IBM280',
'IBM284',
'IBM285',
'IBM297',
'IBM420',
'IBM424',
'IBM500',
'IBM860',
'IBM861',
'IBM863',
'IBM864',
'IBM865',
'IBM868',
'IBM869',
'IBM870',
'IBM871',
'IBM918',
'ISO-2022-CN',
'ISO-2022-JP',
'ISO-2022-KR',
'ISO-8859-3',
'ISO-8859-6',
'ISO-8859-8',
'JIS_X0201',
'JIS_X0212-1990',
'Shift_JIS',
'TIS-620',
'windows-1255',
'windows-1256',
'windows-1258',
'windows-31j',
'x-Big5-Solaris',
'x-euc-jp-linux',
'x-EUC-TW',
'x-eucJP-Open',
'x-IBM1006',
'x-IBM1025',
'x-IBM1046',
'x-IBM1097',
'x-IBM1098',
'x-IBM1112',
'x-IBM1122',
'x-IBM1123',
'x-IBM1124',
'x-IBM1381',
'x-IBM1383',
'x-IBM33722',
'x-IBM834',
'x-IBM856',
'x-IBM875',
'x-IBM921',
'x-IBM922',
'x-IBM930',
'x-IBM933',
'x-IBM935',
'x-IBM937',
'x-IBM939',
'x-IBM942',
'x-IBM942C',
'x-IBM943',
'x-IBM943C',
'x-IBM948',
'x-IBM949',
'x-IBM949C',
'x-IBM950',
'x-IBM964',
'x-IBM970',
'x-ISCII91',
'x-ISO2022-CN-CNS',
'x-ISO2022-CN-GB',
'x-iso-8859-11',
'x-JIS0208',
'x-JISAutoDetect',
'x-Johab',
'x-MacArabic',
'x-MacCentralEurope',
'x-MacCroatian',
'x-MacCyrillic',
'x-MacDingbat',
'x-MacGreek',
'x-MacHebrew',
'x-MacIceland',
'x-MacRoman',
'x-MacRomania',
'x-MacSymbol',
'x-MacThai',
'x-MacTurkish',
'x-MacUkraine',
'x-MS950-HKSCS',
'x-mswin-936',
'x-PCK',
'x-SJIS_0213',
'x-windows-50220',
'x-windows-50221',
'x-windows-874',
'x-windows-949',
'x-windows-950',
'x-windows-iso2022jp',
];

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
FORMAT_OPTIONS,
TIMESTAMP_OPTIONS,
DELIMITER_OPTIONS,
QUOTE_OPTIONS,
CHARSET_OPTIONS,
} from './option_lists';
function getOptions(list) {
return list.map(o => (
{
value: o,
inputDisplay: (<span>{o}</span>),
}
));
}
export function getFormatOptions() {
return getOptions(FORMAT_OPTIONS);
}
export function getTimestampFormatOptions() {
return getOptions(TIMESTAMP_OPTIONS);
}
export function getDelimiterOptions() {
return getOptions(DELIMITER_OPTIONS);
}
export function getQuoteOptions() {
return getOptions(QUOTE_OPTIONS);
}
export function getCharsetOptions() {
return getOptions(CHARSET_OPTIONS);
}

View file

@ -0,0 +1,378 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, {
Component,
} from 'react';
import {
EuiForm,
EuiFormRow,
EuiFieldText,
EuiSuperSelect,
EuiCheckbox,
EuiSpacer,
EuiTitle,
EuiTextArea,
} from '@elastic/eui';
import {
getFormatOptions,
getTimestampFormatOptions,
getDelimiterOptions,
getQuoteOptions,
// getCharsetOptions,
} from './options';
const formatOptions = getFormatOptions();
const timestampFormatOptions = getTimestampFormatOptions();
const delimiterOptions = getDelimiterOptions();
const quoteOptions = getQuoteOptions();
// const charsetOptions = getCharsetOptions();
export class Overrides extends Component {
constructor(props) {
super(props);
this.state = {};
}
static getDerivedStateFromProps(props) {
const { originalSettings } = props;
const {
charset,
format,
hasHeaderRow,
columnNames,
delimiter,
quote,
shouldTrimFields,
grokPattern,
timestampField,
timestampFormat,
} = props.overrides;
const {
delimiter: d,
customDelimiter: customD
} = convertDelimiter((delimiter === undefined) ? originalSettings.delimiter : delimiter);
const {
newColumnNames,
originalColumnNames
} = getColumnNames(columnNames, originalSettings);
return {
charset: (charset === undefined) ? originalSettings.charset : charset,
format: (format === undefined) ? originalSettings.format : format,
hasHeaderRow: (hasHeaderRow === undefined) ? originalSettings.hasHeaderRow : hasHeaderRow,
columnNames: newColumnNames,
originalColumnNames,
delimiter: d,
customDelimiter: (customD === undefined) ? '' : customD,
quote: (quote === undefined) ? originalSettings.quote : quote,
shouldTrimFields: (shouldTrimFields === undefined) ? originalSettings.shouldTrimFields : shouldTrimFields,
grokPattern: (grokPattern === undefined) ? originalSettings.grokPattern : grokPattern,
timestampFormat: (timestampFormat === undefined) ? originalSettings.timestampFormat : timestampFormat,
timestampField: (timestampField === undefined) ? originalSettings.timestampField : timestampField,
};
}
componentDidMount() {
if (typeof this.props.setApplyOverrides === 'function') {
this.props.setApplyOverrides(this.applyOverrides);
}
}
componentWillUnmount() {
if (typeof this.props.unsetApplyOverrides === 'function') {
this.props.unsetApplyOverrides();
}
}
applyOverrides = () => {
const overrides = { ...this.state };
overrides.delimiter = convertDelimiterBack(overrides);
delete overrides.customDelimiter;
delete overrides.originalColumnNames;
this.props.setOverrides(overrides);
}
onFormatChange = (format) => {
this.setState({ format });
}
onTimestampFormatChange = (timestampFormat) => {
this.setState({ timestampFormat });
}
onTimestampFieldChange = (timestampField) => {
this.setState({ timestampField });
}
onDelimiterChange = (delimiter) => {
this.setState({ delimiter });
}
onCustomDelimiterChange = (e) => {
this.setState({ customDelimiter: e.target.value });
}
onQuoteChange = (quote) => {
this.setState({ quote });
}
onHasHeaderRowChange = (e) => {
this.setState({ hasHeaderRow: e.target.checked });
}
onShouldTrimFieldsChange = (e) => {
this.setState({ shouldTrimFields: e.target.checked });
}
onCharsetChange = (charset) => {
this.setState({ charset });
}
onColumnNameChange = (e, i) => {
const columnNames = this.state.columnNames;
columnNames[i] = e.target.value;
this.setState({ columnNames });
}
grokPatternChange = (e) => {
this.setState({ grokPattern: e.target.value });
}
render() {
const { fields } = this.props;
const {
timestampFormat,
timestampField,
format,
delimiter,
customDelimiter,
quote,
hasHeaderRow,
shouldTrimFields,
// charset,
columnNames,
originalColumnNames,
grokPattern,
} = this.state;
const fieldOptions = fields.map(f => ({ value: f, inputDisplay: f }));
return (
<EuiForm>
<EuiFormRow
label="Data format"
>
<EuiSuperSelect
options={formatOptions}
valueOfSelected={format}
onChange={this.onFormatChange}
/>
</EuiFormRow>
{
(this.state.format === 'delimited') &&
<React.Fragment>
<EuiFormRow
label="Delimiter"
>
<EuiSuperSelect
options={delimiterOptions}
valueOfSelected={delimiter}
onChange={this.onDelimiterChange}
/>
</EuiFormRow>
{
(delimiter === 'other') &&
<EuiFormRow
label="Custom delimiter"
>
<EuiFieldText
value={customDelimiter}
onChange={this.onCustomDelimiterChange}
/>
</EuiFormRow>
}
<EuiFormRow
label="Quote character"
>
<EuiSuperSelect
options={quoteOptions}
valueOfSelected={quote}
onChange={this.onQuoteChange}
/>
</EuiFormRow>
<EuiFormRow>
<EuiCheckbox
id={'hasHeaderRow'}
label="Has header row"
checked={hasHeaderRow}
onChange={this.onHasHeaderRowChange}
/>
</EuiFormRow>
<EuiFormRow>
<EuiCheckbox
id={'shouldTrimFields'}
label="Should trim fields"
checked={shouldTrimFields}
onChange={this.onShouldTrimFieldsChange}
/>
</EuiFormRow>
</React.Fragment>
}
{
(this.state.format === 'semi_structured_text') &&
<React.Fragment>
<EuiFormRow
label="Grok pattern"
>
<EuiTextArea
placeholder={grokPattern}
value={grokPattern}
onChange={this.grokPatternChange}
/>
</EuiFormRow>
</React.Fragment>
}
<EuiFormRow
label="Timestamp format"
>
<EuiSuperSelect
options={timestampFormatOptions}
valueOfSelected={timestampFormat}
onChange={this.onTimestampFormatChange}
/>
</EuiFormRow>
<EuiFormRow
label="Time field"
>
<EuiSuperSelect
options={fieldOptions}
valueOfSelected={timestampField}
onChange={this.onTimestampFieldChange}
/>
</EuiFormRow>
{/* <EuiFormRow
label="Charset"
>
<EuiSuperSelect
options={charsetOptions}
valueOfSelected={charset}
onChange={this.onCharsetChange}
/>
</EuiFormRow> */}
{
(this.state.format === 'delimited') &&
<React.Fragment>
<EuiSpacer />
<EuiTitle size="s">
<h3>Edit field names</h3>
</EuiTitle>
{
originalColumnNames.map((f, i) => (
<EuiFormRow
label={f}
key={f}
>
<EuiFieldText
value={columnNames[i]}
onChange={(e) => this.onColumnNameChange(e, i)}
/>
</EuiFormRow>
))
}
</React.Fragment>
}
</EuiForm>
);
}
}
// Some delimiter characters cannot be used as items in select list.
// so show a textual description of the character instead.
function convertDelimiter(d) {
switch (d) {
case ',':
return {
delimiter: 'comma',
};
case '\t':
return {
delimiter: 'tab',
};
case ';':
return {
delimiter: 'semicolon',
};
case '|':
return {
delimiter: 'pipe',
};
case ' ':
return {
delimiter: 'space',
};
default:
return {
delimiter: 'other',
customDelimiter: d,
};
}
}
// Convert the delimiter textual descriptions back to their real characters.
function convertDelimiterBack({ delimiter, customDelimiter }) {
switch (delimiter) {
case 'comma':
return ',';
case 'tab':
return '\t';
case 'semicolon':
return ';';
case 'pipe':
return '|';
case 'space':
return ' ';
case 'other':
return customDelimiter;
default:
return undefined;
}
}
function getColumnNames(columnNames, originalSettings) {
const newColumnNames = (columnNames === undefined && originalSettings.columnNames !== undefined) ?
[...originalSettings.columnNames] : columnNames;
const originalColumnNames = (newColumnNames !== undefined) ? [...newColumnNames] : [];
return {
newColumnNames,
originalColumnNames,
};
}

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow } from 'enzyme';
import React from 'react';
import { Overrides } from './overrides';
describe('Overrides', () => {
test('render overrides', () => {
const props = {
setOverrides: () => {},
overrides: {},
defaultSettings: {},
setApplyOverrides: () => {},
fields: [],
};
const component = shallow(
<Overrides {...props} />
);
expect(component).toMatchSnapshot();
});
});

View file

@ -0,0 +1,13 @@
.card-container {
display: inline-grid;
padding: 0px 10px 10px 0px;
}
.ml-field-data-card {
height: 408px;
.card-contents {
height: 378px;
line-height: 21px;
}
}

View file

@ -0,0 +1,8 @@
.fields-stats {
padding: 10px;
}
.field {
margin-bottom: 10px;
}

View file

@ -0,0 +1,2 @@
@import 'fields_stats';
@import 'field_stats_card';

View file

@ -0,0 +1,96 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiSpacer,
} from '@elastic/eui';
import { FieldTypeIcon } from '../../../components/field_type_icon';
export function FieldStatsCard({ field }) {
const percent = Math.round(field.percent * 100) / 100;
let type = field.type;
if (type === 'double' || type === 'long') {
type = 'number';
}
return (
<React.Fragment>
<div className="card-container">
<div className="ml-field-data-card">
<div
className={`ml-field-title-bar ${type}`}
>
<FieldTypeIcon type={type} />
<div className="field-name">{field.name}</div>
</div>
<div className="card-contents">
<div className="stats">
<div className="stat">
<i className="fa fa-files-o" aria-hidden="true" /> {field.count} document{(field.count > 1) ? 's' : ''} ({percent}%)
</div>
<div className="stat">
<i className="fa fa-cubes" aria-hidden="true" /> {field.cardinality} distinct value{(field.cardinality > 1) ? 's' : ''}
</div>
{
(field.mean_value) &&
<React.Fragment>
<div>
<div className="stat min heading">min</div>
<div className="stat median heading">median</div>
<div className="stat max heading">max</div>
</div>
<div>
<div className="stat min heading">{field.min_value}</div>
<div className="stat median heading">{field.median_value}</div>
<div className="stat max heading">{field.max_value}</div>
</div>
</React.Fragment>
}
</div>
{
(field.top_hits) &&
<React.Fragment>
<EuiSpacer size="s" />
<div className="stats">
<div className="stat">top values</div>
{field.top_hits.map(({ count, value }) => {
const pcnt = Math.round(((count / field.count) * 100) * 100) / 100;
return (
<div key={value} className="top-value">
<div className="field-label">{value}&nbsp;</div>
<div className="top-value-bar-holder">
<div
className={`top-value-bar ${type}`}
style={{ width: `${pcnt}%` }}
/>
</div>
<div className="count-label">{pcnt}%</div>
</div>
);
}
)}
</div>
</React.Fragment>
}
</div>
</div>
</div>
</React.Fragment>
);
}

View file

@ -0,0 +1,98 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, {
Component,
} from 'react';
import { FieldStatsCard } from './field_stats_card';
export class FieldsStats extends Component {
constructor(props) {
super(props);
this.state = {
fields: []
};
}
componentDidMount() {
this.setState({
fields: createFields(this.props.results)
});
}
render() {
return (
<div className="fields-stats">
{
this.state.fields.map(f => (
<FieldStatsCard
field={f}
key={f.name}
/>
))
}
</div>
);
}
}
function createFields(results) {
const {
mappings,
field_stats: fieldStats,
column_names: columnNames,
num_messages_analyzed: numMessagesAnalyzed,
timestamp_field: timestampField,
} = results;
if (mappings && fieldStats) {
// if columnNames exists (i.e delimited) use it for the field list
// so we get the same order
const tempFields = (columnNames !== undefined) ? columnNames : Object.keys(fieldStats);
return tempFields.map((fName) => {
if (fieldStats[fName] !== undefined) {
const field = { name: fName };
const f = fieldStats[fName];
const m = mappings[fName];
// sometimes the timestamp field is not in the mappings, and so our
// collection of fields will be missing a time field with a type of date
if (fName === timestampField && field.type === undefined) {
field.type = 'date';
}
if (f !== undefined) {
Object.assign(field, f);
}
if (m !== undefined) {
field.type = m.type;
if (m.format !== undefined) {
field.format = m.format;
}
}
field.percent = ((field.count / numMessagesAnalyzed) * 100);
return field;
} else {
return {
name: fName,
mean_value: 0,
count: 0,
cardinality: 0,
percent: 0,
};
}
});
}
return [];
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { FieldsStats } from './fields_stats';

View file

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiTitle,
EuiSpacer,
} from '@elastic/eui';
import { MLJobEditor, EDITOR_MODE } from '../../../jobs/jobs_list/components/ml_job_editor';
export function FileContents({ data, format, numberOfLines }) {
let mode = EDITOR_MODE.TEXT;
if (format === EDITOR_MODE.JSON) {
mode = EDITOR_MODE.JSON;
}
const formattedData = limitByNumberOfLines(data, numberOfLines);
return (
<React.Fragment>
<EuiTitle size="s">
<h3>File contents</h3>
</EuiTitle>
<div>First {numberOfLines} line{(numberOfLines > 1) ? 's' : ''}</div>
<EuiSpacer size="s" />
<MLJobEditor
mode={mode}
readOnly={true}
value={formattedData}
height="200px"
syntaxChecking={false}
/>
</React.Fragment>
);
}
function limitByNumberOfLines(data, numberOfLines) {
return data.split('\n').slice(0, numberOfLines).join('\n');
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { FileContents } from './file_contents';

View file

@ -0,0 +1 @@
@import 'file_datavisualizer_view'

View file

@ -0,0 +1,315 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, {
Component,
} from 'react';
import {
EuiFilePicker,
EuiSpacer,
EuiLoadingSpinner,
EuiButton,
EuiPanel,
} from '@elastic/eui';
import { isEqual } from 'lodash';
import { ml } from '../../../services/ml_api_service';
import { AboutPanel } from '../about_panel';
import { ResultsView } from '../results_view';
import { FileCouldNotBeRead, FileTooLarge } from './file_error_callouts';
import { EditFlyout } from '../edit_flyout';
import { ImportView } from '../import_view';
import { MAX_BYTES } from '../../../../common/constants/file_datavisualizer';
import { readFile, createUrlOverrides, processResults } from './utils';
const MODE = {
READ: 0,
IMPORT: 1,
};
export class FileDataVisualizerView extends Component {
constructor(props) {
super(props);
this.state = {
files: {},
fileContents: '',
fileSize: 0,
fileTooLarge: false,
fileCouldNotBeRead: false,
serverErrorMessage: '',
loading: false,
loaded: false,
results: undefined,
mode: MODE.READ,
};
this.overrides = {};
this.previousOverrides = {};
this.originalSettings = {};
this.showEditFlyout = () => {};
}
onChange = (files) => {
this.overrides = {};
this.setState({
loading: (files.length > 0),
loaded: false,
fileContents: '',
fileSize: 0,
fileTooLarge: false,
fileCouldNotBeRead: false,
serverErrorMessage: '',
results: undefined,
}, () => {
if (files.length) {
this.loadFile(files[0]);
}
});
};
async loadFile(file) {
if (file.size < MAX_BYTES) {
try {
const fileContents = await readFile(file);
const data = fileContents.data;
this.setState({
fileContents: data,
fileSize: file.size,
});
await this.loadSettings(data);
} catch (error) {
console.error(error);
this.setState({
loaded: false,
loading: false,
fileCouldNotBeRead: true,
});
}
} else {
this.setState({
loaded: false,
loading: false,
fileTooLarge: true,
fileSize: file.size,
});
}
}
async loadSettings(data, overrides, isRetry = false) {
try {
console.log('overrides', overrides);
const { analyzeFile } = ml.fileDatavisualizer;
const resp = await analyzeFile(data, overrides);
const serverSettings = processResults(resp.results);
const serverOverrides = resp.overrides;
this.previousOverrides = this.overrides;
this.overrides = {};
if (serverSettings.format === 'xml') {
throw {
message: 'XML not currently supported'
};
}
if (serverOverrides === undefined) {
this.originalSettings = serverSettings;
} else {
Object.keys(serverOverrides).forEach((o) => {
const camelCaseO = o.replace(/_\w/g, m => m[1].toUpperCase());
this.overrides[camelCaseO] = serverOverrides[o];
});
// check to see if the settings from the server which haven't been overridden have changed.
// e.g. changing the name of the time field which is also the time field
// will cause the timestamp_field setting to change.
// if any have changed, update the originalSettings value
Object.keys(serverSettings).forEach((o) => {
const value = serverSettings[o];
if (
this.overrides[o] === undefined &&
(Array.isArray(value) && (isEqual(value, this.originalSettings[o]) === false) ||
(value !== this.originalSettings[o]))
) {
this.originalSettings[o] = value;
}
});
}
this.setState({
results: resp.results,
loaded: true,
loading: false,
fileCouldNotBeRead: isRetry,
});
} catch (error) {
console.error(error);
this.setState({
results: undefined,
loaded: false,
loading: false,
fileCouldNotBeRead: true,
serverErrorMessage: error.message,
});
// as long as the previous overrides are different to the current overrides,
// reload the results with the previous overrides
if (overrides !== undefined && isEqual(this.previousOverrides, overrides) === false) {
this.setState({
loading: true,
loaded: false,
});
this.loadSettings(data, this.previousOverrides, true);
}
}
}
setShowEditFlyoutFunction = (func) => {
this.showEditFlyout = func;
}
unsetShowEditFlyoutFunction = () => {
this.showEditFlyout = () => {};
}
setOverrides = (overrides) => {
console.log('setOverrides', overrides);
this.setState({
loading: true,
loaded: false,
}, () => {
const formattedOverrides = createUrlOverrides(overrides, this.originalSettings);
this.loadSettings(this.state.fileContents, formattedOverrides);
});
}
changeMode = (mode) => {
this.setState({ mode });
}
render() {
const {
loading,
loaded,
results,
fileContents,
fileSize,
fileTooLarge,
fileCouldNotBeRead,
serverErrorMessage,
mode,
} = this.state;
const fields = (results !== undefined && results.field_stats !== undefined) ? Object.keys(results.field_stats) : [];
return (
<React.Fragment>
{(mode === MODE.READ) &&
<React.Fragment>
<div style={{ textAlign: 'center' }} >
<EuiFilePicker
id="filePicker"
initialPromptText="Select or drag and drop a file"
onChange={files => this.onChange(files)}
/>
</div>
<EuiSpacer size="l" />
{(!loading && !loaded) &&
<React.Fragment>
<AboutPanel />
<EuiSpacer size="l" />
</React.Fragment>
}
{(loading) &&
<div style={{ textAlign: 'center' }} >
<EuiLoadingSpinner size="xl"/>
</div>
}
{(fileTooLarge) &&
<FileTooLarge
fileSize={fileSize}
maxFileSize={MAX_BYTES}
/>
}
{(fileCouldNotBeRead && loading === false) &&
<React.Fragment>
<FileCouldNotBeRead
error={serverErrorMessage}
loaded={loaded}
/>
<EuiSpacer size="l" />
</React.Fragment>
}
{(loaded) &&
<React.Fragment>
<ResultsView
results={results}
data={fileContents}
showEditFlyout={() => this.showEditFlyout()}
/>
</React.Fragment>
}
<EditFlyout
setShowFunction={this.setShowEditFlyoutFunction}
setOverrides={this.setOverrides}
originalSettings={this.originalSettings}
overrides={this.overrides}
fields={fields}
/>
{(loaded) &&
<React.Fragment>
<EuiSpacer size="m" />
<EuiPanel>
<EuiButton
onClick={() => this.changeMode(MODE.IMPORT)}
>
Import
</EuiButton>
</EuiPanel>
</React.Fragment>
}
</React.Fragment>
}
{(mode === MODE.IMPORT) &&
<React.Fragment>
<ImportView
results={results}
fileContents={fileContents}
fileSize={fileSize}
indexPatterns={this.props.indexPatterns}
/>
<EuiSpacer size="m" />
<EuiPanel>
<EuiButton
onClick={() => this.changeMode(MODE.READ)}
>
Back
</EuiButton>
</EuiPanel>
</React.Fragment>
}
</React.Fragment>
);
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiCallOut,
} from '@elastic/eui';
export function FileTooLarge({ fileSize, maxFileSize }) {
return (
<EuiCallOut
title="File size is too large"
color="danger"
iconType="cross"
>
<p>
The size of the file you selected for upload is {fileSize} which exceeds the maximum permitted size of {maxFileSize}
</p>
</EuiCallOut>
);
}
export function FileCouldNotBeRead({ error, loaded }) {
return (
<EuiCallOut
title="File could not be read"
color="danger"
iconType="cross"
>
{
(error !== undefined) &&
<p>{error}</p>
}
{
loaded &&
<p>Reverting to previous settings</p>
}
</EuiCallOut>
);
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { FileDataVisualizerView } from './file_datavisualizer_view';

View file

@ -0,0 +1,19 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export const overrideDefaults = {
timestampFormat: undefined,
timestampField: undefined,
format: undefined,
delimiter: undefined,
quote: undefined,
hasHeaderRow: undefined,
charset: undefined,
columnNames: undefined,
shouldTrimFields: undefined,
grokPattern: undefined,
};

View file

@ -0,0 +1,99 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { overrideDefaults } from './overrides';
import { isEqual } from 'lodash';
export function readFile(file) {
return new Promise((resolve, reject) => {
if (file && file.size) {
const reader = new FileReader();
reader.readAsText(file);
reader.onload = (() => {
return () => {
const data = reader.result;
if (data === '') {
reject();
} else {
resolve({ data });
}
};
})(file);
} else {
reject();
}
});
}
export function createUrlOverrides(overrides, originalSettings) {
const formattedOverrides = {};
for (const o in overrideDefaults) {
if (overrideDefaults.hasOwnProperty(o)) {
let value = overrides[o];
if (
(Array.isArray(value) && isEqual(value, originalSettings[o]) ||
(value === undefined || value === originalSettings[o]))
) {
value = '';
}
const snakeCaseO = o.replace(/([A-Z])/g, $1 => `_${$1.toLowerCase()}`);
formattedOverrides[snakeCaseO] = value;
}
}
if (formattedOverrides.format === '' && originalSettings.format === 'delimited') {
if (
formattedOverrides.should_trim_fields !== '' ||
formattedOverrides.has_header_row !== '' ||
formattedOverrides.delimiter !== '' ||
formattedOverrides.quote !== '' ||
formattedOverrides.column_names !== ''
) {
formattedOverrides.format = originalSettings.format;
}
}
if (formattedOverrides.format === '' && originalSettings.format === 'semi_structured_text') {
if (formattedOverrides.grok_pattern !== '') {
formattedOverrides.format = originalSettings.format;
}
}
if (formattedOverrides.format === 'json' || originalSettings.format === 'json') {
formattedOverrides.should_trim_fields = '';
formattedOverrides.has_header_row = '';
formattedOverrides.delimiter = '';
formattedOverrides.quote = '';
formattedOverrides.column_names = '';
}
// escape grok pattern as it can contain bad characters
if (formattedOverrides.grok_pattern !== '') {
formattedOverrides.grok_pattern = encodeURIComponent(formattedOverrides.grok_pattern);
}
return formattedOverrides;
}
export function processResults(results) {
const timestampFormat = (results.joda_timestamp_formats !== undefined && results.joda_timestamp_formats.length) ?
results.joda_timestamp_formats[0] : undefined;
return {
format: results.format,
delimiter: results.delimiter,
timestampField: results.timestamp_field,
timestampFormat,
quote: results.quote,
hasHeaderRow: results.has_header_row,
shouldTrimFields: results.should_trim_fields,
charset: results.charset,
columnNames: results.column_names,
grokPattern: results.grok_pattern,
};
}

View file

@ -0,0 +1,103 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiCallOut,
EuiAccordion,
} from '@elastic/eui';
import { IMPORT_STATUS } from '../import_progress';
export function ImportErrors({ errors, statuses }) {
return (
<EuiCallOut
title={title(statuses)}
color="danger"
iconType="cross"
>
{
errors.map((e, i) => (
<ImportError error={e} key={i} />
))
}
</EuiCallOut>
);
}
function title(statuses) {
switch (IMPORT_STATUS.FAILED) {
case statuses.readStatus:
return 'Error reading file';
case statuses.indexCreatedStatus:
return 'Error creating index';
case statuses.ingestPipelineCreatedStatus:
return 'Error creating ingest pipeline';
case statuses.uploadStatus:
return 'Error uploading data';
case statuses.indexPatternCreatedStatus:
return 'Error creating index pattern';
default:
return 'Error';
}
}
function ImportError(error, key) {
const errorObj = toString(error);
return (
<React.Fragment>
<p key={key}>
{ errorObj.msg }
</p>
{errorObj.more !== undefined &&
<EuiAccordion
id="more"
buttonContent="More"
paddingSize="m"
>
{errorObj.more}
</EuiAccordion>
}
</React.Fragment>
);
}
function toString(error) {
if (typeof error === 'string') {
return { msg: error };
}
if (typeof error === 'object') {
if (error.msg !== undefined) {
return { msg: error.msg };
} else if (error.error !== undefined) {
if (typeof error.error === 'object') {
if (error.error.msg !== undefined) {
// this will catch a bulk ingest failure
const errorObj = { msg: error.error.msg };
if (error.error.body !== undefined) {
errorObj.more = error.error.response;
}
return errorObj;
}
} else {
return { msg: error.error };
}
} else {
// last resort, just display the whole object
return { msg: JSON.stringify(error) };
}
}
return { msg: 'Unknown error' };
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ImportErrors } from './errors';

View file

@ -0,0 +1,169 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiStepsHorizontal,
EuiProgress,
EuiSpacer,
} from '@elastic/eui';
export const IMPORT_STATUS = {
INCOMPLETE: 'incomplete',
COMPLETE: 'complete',
FAILED: 'danger',
};
export function ImportProgress({ statuses }) {
const {
reading,
readStatus,
indexCreatedStatus,
ingestPipelineCreatedStatus,
indexPatternCreatedStatus,
uploadProgress,
uploadStatus,
createIndexPattern,
} = statuses;
let statusInfo = null;
let completedStep = 0;
if (reading === true && readStatus === IMPORT_STATUS.INCOMPLETE) {
completedStep = 0;
}
if (
readStatus === IMPORT_STATUS.COMPLETE &&
indexCreatedStatus === IMPORT_STATUS.INCOMPLETE &&
ingestPipelineCreatedStatus === IMPORT_STATUS.INCOMPLETE
) {
completedStep = 1;
}
if (indexCreatedStatus === IMPORT_STATUS.COMPLETE) {
completedStep = 2;
}
if (ingestPipelineCreatedStatus === IMPORT_STATUS.COMPLETE) {
completedStep = 3;
}
if (uploadStatus === IMPORT_STATUS.COMPLETE) {
completedStep = 4;
}
if (indexPatternCreatedStatus === IMPORT_STATUS.COMPLETE) {
completedStep = 5;
}
let processFileTitle = 'Process file';
let createIndexTitle = 'Create index';
let createIngestPipelineTitle = 'Create ingest pipeline';
let uploadingDataTitle = 'Upload data';
let createIndexPatternTitle = 'Create index pattern';
if (completedStep >= 0) {
processFileTitle = 'Processing file';
statusInfo = (<p>Processing file for import</p>);
}
if (completedStep >= 1) {
processFileTitle = 'File processed';
createIndexTitle = 'Creating index';
statusInfo = (<p>Creating index and ingest pipeline</p>);
}
if (completedStep >= 2) {
createIndexTitle = 'Index created';
createIngestPipelineTitle = 'Creating ingest pipeline';
statusInfo = (<p>Creating index and ingest pipeline</p>);
}
if (completedStep >= 3) {
createIngestPipelineTitle = 'Ingest pipeline created';
uploadingDataTitle = 'Uploading data';
statusInfo = (<UploadFunctionProgress progress={uploadProgress} />);
}
if (completedStep >= 4) {
uploadingDataTitle = 'Data uploaded';
if (createIndexPattern === true) {
createIndexPatternTitle = 'Creating index pattern';
statusInfo = (<p>Creating index pattern</p>);
}
}
if (completedStep >= 5) {
createIndexPatternTitle = 'Index pattern created';
statusInfo = null;
}
const steps = [
{
title: processFileTitle,
isSelected: true,
isComplete: (readStatus === IMPORT_STATUS.COMPLETE),
status: readStatus,
onClick: () => {},
},
{
title: createIndexTitle,
isSelected: (readStatus === IMPORT_STATUS.COMPLETE),
isComplete: (indexCreatedStatus === IMPORT_STATUS.COMPLETE),
status: indexCreatedStatus,
onClick: () => {},
},
{
title: createIngestPipelineTitle,
isSelected: (indexCreatedStatus === IMPORT_STATUS.COMPLETE),
isComplete: (ingestPipelineCreatedStatus === IMPORT_STATUS.COMPLETE),
status: ingestPipelineCreatedStatus,
onClick: () => {},
},
{
title: uploadingDataTitle,
isSelected: (indexCreatedStatus === IMPORT_STATUS.COMPLETE && ingestPipelineCreatedStatus === IMPORT_STATUS.COMPLETE),
isComplete: (uploadStatus === IMPORT_STATUS.COMPLETE),
status: uploadStatus,
onClick: () => {},
}
];
if (createIndexPattern === true) {
steps.push({
title: createIndexPatternTitle,
isSelected: (uploadStatus === IMPORT_STATUS.COMPLETE),
isComplete: (indexPatternCreatedStatus === IMPORT_STATUS.COMPLETE),
status: indexPatternCreatedStatus,
onClick: () => {},
});
}
return (
<React.Fragment>
<EuiStepsHorizontal
steps={steps}
style={{ backgroundColor: 'transparent' }}
/>
{ statusInfo &&
<React.Fragment>
<EuiSpacer size="m" />
{ statusInfo }
</React.Fragment>
}
</React.Fragment>
);
}
function UploadFunctionProgress({ progress }) {
return (
<React.Fragment>
<p>Uploading data</p>
{(progress < 100) &&
<React.Fragment>
<EuiSpacer size="s" />
<EuiProgress value={progress} max={100} color="primary" size="s" />
</React.Fragment>
}
</React.Fragment>
);
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ImportProgress, IMPORT_STATUS } from './import_progress';

View file

@ -0,0 +1,173 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiFieldText,
EuiSpacer,
EuiFormRow,
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { MLJobEditor, EDITOR_MODE } from '../../../jobs/jobs_list/components/ml_job_editor';
const EDITOR_HEIGHT = '300px';
export function AdvancedSettings({
index,
indexPattern,
initialized,
onIndexChange,
createIndexPattern,
onCreateIndexPatternChange,
onIndexPatternChange,
indexSettingsString,
mappingsString,
pipelineString,
onIndexSettingsStringChange,
onMappingsStringChange,
onPipelineStringChange,
indexNameError,
indexPatternNameError,
}) {
return (
<React.Fragment>
<EuiFormRow
label="Index name"
isInvalid={indexNameError !== ''}
error={[indexNameError]}
>
<EuiFieldText
placeholder="index name"
value={index}
disabled={(initialized === true)}
onChange={onIndexChange}
isInvalid={indexNameError !== ''}
/>
</EuiFormRow>
<EuiCheckbox
id="createIndexPattern"
label="Create index pattern"
checked={(createIndexPattern === true)}
disabled={(initialized === true)}
onChange={onCreateIndexPatternChange}
/>
<EuiSpacer size="s" />
<EuiFormRow
label="Index pattern name"
disabled={(createIndexPattern === false || initialized === true)}
isInvalid={indexPatternNameError !== ''}
error={[indexPatternNameError]}
>
<EuiFieldText
disabled={(createIndexPattern === false || initialized === true)}
placeholder={(createIndexPattern === true) ? index : ''}
value={indexPattern}
onChange={onIndexPatternChange}
isInvalid={indexPatternNameError !== ''}
/>
</EuiFormRow>
<EuiFlexGroup>
<EuiFlexItem>
<IndexSettings
initialized={initialized}
data={indexSettingsString}
onChange={onIndexSettingsStringChange}
/>
</EuiFlexItem>
<EuiFlexItem>
<Mappings
initialized={initialized}
data={mappingsString}
onChange={onMappingsStringChange}
/>
</EuiFlexItem>
<EuiFlexItem>
<IngestPipeline
initialized={initialized}
data={pipelineString}
onChange={onPipelineStringChange}
/>
</EuiFlexItem>
</EuiFlexGroup>
</React.Fragment>
);
}
function IndexSettings({ initialized, data, onChange }) {
return (
<React.Fragment>
<EuiFormRow
label="Index settings"
disabled={(initialized === true)}
fullWidth
>
<MLJobEditor
mode={EDITOR_MODE.JSON}
readOnly={(initialized === true)}
value={data}
height={EDITOR_HEIGHT}
syntaxChecking={false}
onChange={onChange}
/>
</EuiFormRow>
</React.Fragment>
);
}
function Mappings({ initialized, data, onChange }) {
return (
<React.Fragment>
<EuiFormRow
label="Mappings"
disabled={(initialized === true)}
fullWidth
>
<MLJobEditor
mode={EDITOR_MODE.JSON}
readOnly={(initialized === true)}
value={data}
height={EDITOR_HEIGHT}
syntaxChecking={false}
onChange={onChange}
/>
</EuiFormRow>
</React.Fragment>
);
}
function IngestPipeline({ initialized, data, onChange }) {
return (
<React.Fragment>
<EuiFormRow
label="Ingest pipeline"
disabled={(initialized === true)}
fullWidth
>
<MLJobEditor
mode={EDITOR_MODE.JSON}
readOnly={(initialized === true)}
value={data}
height={EDITOR_HEIGHT}
syntaxChecking={false}
onChange={onChange}
/>
</EuiFormRow>
</React.Fragment>
);
}

View file

@ -0,0 +1,93 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiTabbedContent,
EuiSpacer,
} from '@elastic/eui';
import { SimpleSettings } from './simple';
import { AdvancedSettings } from './advanced';
export function ImportSettings({
index,
indexPattern,
initialized,
onIndexChange,
createIndexPattern,
onCreateIndexPatternChange,
onIndexPatternChange,
indexSettingsString,
mappingsString,
pipelineString,
onIndexSettingsStringChange,
onMappingsStringChange,
onPipelineStringChange,
indexNameError,
indexPatternNameError
}) {
const tabs = [{
id: 'simple-settings',
name: 'Simple',
content: (
<React.Fragment>
<EuiSpacer size="m" />
<SimpleSettings
index={index}
initialized={initialized}
onIndexChange={onIndexChange}
createIndexPattern={createIndexPattern}
onCreateIndexPatternChange={onCreateIndexPatternChange}
indexNameError={indexNameError}
/>
</React.Fragment>
)
},
{
id: 'advanced-settings',
name: 'Advanced',
content: (
<React.Fragment>
<EuiSpacer size="m" />
<AdvancedSettings
index={index}
indexPattern={indexPattern}
initialized={initialized}
onIndexChange={onIndexChange}
createIndexPattern={createIndexPattern}
onCreateIndexPatternChange={onCreateIndexPatternChange}
onIndexPatternChange={onIndexPatternChange}
indexSettingsString={indexSettingsString}
mappingsString={mappingsString}
pipelineString={pipelineString}
onIndexSettingsStringChange={onIndexSettingsStringChange}
onMappingsStringChange={onMappingsStringChange}
onPipelineStringChange={onPipelineStringChange}
indexNameError={indexNameError}
indexPatternNameError={indexPatternNameError}
/>
</React.Fragment>
)
}
];
return (
<React.Fragment>
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[0]}
onTabClick={() => { }}
/>
</React.Fragment>
);
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ImportSettings } from './import_settings';

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiFieldText,
EuiFormRow,
EuiCheckbox,
} from '@elastic/eui';
export function SimpleSettings({
index,
initialized,
onIndexChange,
createIndexPattern,
onCreateIndexPatternChange,
indexNameError,
}) {
return (
<React.Fragment>
<EuiFormRow
label="Index name"
isInvalid={indexNameError !== ''}
error={[indexNameError]}
>
<EuiFieldText
placeholder="index name"
value={index}
disabled={(initialized === true)}
onChange={onIndexChange}
isInvalid={indexNameError !== ''}
/>
</EuiFormRow>
<EuiCheckbox
id="createIndexPattern"
label="Create index pattern"
checked={(createIndexPattern === true)}
disabled={(initialized === true)}
onChange={onCreateIndexPatternChange}
/>
</React.Fragment>
);
}

View file

@ -0,0 +1,20 @@
.import-summary-list.euiDescriptionList {
// adding overrides for title and desciption
// these have to be overridden here as they are not
// accessable as overrides in the EuiDescriptionList component
.euiDescriptionList__title {
flex-basis: 15%;
}
.euiDescriptionList__description {
flex-basis: 85%;
}
}
.failure-list {
max-height: 200px;
overflow-y: auto;
.error-message {
color: $euiColorDanger;
}
}

View file

@ -0,0 +1 @@
@import 'import_sumary'

View file

@ -0,0 +1,122 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiSpacer,
EuiDescriptionList,
EuiCallOut,
EuiAccordion,
} from '@elastic/eui';
export function ImportSummary({
index,
indexPattern,
ingestPipelineId,
docCount,
importFailures,
}) {
const items = createDisplayItems(
index,
indexPattern,
ingestPipelineId,
docCount,
importFailures
);
return (
<React.Fragment>
<EuiCallOut
title="Import complete"
color="success"
iconType="check"
>
<EuiDescriptionList
type="column"
listItems={items}
className="import-summary-list"
/>
</EuiCallOut>
{(importFailures && importFailures.length > 0) &&
<React.Fragment>
<EuiSpacer size="m" />
<EuiCallOut
title="Some documents could not be imported"
color="warning"
iconType="help"
>
<p>
{importFailures.length} out of {docCount} documents could not be imported.
This could be due to lines not matching the Grok pattern.
</p>
<Failures failedDocs={importFailures} />
</EuiCallOut>
</React.Fragment>
}
</React.Fragment>
);
}
function Failures({ failedDocs }) {
return (
<EuiAccordion
id="failureList"
buttonContent="Failed documents"
paddingSize="m"
>
<div className="failure-list">
{
failedDocs.map(({ item, reason, doc }) => (
<div key={item}>
<div className="error-message">{item}: {reason}</div>
<div>{JSON.stringify(doc)}</div>
</div>
))
}
</div>
</EuiAccordion>
);
}
function createDisplayItems(
index,
indexPattern,
ingestPipelineId,
docCount,
importFailures
) {
const items = [
{
title: 'Index',
description: index,
},
{
title: 'Index pattern',
description: indexPattern,
},
{
title: 'Ingest pipeline',
description: ingestPipelineId,
},
{
title: 'Documents ingested',
description: docCount - ((importFailures && importFailures.length) || 0),
}
];
if (importFailures && importFailures.length > 0) {
items.push({
title: 'Failed documents',
description: importFailures.length,
});
}
return items;
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ImportSummary } from './import_summary';

View file

@ -0,0 +1,500 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, {
Component,
} from 'react';
import {
EuiButton,
EuiSpacer,
EuiPanel,
EuiTitle,
} from '@elastic/eui';
import { importerFactory } from './importer';
import { ResultsLinks } from '../results_links';
import { ImportProgress, IMPORT_STATUS } from '../import_progress';
import { ImportErrors } from '../import_errors';
import { ImportSummary } from '../import_summary';
import { ImportSettings } from '../import_settings';
import { getIndexPatternNames, refreshIndexPatterns } from '../../../util/index_utils';
import { ml } from '../../../services/ml_api_service';
const DEFAULT_TIME_FIELD = '@timestamp';
const CONFIG_MODE = { SIMPLE: 0, ADVANCED: 1 };
const DEFAULT_STATE = {
index: '',
importing: false,
imported: false,
initialized: false,
reading: false,
readProgress: 0,
readStatus: IMPORT_STATUS.INCOMPLETE,
indexCreatedStatus: IMPORT_STATUS.INCOMPLETE,
indexPatternCreatedStatus: IMPORT_STATUS.INCOMPLETE,
ingestPipelineCreatedStatus: IMPORT_STATUS.INCOMPLETE,
uploadProgress: 0,
uploadStatus: IMPORT_STATUS.INCOMPLETE,
createIndexPattern: true,
indexPattern: '',
indexPatternId: '',
ingestPipelineId: '',
errors: [],
importFailures: [],
docCount: 0,
configMode: CONFIG_MODE.SIMPLE,
indexSettingsString: '',
mappingsString: '',
pipelineString: '',
indexNames: [],
indexPatternNames: [],
indexNameError: '',
indexPatternNameError: '',
};
export class ImportView extends Component {
constructor(props) {
super(props);
this.state = getDefaultState(DEFAULT_STATE, this.props.results);
}
componentDidMount() {
this.loadIndexNames();
this.loadIndexPatternNames();
}
clickReset = () => {
const state = getDefaultState(this.state, this.props.results);
this.setState(state, () => {
this.loadIndexNames();
this.loadIndexPatternNames();
});
}
clickImport = () => {
this.import();
}
// TODO - sort this function out. it's a mess
async import() {
const { fileContents, results } = this.props;
const { format } = results;
const {
index,
indexPattern,
createIndexPattern,
indexSettingsString,
mappingsString,
pipelineString,
} = this.state;
const errors = [];
if (index !== '') {
this.setState({
importing: true,
imported: false,
reading: true,
initialized: true,
}, () => {
setTimeout(async () => {
let success = false;
let indexCreationSettings = {};
try {
indexCreationSettings = {
settings: JSON.parse(indexSettingsString),
mappings: JSON.parse(mappingsString),
pipeline: JSON.parse(pipelineString),
};
success = true;
} catch (error) {
success = false;
errors.push(error);
}
if (success) {
const importer = importerFactory(format, results, indexCreationSettings);
if (importer !== undefined) {
const readResp = await importer.read(fileContents, this.setReadProgress);
success = readResp.success;
this.setState({
readStatus: success ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
reading: false,
});
if (readResp.success === false) {
console.error(readResp.error);
errors.push(readResp.error);
}
if (success) {
const initializeImportResp = await importer.initializeImport(index);
const indexCreated = (initializeImportResp.index !== undefined);
this.setState({
indexCreatedStatus: indexCreated ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
});
const pipelineCreated = (initializeImportResp.pipelineId !== undefined);
if (indexCreated) {
this.setState({
ingestPipelineCreatedStatus: pipelineCreated ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
ingestPipelineId: pipelineCreated ? initializeImportResp.pipelineId : '',
});
}
success = (indexCreated && pipelineCreated);
if (success) {
const importId = initializeImportResp.id;
const pipelineId = initializeImportResp.pipelineId;
const importResp = await importer.import(importId, index, pipelineId, this.setImportProgress);
success = importResp.success;
this.setState({
uploadStatus: importResp.success ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
importFailures: importResp.failures,
docCount: importResp.docCount,
});
if (success && createIndexPattern) {
const indexPatternName = (indexPattern === '') ? index : indexPattern;
const indexPatternResp = await createKibanaIndexPattern(indexPatternName, this.props.indexPatterns);
success = indexPatternResp.success;
this.setState({
indexPatternCreatedStatus: indexPatternResp.success ? IMPORT_STATUS.COMPLETE : IMPORT_STATUS.FAILED,
indexPatternId: indexPatternResp.id,
});
if (indexPatternResp.success === false) {
errors.push(indexPatternResp.error);
}
} else {
errors.push(importResp.error);
}
} else {
errors.push(initializeImportResp.error);
}
}
}
}
this.setState({
importing: false,
imported: success,
errors,
});
}, 500);
});
}
}
onConfigModeChange = (configMode) => {
this.setState({
configMode,
});
}
onIndexChange = (e) => {
const name = e.target.value;
this.setState({
index: name,
indexNameError: isIndexNameValid(name, this.state.indexNames),
});
}
onIndexPatternChange = (e) => {
const name = e.target.value;
const { indexPatternNames, index } = this.state;
this.setState({
indexPattern: name,
indexPatternNameError: isIndexPatternNameValid(name, indexPatternNames, index),
});
}
onCreateIndexPatternChange = (e) => {
this.setState({
createIndexPattern: e.target.checked,
});
}
onIndexSettingsStringChange = (text) => {
this.setState({
indexSettingsString: text,
});
}
onMappingsStringChange = (text) => {
this.setState({
mappingsString: text,
});
}
onPipelineStringChange = (text) => {
this.setState({
pipelineString: text,
});
}
setImportProgress = (progress) => {
this.setState({
uploadProgress: progress,
});
}
setReadProgress = (progress) => {
this.setState({
readProgress: progress,
});
}
async loadIndexNames() {
const indices = await ml.getIndices();
const indexNames = indices.map(i => i.name);
this.setState({ indexNames });
}
async loadIndexPatternNames() {
await refreshIndexPatterns();
const indexPatternNames = getIndexPatternNames();
this.setState({ indexPatternNames });
}
render() {
const {
index,
indexPattern,
indexPatternId,
ingestPipelineId,
importing,
imported,
reading,
initialized,
readStatus,
indexCreatedStatus,
ingestPipelineCreatedStatus,
indexPatternCreatedStatus,
uploadProgress,
uploadStatus,
createIndexPattern,
errors,
docCount,
importFailures,
indexSettingsString,
mappingsString,
pipelineString,
indexNameError,
indexPatternNameError,
} = this.state;
const statuses = {
reading,
readStatus,
indexCreatedStatus,
ingestPipelineCreatedStatus,
indexPatternCreatedStatus,
uploadProgress,
uploadStatus,
createIndexPattern,
};
const disableImport = (
index === '' ||
indexNameError !== '' ||
(createIndexPattern === true && indexPatternNameError !== '') ||
initialized === true
);
return (
<React.Fragment>
<EuiPanel>
<EuiTitle size="s">
<h3>Import data</h3>
</EuiTitle>
<ImportSettings
index={index}
indexPattern={indexPattern}
initialized={initialized}
onIndexChange={this.onIndexChange}
createIndexPattern={createIndexPattern}
onCreateIndexPatternChange={this.onCreateIndexPatternChange}
onIndexPatternChange={this.onIndexPatternChange}
indexSettingsString={indexSettingsString}
mappingsString={mappingsString}
pipelineString={pipelineString}
onIndexSettingsStringChange={this.onIndexSettingsStringChange}
onMappingsStringChange={this.onMappingsStringChange}
onPipelineStringChange={this.onPipelineStringChange}
indexNameError={indexNameError}
indexPatternNameError={indexPatternNameError}
/>
<EuiSpacer size="m" />
{(initialized === false || importing === true) &&
<EuiButton
isDisabled={disableImport}
onClick={this.clickImport}
isLoading={importing}
iconSide="right"
>
Import
</EuiButton>
}
{
(initialized === true && importing === false) &&
<EuiButton
onClick={this.clickReset}
>
Reset
</EuiButton>
}
</EuiPanel>
{(initialized === true) &&
<React.Fragment>
<EuiSpacer size="m" />
<EuiPanel>
<ImportProgress statuses={statuses} />
{(imported === true) &&
<React.Fragment>
<EuiSpacer size="m" />
<ImportSummary
index={index}
indexPattern={((indexPattern === '') ? index : indexPattern)}
ingestPipelineId={ingestPipelineId}
docCount={docCount}
importFailures={importFailures}
/>
<EuiSpacer size="l" />
<ResultsLinks
index={(index)}
indexPatternId={(indexPatternId)}
timeFieldName={DEFAULT_TIME_FIELD}
/>
</React.Fragment>
}
</EuiPanel>
{
(errors.length > 0) &&
<React.Fragment>
<EuiSpacer size="m" />
<ImportErrors
errors={errors}
statuses={statuses}
/>
</React.Fragment>
}
</React.Fragment>
}
</React.Fragment>
);
}
}
async function createKibanaIndexPattern(indexPatternName, indexPatterns, timeFieldName = DEFAULT_TIME_FIELD) {
try {
const emptyPattern = await indexPatterns.get();
Object.assign(emptyPattern, {
id: '',
title: indexPatternName,
timeFieldName,
});
const id = await emptyPattern.create();
return {
success: true,
id,
};
} catch (error) {
console.error(error);
return {
success: false,
error,
};
}
}
function getDefaultState(state, results) {
const indexSettingsString = (state.indexSettingsString === '') ? '{}' : state.indexSettingsString;
const mappingsString = (state.mappingsString === '') ? JSON.stringify(results.mappings, null, 2) : state.mappingsString;
const pipelineString = (state.pipelineString === '') ? JSON.stringify(results.ingest_pipeline, null, 2) : state.pipelineString;
return {
... DEFAULT_STATE,
indexSettingsString,
mappingsString,
pipelineString,
};
}
function isIndexNameValid(name, indexNames) {
if (indexNames.find(i => i === name)) {
return 'Index name already exists';
}
const reg = new RegExp('[\\\\/\*\?\"\<\>\|\\s\,\#]+');
if (
(name !== name.toLowerCase()) || // name should be lowercase
(name === '.' || name === '..') || // name can't be . or ..
name.match(/^[-_+]/) !== null || // name can't start with these chars
name.match(reg) !== null // name can't contain these chars
) {
return 'Index name contains illegal characters';
}
return '';
}
function isIndexPatternNameValid(name, indexPatternNames, index) {
// if a blank name is entered, the index name will be used so avoid validation
if (name === '') {
return '';
}
if (indexPatternNames.find(i => i === name)) {
return 'Index pattern name already exists';
}
// escape . and + to stop the regex matching more than it should.
let newName = name.replace('.', '\\.');
newName = newName.replace('+', '\\+');
// replace * with .* to make the wildcard match work.
newName = newName.replace('*', '.*');
const reg = new RegExp(`^${newName}$`);
if (index.match(reg) === null) { // name should match index
return 'Index pattern does not match index name';
}
return '';
}

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Importer } from './importer';
import Papa from 'papaparse';
export class CsvImporter extends Importer {
constructor(results, settings) {
super(settings);
this.format = results.format;
this.delimiter = results.delimiter;
this.quote = results.quote;
this.hasHeaderRow = results.has_header_row;
this.columnNames = results.column_names;
}
async read(csv) {
try {
const config = {
header: false,
skipEmptyLines: 'greedy',
delimiter: this.delimiter,
quoteChar: this.quote,
};
const parserOutput = Papa.parse(csv, config);
if (parserOutput.errors.length) {
// throw an error with the message of the first error encountered
throw parserOutput.errors[0].message;
}
this.data = parserOutput.data;
if (this.hasHeaderRow) {
this.data.shift();
}
this.docArray = formatToJson(this.data, this.columnNames);
return {
success: true,
};
} catch (error) {
return {
success: false,
error,
};
}
}
}
function formatToJson(data, columnNames) {
const docArray = [];
for (let i = 0; i < data.length; i++) {
const line = {};
for (let c = 0; c < columnNames.length; c++) {
const col = columnNames[c];
line[col] = data[i][c];
}
docArray.push(line);
}
return docArray;
}

View file

@ -0,0 +1,149 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { ml } from '../../../../services/ml_api_service';
import { chunk } from 'lodash';
import moment from 'moment';
const CHUNK_SIZE = 10000;
const IMPORT_RETRIES = 5;
export class Importer {
constructor({ settings, mappings, pipeline }) {
this.settings = settings;
this.mappings = mappings;
this.pipeline = pipeline;
this.data = [];
this.docArray = [];
}
async initializeImport(index) {
const settings = this.settings;
const mappings = this.mappings;
const pipeline = this.pipeline;
updatePipelineTimezone(pipeline);
const ingestPipeline = {
id: `${index}-pipeline`,
pipeline,
};
const createIndexResp = await ml.fileDatavisualizer.import({
id: undefined,
index,
data: [],
settings,
mappings,
ingestPipeline
});
return createIndexResp;
}
async import(id, index, pipelineId, setImportProgress) {
if (!id || !index) {
return {
success: false,
error: 'no ID or index supplied'
};
}
const chunks = chunk(this.docArray, CHUNK_SIZE);
const ingestPipeline = {
id: pipelineId,
};
let success = true;
const failures = [];
let error;
for (let i = 0; i < chunks.length; i++) {
const aggs = {
id,
index,
data: chunks[i],
settings: {},
mappings: {},
ingestPipeline
};
let retries = IMPORT_RETRIES;
let resp = {
success: false,
failures: [],
docCount: 0,
};
while (resp.success === false && retries > 0) {
resp = await ml.fileDatavisualizer.import(aggs);
if (retries < IMPORT_RETRIES) {
console.log(`Retrying import ${IMPORT_RETRIES - retries}`);
}
retries--;
}
if (resp.success) {
setImportProgress(((i + 1) / chunks.length) * 100);
} else {
console.error(resp);
success = false;
error = resp.error;
populateFailures(resp, failures, i);
break;
}
populateFailures(resp, failures, i);
}
const result = {
success,
failures,
docCount: this.docArray.length,
};
if (success) {
setImportProgress(100);
} else {
result.error = error;
}
return result;
}
}
function populateFailures(error, failures, chunkCount) {
if (error.failures && error.failures.length) {
// update the item value to include the chunk count
// e.g. item 3 in chunk 2 is actually item 20003
for (let f = 0; f < error.failures.length; f++) {
const failure = error.failures[f];
failure.item = failure.item + (CHUNK_SIZE * chunkCount);
}
failures.push(...error.failures);
}
}
// The file structure endpoint sets the timezone to be {{ beat.timezone }}
// as that's the variable Filebeat would send the client timezone in.
// In this data import function the UI is effectively performing the role of Filebeat,
// i.e. doing basic parsing, processing and conversion to JSON before forwarding to the ingest pipeline.
// But it's not sending every single field that Filebeat would add, so the ingest pipeline
// cannot look for a beat.timezone variable in each input record.
// Therefore we need to replace {{ beat.timezone }} with the actual browser timezone
function updatePipelineTimezone(ingestPipeline) {
if (ingestPipeline !== undefined && ingestPipeline.processors && ingestPipeline.processors) {
const dateProcessor = ingestPipeline.processors.find(p => (p.date !== undefined && p.date.timezone === '{{ beat.timezone }}'));
if (dateProcessor) {
dateProcessor.date.timezone = moment.tz.guess();
}
}
}

View file

@ -0,0 +1,24 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { CsvImporter } from './csv_importer';
import { SstImporter } from './sst_importer';
import { JsonImporter } from './json_importer';
export function importerFactory(format, results, settings) {
switch (format) {
case 'delimited':
return new CsvImporter(results, settings);
case 'semi_structured_text':
return new SstImporter(results, settings);
case 'json':
return new JsonImporter(results, settings);
default:
return;
}
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { importerFactory } from './importer_factory';

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Importer } from './importer';
export class JsonImporter extends Importer {
constructor(results, settings) {
super(settings);
}
async read(json) {
try {
const splitJson = json.split(/}\s*\n/);
const ndjson = [];
for (let i = 0; i < splitJson.length; i++) {
if (splitJson[i] !== '') {
// note the extra } at the end of the line, adding back
// the one that was eaten in the split
ndjson.push(`${splitJson[i]}}`);
}
}
this.docArray = ndjson;
return {
success: true,
};
} catch (error) {
return {
success: false,
error,
};
}
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Importer } from './importer';
export class SstImporter extends Importer {
constructor(results, settings) {
super(settings);
this.format = results.format;
this.multilineStartPattern = results.multiline_start_pattern;
this.grokPattern = results.grok_pattern;
}
// convert the semi structured text string into an array of lines
// by looking over each char, looking for newlines.
// if one is found, check the next line to see if it starts with the
// multiline_start_pattern regex
// if it does, it is a legitimate end of line and can be pushed into the list,
// if not, it must be a new line char inside a field value, so keep looking.
async read(text) {
try {
const data = [];
let message = '';
let line = '';
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (char === '\n') {
if (line.match(this.multilineStartPattern) !== null) {
data.push({ message });
message = '';
} else {
message += char;
}
message += line;
line = '';
} else {
line += char;
}
}
// add the last line of the file to the list
if (message !== '') {
data.push({ message });
}
// remove first line if it is blank
if (data[0] && data[0].message === '') {
data.shift();
}
this.data = data;
this.docArray = this.data;
return {
success: true,
};
} catch (error) {
console.error(error);
return {
success: false,
error,
};
}
}
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ImportView } from './import_view';

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ResultsLinks } from './results_links';

View file

@ -0,0 +1,147 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React, {
Component,
} from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiCard,
EuiIcon,
} from '@elastic/eui';
import moment from 'moment';
import uiChrome from 'ui/chrome';
import { ml } from '../../../services/ml_api_service';
const RECHECK_DELAY_MS = 3000;
export class ResultsLinks extends Component {
constructor(props) {
super(props);
this.state = {
from: 'now-30m',
to: 'now',
};
this.recheckTimeout = null;
}
componentDidMount() {
this.updateTimeValues();
}
componentWillUnmount() {
clearTimeout(this.recheckTimeout);
}
async updateTimeValues(recheck = true) {
const {
index,
timeFieldName,
} = this.props;
const { from, to, } = await getFullTimeRange(index, timeFieldName);
this.setState({
from: (from === null) ? this.state.from : from,
to: (to === null) ? this.state.to : to,
});
// these links may have been drawn too quickly for the index to be ready
// to give us the correct start and end times.
// especially if the data was small.
// so if the start and end were null, try again in 3s
// the timeout is cleared when this component unmounts. just in case the user
// resets the form or navigates away within 3s
if (recheck && (from === null || to === null)) {
this.recheckTimeout = setTimeout(() => {
this.updateTimeValues(false);
}, RECHECK_DELAY_MS);
}
}
render() {
const {
indexPatternId,
} = this.props;
const {
from,
to,
} = this.state;
const _g = `&_g=(time:(from:'${from}',mode:quick,to:'${to}'))`;
return (
<EuiFlexGroup gutterSize="l">
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type={`discoverApp`} />}
title="View index in Discover"
description=""
href={`${uiChrome.getBasePath()}/app/kibana#/discover?&_a=(index:'${indexPatternId}')${_g}`}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type={`machineLearningApp`} />}
title="Create new ML job"
description=""
href={`${uiChrome.getBasePath()}/app/ml#/jobs/new_job/step/job_type?index=${indexPatternId}${_g}`}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type={`dataVisualizer`} />}
title="Open in Data Visualizer"
description=""
href={`${uiChrome.getBasePath()}/app/ml#/jobs/new_job/datavisualizer?index=${indexPatternId}${_g}`}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type={`managementApp`} />}
title="Index Management"
description=""
href={`${uiChrome.getBasePath()}/app/kibana#/management/elasticsearch/index_management/home`}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiCard
icon={<EuiIcon size="xxl" type={`managementApp`} />}
title="Index Pattern Management"
description=""
href={`${uiChrome.getBasePath()}/app/kibana#/management/kibana/indices/${indexPatternId}`}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
}
}
async function getFullTimeRange(index, timeFieldName) {
const query = { bool: { must: [{ query_string: { analyze_wildcard: true, query: '*' } }] } };
const resp = await ml.getTimeFieldRange({
index,
timeFieldName,
query
});
return {
from: moment(resp.start.epoch).toISOString(),
to: moment(resp.end.epoch).toISOString(),
};
}

View file

@ -0,0 +1 @@
@import 'results_view'

View file

@ -0,0 +1,10 @@
.results {
.euiDescriptionList{
dd, dt {
margin-top: 5px;
}
dd:nth-child(1), dt:nth-child(1), {
margin-top: 0px;
}
}
}

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ResultsView } from './results_view';

View file

@ -0,0 +1,69 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import {
EuiTabbedContent,
EuiButton,
EuiSpacer,
EuiPanel,
} from '@elastic/eui';
import { FileContents } from '../file_contents';
import { AnalysisSummary } from '../analysis_summary';
import { FieldsStats } from '../fields_stats';
export function ResultsView({ data, results, showEditFlyout }) {
console.log(results);
const tabs = [
{
id: 'file-stats',
name: 'File stats',
content: <FieldsStats results={results} />,
}
];
return (
<div className="results">
<EuiPanel>
<FileContents
data={data}
format={results.format}
numberOfLines={results.num_lines_analyzed}
/>
</EuiPanel>
<EuiSpacer size="m" />
<EuiPanel>
<AnalysisSummary
results={results}
/>
<EuiSpacer size="m" />
<EuiButton onClick={() => showEditFlyout()}>
Override settings
</EuiButton>
</EuiPanel>
<EuiSpacer size="m" />
<EuiPanel>
<EuiTabbedContent
tabs={tabs}
initialSelectedTab={tabs[0]}
onTabClick={() => { }}
/>
</EuiPanel>
</div>
);
}

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FileDataVisualizerView } from './components/file_datavisualizer_view';
import React from 'react';
export function FileDataVisualizerPage({ indexPatterns }) {
return (
<div className="file-datavisualizer-container">
<FileDataVisualizerView indexPatterns={indexPatterns} />
</div>
);
}

View file

@ -0,0 +1,46 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import 'ngreact';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { checkBasicLicense } from 'plugins/ml/license/check_license';
import { checkFindFileStructurePrivilege } from 'plugins/ml/privilege/check_privilege';
import { getMlNodeCount } from 'plugins/ml/ml_nodes_check/check_ml_nodes';
import { loadNewJobDefaults } from 'plugins/ml/jobs/new_job/utils/new_job_defaults';
import { loadIndexPatterns } from '../util/index_utils';
import { initPromise } from 'plugins/ml/util/promise';
import uiRoutes from 'ui/routes';
const template = '<ml-nav-menu name="filedatavisualizer" /><file-datavisualizer-page />';
uiRoutes
.when('/filedatavisualizer/?', {
template,
resolve: {
CheckLicense: checkBasicLicense,
privileges: checkFindFileStructurePrivilege,
indexPatterns: loadIndexPatterns,
mlNodeCount: getMlNodeCount,
loadNewJobDefaults,
initPromise: initPromise(true)
}
});
import { FileDataVisualizerPage } from './file_datavisualizer';
module.directive('fileDatavisualizerPage', function ($injector) {
const reactDirective = $injector.get('reactDirective');
const indexPatterns = $injector.get('indexPatterns');
return reactDirective(FileDataVisualizerPage, undefined, { restrict: 'E' }, { indexPatterns });
});

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import './file_datavisualizer_directive';

View file

@ -0,0 +1,10 @@
// Should import both the EUI constants and any Kibana ones that are considered global
@import 'ui/public/styles/styling_constants';
// ML needs EUI card styling till it fully adopts React components
@import '@elastic/eui/src/components/panel/variables';
@import '@elastic/eui/src/components/panel/mixins';
@import 'datavisualizer/index';
@import 'file_datavisualizer/index';
@import 'components/nav_menu/index';

View file

@ -5,4 +5,4 @@
*/
export { MLJobEditor } from './ml_job_editor';
export { MLJobEditor, EDITOR_MODE } from './ml_job_editor';

View file

@ -12,7 +12,17 @@ import {
EuiCodeEditor
} from '@elastic/eui';
export function MLJobEditor({ value, height = '500px', width = '100%', mode = 'json', readOnly = false, onChange = () => {} }) {
export const EDITOR_MODE = { TEXT: 'text', JSON: 'json' };
export function MLJobEditor({
value,
height = '500px',
width = '100%',
mode = EDITOR_MODE.JSON,
readOnly = false,
syntaxChecking = true,
onChange = () => {}
}) {
return (
<EuiCodeEditor
value={value}
@ -23,6 +33,7 @@ export function MLJobEditor({ value, height = '500px', width = '100%', mode = 'j
wrapEnabled={true}
showPrintMargin={false}
editorProps={{ $blockScrolling: true }}
setOptions={{ useWorker: syntaxChecking }}
onChange={onChange}
/>
);

View file

@ -13,6 +13,7 @@
line-height: 1.25;
font-weight: 300;
line-height: 2.5rem;
vertical-align: bottom;
}
.actions-border, .actions-border-large {

View file

@ -11,7 +11,7 @@ import React from 'react';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { checkLicense } from 'plugins/ml/license/check_license';
import { checkFullLicense } from 'plugins/ml/license/check_license';
import { checkGetJobsPrivilege } from 'plugins/ml/privilege/check_privilege';
import { getMlNodeCount } from 'plugins/ml/ml_nodes_check/check_ml_nodes';
import { loadNewJobDefaults } from 'plugins/ml/jobs/new_job/utils/new_job_defaults';
@ -25,7 +25,7 @@ uiRoutes
.when('/jobs/?', {
template,
resolve: {
CheckLicense: checkLicense,
CheckLicense: checkFullLicense,
privileges: checkGetJobsPrivilege,
mlNodeCount: getMlNodeCount,
loadNewJobDefaults,

View file

@ -20,7 +20,7 @@ import { copyTextToClipboard } from 'plugins/ml/util/clipboard_utils';
import { timefilter } from 'ui/timefilter';
import uiRoutes from 'ui/routes';
import { checkLicense } from 'plugins/ml/license/check_license';
import { checkFullLicense } from 'plugins/ml/license/check_license';
import { checkGetJobsPrivilege, checkPermission, createPermissionFailureMessage } from 'plugins/ml/privilege/check_privilege';
import { addItemToRecentlyAccessed } from 'plugins/ml/util/recently_accessed';
import { getMlNodeCount, mlNodesAvailable, permissionToViewMlNodeCount } from 'plugins/ml/ml_nodes_check/check_ml_nodes';
@ -43,7 +43,7 @@ uiRoutes
.when('/jobs_old/?', {
template,
resolve: {
CheckLicense: checkLicense,
CheckLicense: checkFullLicense,
privileges: checkGetJobsPrivilege,
mlNodeCount: getMlNodeCount,
loadNewJobDefaults,

View file

@ -14,7 +14,7 @@ import { parseInterval } from 'ui/utils/parse_interval';
import { timefilter } from 'ui/timefilter';
import uiRoutes from 'ui/routes';
import { checkLicense } from 'plugins/ml/license/check_license';
import { checkFullLicense } from 'plugins/ml/license/check_license';
import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege';
import template from './new_job.html';
import saveStatusTemplate from 'plugins/ml/jobs/new_job/advanced/save_status_modal/save_status_modal.html';
@ -38,7 +38,7 @@ uiRoutes
.when('/jobs/new_job/advanced', {
template,
resolve: {
CheckLicense: checkLicense,
CheckLicense: checkFullLicense,
privileges: checkCreateJobsPrivilege,
indexPattern: loadCurrentIndexPattern,
indexPatterns: loadIndexPatterns,
@ -51,7 +51,7 @@ uiRoutes
.when('/jobs/new_job/advanced/:jobId', {
template,
resolve: {
CheckLicense: checkLicense,
CheckLicense: checkFullLicense,
privileges: checkCreateJobsPrivilege,
indexPattern: loadCurrentIndexPattern,
indexPatterns: loadIndexPatterns,

View file

@ -12,9 +12,9 @@
*/
import uiRoutes from 'ui/routes';
import { checkLicenseExpired } from 'plugins/ml/license/check_license';
import { checkLicenseExpired, checkBasicLicense } from 'plugins/ml/license/check_license';
import { preConfiguredJobRedirect } from 'plugins/ml/jobs/new_job/wizard/preconfigured_job_redirect';
import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege';
import { checkCreateJobsPrivilege, checkFindFileStructurePrivilege } from 'plugins/ml/privilege/check_privilege';
import { loadIndexPatterns, getIndexPatterns } from 'plugins/ml/util/index_utils';
import { checkMlNodesAvailable } from 'plugins/ml/ml_nodes_check/check_ml_nodes';
import { initPromise } from 'plugins/ml/util/promise';
@ -35,7 +35,20 @@ uiRoutes
indexPatterns: loadIndexPatterns,
preConfiguredJobRedirect,
checkMlNodesAvailable,
initPromise: initPromise(true)
initPromise: initPromise(true),
nextStepPath: () => '#/jobs/new_job/step/job_type',
}
});
uiRoutes
.when('/datavisualizer_index_select', {
template,
resolve: {
CheckLicense: checkBasicLicense,
privileges: checkFindFileStructurePrivilege,
indexPatterns: loadIndexPatterns,
initPromise: initPromise(true),
nextStepPath: () => '#jobs/new_job/datavisualizer',
}
});
@ -43,27 +56,26 @@ import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
module.controller('MlNewJobStepIndexOrSearch',
function ($scope) {
function ($scope, $route) {
timefilter.disableTimeRangeSelector(); // remove time picker from top of page
timefilter.disableAutoRefreshSelector(); // remove time picker from top of page
$scope.indexPatterns = getIndexPatterns();
const path = $route.current.locals.nextStepPath;
$scope.withIndexPatternUrl = function (pattern) {
if (!pattern) {
return;
}
return '#/jobs/new_job/step/job_type?index=' + encodeURIComponent(pattern.id);
return `${path}?index=${encodeURIComponent(pattern.id)}`;
};
$scope.withSavedSearchUrl = function (savedSearch) {
if (!savedSearch) {
return;
}
return '#/jobs/new_job/step/job_type?savedSearchId=' + encodeURIComponent(savedSearch.id);
return `${path}?savedSearchId=${encodeURIComponent(savedSearch.id)}`;
};
});

View file

@ -8,54 +8,74 @@
import React from 'react';
import { XPackInfoProvider } from 'plugins/xpack_main/services/xpack_info';
import { banners, addAppRedirectMessageToUrl } from 'ui/notify';
import { LICENSE_TYPE } from '../../common/constants/license';
import chrome from 'ui/chrome';
import { EuiCallOut } from '@elastic/eui';
let licenseHasExpired = true;
let licenseType = null;
let expiredLicenseBannerId;
export function checkLicense(Private, kbnBaseUrl) {
const xpackInfo = Private(XPackInfoProvider);
const features = xpackInfo.get('features.ml');
export function checkFullLicense(Private, kbnBaseUrl, kbnUrl) {
const features = getFeatures(Private);
licenseType = features.licenseType;
const licenseAllowsToShowThisPage = features.isAvailable;
if (!licenseAllowsToShowThisPage) {
const { message } = features;
const newUrl = addAppRedirectMessageToUrl(chrome.addBasePath(kbnBaseUrl), message);
window.location.href = newUrl;
return Promise.halt();
}
if (features.isAvailable === false) {
// ML is not enabled
return redirectToKibana(features, kbnBaseUrl);
licenseHasExpired = features.hasExpired || false;
} else if (features.licenseType === LICENSE_TYPE.BASIC) {
// If the license has expired ML app will still work for 7 days and then
// the job management endpoints (e.g. create job, start datafeed) will be restricted.
// Therefore we need to keep the app enabled but show an info banner to the user.
if(licenseHasExpired) {
const message = features.message;
if (expiredLicenseBannerId === undefined) {
// Only show the banner once with no way to dismiss it
expiredLicenseBannerId = banners.add({
component: (
<EuiCallOut
iconType="iInCircle"
color="warning"
title={message}
/>
),
});
// ML is enabled, but only with a basic or gold license
return redirectToBasic(kbnUrl);
} else {
// ML is enabled
licenseHasExpired = (features.hasExpired || false);
// If the license has expired ML app will still work for 7 days and then
// the job management endpoints (e.g. create job, start datafeed) will be restricted.
// Therefore we need to keep the app enabled but show an info banner to the user.
if(licenseHasExpired) {
const message = features.message;
if (expiredLicenseBannerId === undefined) {
// Only show the banner once with no way to dismiss it
expiredLicenseBannerId = banners.add({
component: (
<EuiCallOut
iconType="iInCircle"
color="warning"
title={message}
/>
),
});
}
}
return Promise.resolve(features);
}
return Promise.resolve(features);
}
// a wrapper for checkLicense which doesn't resolve if the license has expired.
export function checkBasicLicense(Private, kbnBaseUrl) {
const features = getFeatures(Private);
licenseType = features.licenseType;
if (features.isAvailable === false) {
// ML is not enabled
return redirectToKibana(features, kbnBaseUrl);
} else {
// ML is enabled
return Promise.resolve(features);
}
}
// a wrapper for checkFullLicense which doesn't resolve if the license has expired.
// this is used by all create jobs pages to redirect back to the jobs list
// if the user's license has expired.
export function checkLicenseExpired(Private, Promise, kbnBaseUrl, kbnUrl) {
return checkLicense(Private, Promise, kbnBaseUrl)
export function checkLicenseExpired(Private, kbnBaseUrl, kbnUrl) {
return checkFullLicense(Private, kbnBaseUrl, kbnUrl)
.then((features) => {
if (features.hasExpired) {
kbnUrl.redirect('/jobs');
@ -69,17 +89,37 @@ export function checkLicenseExpired(Private, Promise, kbnBaseUrl, kbnUrl) {
});
}
export function getLicenseHasExpired() {
function getFeatures(Private) {
const xpackInfo = Private(XPackInfoProvider);
return xpackInfo.get('features.ml');
}
function redirectToKibana(features, kbnBaseUrl) {
const { message } = features;
const newUrl = addAppRedirectMessageToUrl(chrome.addBasePath(kbnBaseUrl), (message || ''));
window.location.href = newUrl;
return Promise.halt();
}
function redirectToBasic(kbnUrl) {
kbnUrl.redirect('/datavisualizer');
return Promise.halt();
}
export function hasLicenseExpired() {
return licenseHasExpired;
}
export function isFullLicense() {
return (licenseType === LICENSE_TYPE.FULL);
}
export function xpackFeatureProvider(Private) {
const xpackInfo = Private(XPackInfoProvider);
function isAvailable(feature) {
return xpackInfo.get(`features.${feature}.isAvailable`, false);
}
return {
isAvailable
isAvailable(feature) {
xpackInfo.get(`features.${feature}.isAvailable`, false);
}
};
}

View file

@ -7,7 +7,7 @@
import { getPrivileges } from 'plugins/ml/privilege/get_privileges';
import { getLicenseHasExpired } from 'plugins/ml/license/check_license';
import { hasLicenseExpired } from 'plugins/ml/license/check_license';
let privileges = {};
@ -16,7 +16,7 @@ export function checkGetJobsPrivilege(Private, Promise, kbnUrl) {
getPrivileges()
.then((priv) => {
privileges = priv;
// the minimum privilege for using ML is being able to get the jobs list.
// the minimum privilege for using ML with a platinum license is being able to get the jobs list.
// all other functionality is controlled by the return privileges object
if (privileges.canGetJobs) {
return resolve(privileges);
@ -45,10 +45,27 @@ export function checkCreateJobsPrivilege(Private, Promise, kbnUrl) {
});
}
export function checkFindFileStructurePrivilege(Private, Promise, kbnUrl) {
return new Promise((resolve, reject) => {
getPrivileges()
.then((priv) => {
privileges = priv;
// the minimum privilege for using ML with a basic license is being able to use the datavisualizer.
// all other functionality is controlled by the return privileges object
if (privileges.canFindFileStructure) {
return resolve(privileges);
} else {
kbnUrl.redirect('/access-denied');
return reject();
}
});
});
}
// check the privilege type and the license to see whether a user has permission to access a feature.
// takes the name of the privilege variable as specified in get_privileges.js
export function checkPermission(privilegeType) {
const licenseHasExpired = getLicenseHasExpired();
const licenseHasExpired = hasLicenseExpired();
return (privileges[privilegeType] === true && licenseHasExpired !== true);
}
@ -56,7 +73,7 @@ export function checkPermission(privilegeType) {
// expired or if they don't have the privilege to press that button
export function createPermissionFailureMessage(privilegeType) {
let message = '';
const licenseHasExpired = getLicenseHasExpired();
const licenseHasExpired = hasLicenseExpired();
if (licenseHasExpired) {
message = 'Your license has expired.';
} else if (privilegeType === 'canCreateJob') {

View file

@ -26,6 +26,7 @@ export function getPrivileges() {
canGetFilters: false,
canCreateFilter: false,
canDeleteFilter: false,
canFindFileStructure: false,
};
return new Promise((resolve, reject) => {
@ -57,6 +58,7 @@ export function getPrivileges() {
'cluster:admin/xpack/ml/filters/get',
'cluster:admin/xpack/ml/filters/update',
'cluster:admin/xpack/ml/filters/delete',
'cluster:monitor/xpack/ml/findfilestructure',
]
};
@ -146,6 +148,10 @@ export function getPrivileges() {
privileges.canDeleteFilter = true;
}
if (resp.cluster['cluster:monitor/xpack/ml/findfilestructure']) {
privileges.canFindFileStructure = true;
}
}
resolve(privileges);

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import chrome from 'ui/chrome';
import { http } from '../../services/http_service';
const basePath = chrome.addBasePath('/api/ml');
export const fileDatavisualizer = {
analyzeFile(obj, params) {
let paramString = '?';
for (const p in params) {
if (params.hasOwnProperty(p)) {
paramString += `&${p}=${params[p]}`;
}
}
return http({
url: `${basePath}/file_data_visualizer/analyze_file${paramString}`,
method: 'POST',
data: obj
});
},
import(obj) {
const paramString = (obj.id !== undefined) ? `?id=${obj.id}` : '';
const {
index,
data,
settings,
mappings,
ingestPipeline
} = obj;
return http({
url: `${basePath}/file_data_visualizer/import${paramString}`,
method: 'POST',
data: {
index,
data,
settings,
mappings,
ingestPipeline,
}
});
}
};

View file

@ -14,6 +14,7 @@ import { http } from '../../services/http_service';
import { filters } from './filters';
import { results } from './results';
import { jobs } from './jobs';
import { fileDatavisualizer } from './datavisualizer';
const basePath = chrome.addBasePath('/api/ml');
@ -408,7 +409,16 @@ export const ml = {
});
},
getIndices() {
const tempBasePath = chrome.addBasePath('/api');
return http({
url: `${tempBasePath}/index_management/indices`,
method: 'GET',
});
},
filters,
results,
jobs,
fileDatavisualizer,
};

View file

@ -12,7 +12,7 @@ import ReactDOM from 'react-dom';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml', ['react']);
import { checkLicense } from 'plugins/ml/license/check_license';
import { checkFullLicense } from 'plugins/ml/license/check_license';
import { checkGetJobsPrivilege } from 'plugins/ml/privilege/check_privilege';
import { getMlNodeCount } from 'plugins/ml/ml_nodes_check/check_ml_nodes';
import { initPromise } from 'plugins/ml/util/promise';
@ -30,7 +30,7 @@ uiRoutes
.when('/settings/filter_lists/new_filter_list', {
template,
resolve: {
CheckLicense: checkLicense,
CheckLicense: checkFullLicense,
privileges: checkGetJobsPrivilege,
mlNodeCount: getMlNodeCount,
initPromise: initPromise(false)
@ -39,7 +39,7 @@ uiRoutes
.when('/settings/filter_lists/edit_filter_list/:filterId', {
template,
resolve: {
CheckLicense: checkLicense,
CheckLicense: checkFullLicense,
privileges: checkGetJobsPrivilege,
mlNodeCount: getMlNodeCount,
initPromise: initPromise(false)

Some files were not shown because too many files have changed in this diff Show more