mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
b981546290
commit
25d35fac27
117 changed files with 5290 additions and 137 deletions
|
@ -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;
|
11
x-pack/plugins/ml/common/constants/license.js
Normal file
11
x-pack/plugins/ml/common/constants/license.js
Normal 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,
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
1
x-pack/plugins/ml/public/components/nav_menu/_index.scss
Normal file
1
x-pack/plugins/ml/public/components/nav_menu/_index.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@import 'nav_menu'
|
|
@ -0,0 +1,4 @@
|
|||
.disabled-nav-link {
|
||||
color: $euiColorMediumShade;
|
||||
pointer-events: none;
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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' },
|
||||
|
|
1
x-pack/plugins/ml/public/datavisualizer/_index.scss
Normal file
1
x-pack/plugins/ml/public/datavisualizer/_index.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@import './selector/index';
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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')) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
@import 'selector';
|
|
@ -0,0 +1,5 @@
|
|||
.ml-datavisualizer-selector {
|
||||
flex-grow: 1;
|
||||
background-color: $euiColorLightestShade;
|
||||
min-height: 100vh;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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' }, { });
|
||||
});
|
|
@ -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';
|
|
@ -25,6 +25,9 @@
|
|||
display: inline-block;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.no-sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.datavisualizer-sidebar {
|
||||
width: 290px;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
@import 'components/index';
|
||||
|
||||
.file-datavisualizer-container {
|
||||
padding: 20px;
|
||||
background-color: $euiColorLightestShade;
|
||||
min-height: calc(100vh - 70px);
|
||||
}
|
1
x-pack/plugins/ml/public/file_datavisualizer/_index.scss
Normal file
1
x-pack/plugins/ml/public/file_datavisualizer/_index.scss
Normal file
|
@ -0,0 +1 @@
|
|||
@import 'file_datavisualizer';
|
|
@ -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';
|
|
@ -0,0 +1,6 @@
|
|||
.file-datavisualizer-about-panel__icon {
|
||||
width: $euiSizeXL * 3;
|
||||
height: $euiSizeXL * 3;
|
||||
margin-left: $euiSizeXL;
|
||||
margin-right: $euiSizeL;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
@import 'about_panel'
|
|
@ -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
|
||||
<EuiLink
|
||||
href="https://github.com/elastic/kibana/issues/new"
|
||||
target="_blank"
|
||||
>
|
||||
GitHub
|
||||
</EuiLink>.
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -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';
|
|
@ -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%;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
@import 'analysis_summary';
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
|
@ -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>
|
||||
`;
|
|
@ -0,0 +1 @@
|
|||
@import 'edit_flyout'
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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';
|
|
@ -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',
|
||||
];
|
|
@ -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);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
.fields-stats {
|
||||
padding: 10px;
|
||||
}
|
||||
.field {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
@import 'fields_stats';
|
||||
@import 'field_stats_card';
|
|
@ -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} </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>
|
||||
);
|
||||
}
|
|
@ -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 [];
|
||||
}
|
|
@ -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';
|
|
@ -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');
|
||||
}
|
|
@ -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';
|
|
@ -0,0 +1 @@
|
|||
@import 'file_datavisualizer_view'
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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';
|
|
@ -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,
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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' };
|
||||
}
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
@import 'import_sumary'
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
|
@ -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 '';
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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';
|
|
@ -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(),
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
@import 'results_view'
|
|
@ -0,0 +1,10 @@
|
|||
.results {
|
||||
.euiDescriptionList{
|
||||
dd, dt {
|
||||
margin-top: 5px;
|
||||
}
|
||||
dd:nth-child(1), dt:nth-child(1), {
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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 });
|
||||
});
|
8
x-pack/plugins/ml/public/file_datavisualizer/index.js
Normal file
8
x-pack/plugins/ml/public/file_datavisualizer/index.js
Normal 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';
|
10
x-pack/plugins/ml/public/index.scss
Normal file
10
x-pack/plugins/ml/public/index.scss
Normal 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';
|
|
@ -5,4 +5,4 @@
|
|||
*/
|
||||
|
||||
|
||||
export { MLJobEditor } from './ml_job_editor';
|
||||
export { MLJobEditor, EDITOR_MODE } from './ml_job_editor';
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
line-height: 1.25;
|
||||
font-weight: 300;
|
||||
line-height: 2.5rem;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.actions-border, .actions-border-large {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)}`;
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue