[ML] Show better file structure finder explanations (#62316)

* [ML] Show better file structure finder explanations

* more typescript changes

* changing function format

* fixing some types

* fixing translation id

* fix boom error reporting

* changes based on review

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
James Gowdy 2020-04-07 08:47:39 +01:00 committed by GitHub
parent 8429a8ede9
commit 64f27ca34e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 608 additions and 266 deletions

View file

@ -9,6 +9,7 @@ export interface ErrorResponse {
statusCode: number;
error: string;
message: string;
attributes?: any;
};
name: string;
}

View file

@ -4,6 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
export interface InputOverrides {
[key: string]: string;
}
export type FormattedOverrides = InputOverrides & {
column_names: string[];
has_header_row: boolean;
should_trim_fields: boolean;
};
export interface AnalysisResult {
results: FindFileStructureResponse;
overrides?: FormattedOverrides;
}
export interface FindFileStructureResponse {
charset: string;
has_header_row: boolean;
@ -28,4 +43,54 @@ export interface FindFileStructureResponse {
need_client_timezone: boolean;
num_lines_analyzed: number;
column_names: string[];
explanation?: string[];
grok_pattern?: string;
multiline_start_pattern?: string;
exclude_lines_pattern?: string;
java_timestamp_formats?: string[];
joda_timestamp_formats?: string[];
timestamp_field?: string;
should_trim_fields?: boolean;
}
export interface ImportResponse {
success: boolean;
id: string;
index?: string;
pipelineId?: string;
docCount: number;
failures: ImportFailure[];
error?: any;
ingestError?: boolean;
}
export interface ImportFailure {
item: number;
reason: string;
doc: Doc;
}
export interface Doc {
message: string;
}
export interface Settings {
pipeline?: string;
index: string;
body: any[];
[key: string]: any;
}
export interface Mappings {
[key: string]: any;
}
export interface IngestPipelineWrapper {
id: string;
pipeline: IngestPipeline;
}
export interface IngestPipeline {
description: string;
processors: any[];
}

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { FC } from 'react';
import {
EuiFlexGroup,
@ -23,7 +23,11 @@ import {
import { WelcomeContent } from './welcome_content';
export function AboutPanel({ onFilePickerChange }) {
interface Props {
onFilePickerChange(files: FileList | null): void;
}
export const AboutPanel: FC<Props> = ({ onFilePickerChange }) => {
return (
<EuiPage restrictWidth={1000} data-test-subj="mlPageFileDataVisualizerUpload">
<EuiPageBody>
@ -54,9 +58,9 @@ export function AboutPanel({ onFilePickerChange }) {
</EuiPageBody>
</EuiPage>
);
}
};
export function LoadingPanel() {
export const LoadingPanel: FC = () => {
return (
<EuiPage restrictWidth={400} data-test-subj="mlPageFileDataVisLoading">
<EuiPageBody>
@ -79,4 +83,4 @@ export function LoadingPanel() {
</EuiPageBody>
</EuiPage>
);
}
};

View file

@ -5,7 +5,8 @@
*/
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiFlexGroup,
@ -19,7 +20,14 @@ import {
import { ExperimentalBadge } from '../experimental_badge';
export function WelcomeContent() {
export const WelcomeContent: FC = () => {
const toolTipContent = i18n.translate(
'xpack.ml.fileDatavisualizer.welcomeContent.experimentalFeatureTooltip',
{
defaultMessage: "Experimental feature. We'd love to hear your feedback.",
}
);
return (
<EuiFlexGroup gutterSize="xl" alignItems="center">
<EuiFlexItem grow={false}>
@ -32,16 +40,7 @@ export function WelcomeContent() {
id="xpack.ml.fileDatavisualizer.welcomeContent.visualizeDataFromLogFileTitle"
defaultMessage="Visualize data from a log file&nbsp;{experimentalBadge}"
values={{
experimentalBadge: (
<ExperimentalBadge
tooltipContent={
<FormattedMessage
id="xpack.ml.fileDatavisualizer.welcomeContent.experimentalFeatureTooltip"
defaultMessage="Experimental feature. We'd love to hear your feedback."
/>
}
/>
),
experimentalBadge: <ExperimentalBadge tooltipContent={toolTipContent} />,
}}
/>
</h1>
@ -144,4 +143,4 @@ export function WelcomeContent() {
</EuiFlexItem>
</EuiFlexGroup>
);
}
};

View file

@ -5,11 +5,12 @@
*/
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { FC } from 'react';
import { EuiTitle, EuiSpacer, EuiDescriptionList } from '@elastic/eui';
import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
export function AnalysisSummary({ results }) {
export const AnalysisSummary: FC<{ results: FindFileStructureResponse }> = ({ results }) => {
const items = createDisplayItems(results);
return (
@ -28,10 +29,10 @@ export function AnalysisSummary({ results }) {
<EuiDescriptionList type="column" listItems={items} className="analysis-summary-list" />
</React.Fragment>
);
}
};
function createDisplayItems(results) {
const items = [
function createDisplayItems(results: FindFileStructureResponse) {
const items: Array<{ title: any; description: string | number }> = [
{
title: (
<FormattedMessage

View file

@ -5,11 +5,11 @@
*/
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { FC } from 'react';
import { EuiBetaBadge } from '@elastic/eui';
export function ExperimentalBadge({ tooltipContent }) {
export const ExperimentalBadge: FC<{ tooltipContent: string }> = ({ tooltipContent }) => {
return (
<span>
<EuiBetaBadge
@ -24,4 +24,4 @@ export function ExperimentalBadge({ tooltipContent }) {
/>
</span>
);
}
};

View file

@ -0,0 +1,82 @@
/*
* 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, { FC } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiFlyout,
EuiFlyoutFooter,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutHeader,
EuiButtonEmpty,
EuiTitle,
EuiFlyoutBody,
EuiSpacer,
EuiText,
EuiSubSteps,
} from '@elastic/eui';
import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
interface Props {
results: FindFileStructureResponse;
closeFlyout(): void;
}
export const ExplanationFlyout: FC<Props> = ({ results, closeFlyout }) => {
const explanation = results.explanation!;
return (
<EuiFlyout onClose={closeFlyout} hideCloseButton size={'m'}>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="m">
<h2>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.explanationFlyout.title"
defaultMessage="Analysis explanation"
/>
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<Content explanation={explanation} />
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={closeFlyout} flush="left">
<FormattedMessage
id="xpack.ml.fileDatavisualizer.explanationFlyout.closeButton"
defaultMessage="Close"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</EuiFlyout>
);
};
const Content: FC<{ explanation: string[] }> = ({ explanation }) => (
<>
<EuiText size={'s'}>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.explanationFlyout.content"
defaultMessage="The logical steps that have produced the analysis results."
/>
<EuiSpacer size="l" />
<EuiSubSteps>
<ul style={{ wordBreak: 'break-word' }}>
{explanation.map((e, i) => (
<li key={i}>
{e}
<EuiSpacer size="s" />
</li>
))}
</ul>
</EuiSubSteps>
</EuiText>
</>
);

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { ImportProgress, IMPORT_STATUS } from './import_progress';
export { ExplanationFlyout } from './explanation_flyout';

View file

@ -5,13 +5,19 @@
*/
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { FC } from 'react';
import { EuiTitle, EuiSpacer } from '@elastic/eui';
import { MLJobEditor, ML_EDITOR_MODE } from '../../../../jobs/jobs_list/components/ml_job_editor';
export function FileContents({ data, format, numberOfLines }) {
interface Props {
data: string;
format: string;
numberOfLines: number;
}
export const FileContents: FC<Props> = ({ data, format, numberOfLines }) => {
let mode = ML_EDITOR_MODE.TEXT;
if (format === ML_EDITOR_MODE.JSON) {
mode = ML_EDITOR_MODE.JSON;
@ -35,7 +41,7 @@ export function FileContents({ data, format, numberOfLines }) {
id="xpack.ml.fileDatavisualizer.fileContents.firstLinesDescription"
defaultMessage="First {numberOfLines, plural, zero {# line} one {# line} other {# lines}}"
values={{
numberOfLines: numberOfLines,
numberOfLines,
}}
/>
</div>
@ -51,9 +57,9 @@ export function FileContents({ data, format, numberOfLines }) {
/>
</React.Fragment>
);
}
};
function limitByNumberOfLines(data, numberOfLines) {
function limitByNumberOfLines(data: string, numberOfLines: number) {
return data
.split('\n')
.slice(0, numberOfLines)

View file

@ -17,9 +17,9 @@ import { BottomBar } from '../bottom_bar';
import { ResultsView } from '../results_view';
import { FileCouldNotBeRead, FileTooLarge } from './file_error_callouts';
import { EditFlyout } from '../edit_flyout';
import { ExplanationFlyout } from '../explanation_flyout';
import { ImportView } from '../import_view';
import { MAX_BYTES } from '../../../../../../common/constants/file_datavisualizer';
import { isErrorResponse } from '../../../../../../common/types/errors';
import {
readFile,
createUrlOverrides,
@ -42,12 +42,14 @@ export class FileDataVisualizerView extends Component {
fileSize: 0,
fileTooLarge: false,
fileCouldNotBeRead: false,
serverErrorMessage: '',
serverError: null,
loading: false,
loaded: false,
results: undefined,
explanation: undefined,
mode: MODE.READ,
isEditFlyoutVisible: false,
isExplanationFlyoutVisible: false,
bottomBarVisible: false,
hasPermissionToImport: false,
};
@ -78,8 +80,9 @@ export class FileDataVisualizerView extends Component {
fileSize: 0,
fileTooLarge: false,
fileCouldNotBeRead: false,
serverErrorMessage: '',
serverError: null,
results: undefined,
explanation: undefined,
},
() => {
if (files.length) {
@ -128,7 +131,7 @@ export class FileDataVisualizerView extends Component {
console.log('overrides', overrides);
const { analyzeFile } = ml.fileDatavisualizer;
const resp = await analyzeFile(lessData, overrides);
const serverSettings = processResults(resp.results);
const serverSettings = processResults(resp);
const serverOverrides = resp.overrides;
this.previousOverrides = this.overrides;
@ -172,6 +175,7 @@ export class FileDataVisualizerView extends Component {
this.setState({
results: resp.results,
explanation: resp.explanation,
loaded: true,
loading: false,
fileCouldNotBeRead: isRetry,
@ -179,19 +183,13 @@ export class FileDataVisualizerView extends Component {
} catch (error) {
console.error(error);
let serverErrorMsg;
if (isErrorResponse(error) === true) {
serverErrorMsg = `${error.body.error}: ${error.body.message}`;
} else {
serverErrorMsg = JSON.stringify(error, null, 2);
}
this.setState({
results: undefined,
explanation: undefined,
loaded: false,
loading: false,
fileCouldNotBeRead: true,
serverErrorMessage: serverErrorMsg,
serverError: error,
});
// as long as the previous overrides are different to the current overrides,
@ -216,6 +214,16 @@ export class FileDataVisualizerView extends Component {
this.hideBottomBar();
};
closeExplanationFlyout = () => {
this.setState({ isExplanationFlyoutVisible: false });
this.showBottomBar();
};
showExplanationFlyout = () => {
this.setState({ isExplanationFlyoutVisible: true });
this.hideBottomBar();
};
showBottomBar = () => {
this.setState({ bottomBarVisible: true });
};
@ -252,14 +260,16 @@ export class FileDataVisualizerView extends Component {
loading,
loaded,
results,
explanation,
fileContents,
fileName,
fileSize,
fileTooLarge,
fileCouldNotBeRead,
serverErrorMessage,
serverError,
mode,
isEditFlyoutVisible,
isExplanationFlyoutVisible,
bottomBarVisible,
hasPermissionToImport,
} = this.state;
@ -281,7 +291,7 @@ export class FileDataVisualizerView extends Component {
{fileCouldNotBeRead && loading === false && (
<React.Fragment>
<FileCouldNotBeRead error={serverErrorMessage} loaded={loaded} />
<FileCouldNotBeRead error={serverError} loaded={loaded} />
<EuiSpacer size="l" />
</React.Fragment>
)}
@ -289,9 +299,12 @@ export class FileDataVisualizerView extends Component {
{loaded && (
<ResultsView
results={results}
explanation={explanation}
fileName={fileName}
data={fileContents}
showEditFlyout={() => this.showEditFlyout()}
showExplanationFlyout={() => this.showExplanationFlyout()}
disableButtons={isEditFlyoutVisible || isExplanationFlyoutVisible}
/>
)}
<EditFlyout
@ -303,6 +316,10 @@ export class FileDataVisualizerView extends Component {
fields={fields}
/>
{isExplanationFlyoutVisible && (
<ExplanationFlyout results={results} closeFlyout={this.closeExplanationFlyout} />
)}
{bottomBarVisible && loaded && (
<BottomBar
mode={MODE.READ}

View file

@ -5,15 +5,21 @@
*/
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { FC } from 'react';
import { EuiCallOut } from '@elastic/eui';
import { EuiCallOut, EuiSpacer } from '@elastic/eui';
import numeral from '@elastic/numeral';
import { ErrorResponse } from '../../../../../../common/types/errors';
const FILE_SIZE_DISPLAY_FORMAT = '0,0.[0] b';
export function FileTooLarge({ fileSize, maxFileSize }) {
interface FileTooLargeProps {
fileSize: number;
maxFileSize: number;
}
export const FileTooLarge: FC<FileTooLargeProps> = ({ fileSize, maxFileSize }) => {
const fileSizeFormatted = numeral(fileSize).format(FILE_SIZE_DISPLAY_FORMAT);
const maxFileSizeFormatted = numeral(maxFileSize).format(FILE_SIZE_DISPLAY_FORMAT);
@ -67,9 +73,15 @@ export function FileTooLarge({ fileSize, maxFileSize }) {
{errorText}
</EuiCallOut>
);
};
interface FileCouldNotBeReadProps {
error: ErrorResponse;
loaded: boolean;
}
export function FileCouldNotBeRead({ error, loaded }) {
export const FileCouldNotBeRead: FC<FileCouldNotBeReadProps> = ({ error, loaded }) => {
const message = error.body.message;
return (
<EuiCallOut
title={
@ -82,15 +94,32 @@ export function FileCouldNotBeRead({ error, loaded }) {
iconType="cross"
data-test-subj="mlFileUploadErrorCallout fileCouldNotBeRead"
>
{error !== undefined && <p>{error}</p>}
{message}
<Explanation error={error} />
{loaded && (
<p>
<>
<EuiSpacer size="s" />
<FormattedMessage
id="xpack.ml.fileDatavisualizer.fileErrorCallouts.revertingToPreviousSettingsDescription"
defaultMessage="Reverting to previous settings"
/>
</p>
</>
)}
</EuiCallOut>
);
}
};
export const Explanation: FC<{ error: ErrorResponse }> = ({ error }) => {
if (!error.body.attributes?.body?.error?.suppressed?.length) {
return null;
}
const reason: string = error.body.attributes.body.error.suppressed[0].reason;
return (
<>
<EuiSpacer size="s" />
{reason.split('\n').map((m, i) => (
<div key={i}>{m}</div>
))}
</>
);
};

View file

@ -5,13 +5,24 @@
*/
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiCallOut, EuiAccordion } from '@elastic/eui';
import { IMPORT_STATUS } from '../import_progress';
import { IMPORT_STATUS, Statuses } from '../import_progress';
export function ImportErrors({ errors, statuses }) {
interface ImportError {
msg: string;
more?: string;
}
interface Props {
errors: any[];
statuses: Statuses;
}
export const ImportErrors: FC<Props> = ({ errors, statuses }) => {
return (
<EuiCallOut title={title(statuses)} color="danger" iconType="cross">
{errors.map((e, i) => (
@ -19,9 +30,9 @@ export function ImportErrors({ errors, statuses }) {
))}
</EuiCallOut>
);
}
};
function title(statuses) {
function title(statuses: Statuses) {
switch (IMPORT_STATUS.FAILED) {
case statuses.readStatus:
return (
@ -82,7 +93,7 @@ function title(statuses) {
}
}
function ImportError(error, key) {
function ImportError(error: any, key: number) {
const errorObj = toString(error);
return (
<React.Fragment>
@ -106,7 +117,7 @@ function ImportError(error, key) {
);
}
function toString(error) {
function toString(error: any): ImportError {
if (typeof error === 'string') {
return { msg: error };
}
@ -118,7 +129,7 @@ function toString(error) {
if (typeof error.error === 'object') {
if (error.error.msg !== undefined) {
// this will catch a bulk ingest failure
const errorObj = { msg: error.error.msg };
const errorObj: ImportError = { msg: error.error.msg };
if (error.error.body !== undefined) {
errorObj.more = error.error.response;
}
@ -139,11 +150,8 @@ function toString(error) {
}
return {
msg: (
<FormattedMessage
id="xpack.ml.fileDatavisualizer.importErrors.unknownErrorMessage"
defaultMessage="Unknown error"
/>
),
msg: i18n.translate('xpack.ml.fileDatavisualizer.importErrors.unknownErrorMessage', {
defaultMessage: 'Unknown error',
}),
};
}

View file

@ -6,17 +6,31 @@
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { FC } from 'react';
import { EuiStepsHorizontal, EuiProgress, EuiSpacer } from '@elastic/eui';
export const IMPORT_STATUS = {
INCOMPLETE: 'incomplete',
COMPLETE: 'complete',
FAILED: 'danger',
};
export enum IMPORT_STATUS {
INCOMPLETE = 'incomplete',
COMPLETE = 'complete',
FAILED = 'danger',
}
export function ImportProgress({ statuses }) {
export interface Statuses {
reading: boolean;
readStatus: IMPORT_STATUS;
parseJSONStatus: IMPORT_STATUS;
indexCreatedStatus: IMPORT_STATUS;
ingestPipelineCreatedStatus: IMPORT_STATUS;
indexPatternCreatedStatus: IMPORT_STATUS;
uploadProgress: number;
uploadStatus: IMPORT_STATUS;
createIndexPattern: boolean;
createPipeline: boolean;
permissionCheckStatus: IMPORT_STATUS;
}
export const ImportProgress: FC<{ statuses: Statuses }> = ({ statuses }) => {
const {
reading,
readStatus,
@ -271,9 +285,9 @@ export function ImportProgress({ statuses }) {
)}
</React.Fragment>
);
}
};
function UploadFunctionProgress({ progress }) {
const UploadFunctionProgress: FC<{ progress: number }> = ({ progress }) => {
return (
<React.Fragment>
<p>
@ -290,4 +304,4 @@ function UploadFunctionProgress({ progress }) {
)}
</React.Fragment>
);
}
};

View file

@ -0,0 +1,7 @@
/*
* 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, Statuses } from './import_progress';

View file

@ -6,7 +6,7 @@
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { FC } from 'react';
import {
EuiFieldText,
@ -20,7 +20,25 @@ import {
import { MLJobEditor, ML_EDITOR_MODE } from '../../../../jobs/jobs_list/components/ml_job_editor';
const EDITOR_HEIGHT = '300px';
export function AdvancedSettings({
interface Props {
index: string;
indexPattern: string;
initialized: boolean;
onIndexChange(): void;
createIndexPattern: boolean;
onCreateIndexPatternChange(): void;
onIndexPatternChange(): void;
indexSettingsString: string;
mappingsString: string;
pipelineString: string;
onIndexSettingsStringChange(): void;
onMappingsStringChange(): void;
onPipelineStringChange(): void;
indexNameError: string;
indexPatternNameError: string;
}
export const AdvancedSettings: FC<Props> = ({
index,
indexPattern,
initialized,
@ -36,7 +54,7 @@ export function AdvancedSettings({
onPipelineStringChange,
indexNameError,
indexPatternNameError,
}) {
}) => {
return (
<React.Fragment>
<EuiFormRow
@ -93,7 +111,6 @@ export function AdvancedSettings({
defaultMessage="Index pattern name"
/>
}
disabled={createIndexPattern === false || initialized === true}
isInvalid={indexPatternNameError !== ''}
error={[indexPatternNameError]}
>
@ -133,9 +150,15 @@ export function AdvancedSettings({
</EuiFlexGroup>
</React.Fragment>
);
};
interface JsonEditorProps {
initialized: boolean;
data: string;
onChange(): void;
}
function IndexSettings({ initialized, data, onChange }) {
const IndexSettings: FC<JsonEditorProps> = ({ initialized, data, onChange }) => {
return (
<React.Fragment>
<EuiFormRow
@ -145,7 +168,6 @@ function IndexSettings({ initialized, data, onChange }) {
defaultMessage="Index settings"
/>
}
disabled={initialized === true}
fullWidth
>
<MLJobEditor
@ -159,9 +181,9 @@ function IndexSettings({ initialized, data, onChange }) {
</EuiFormRow>
</React.Fragment>
);
}
};
function Mappings({ initialized, data, onChange }) {
const Mappings: FC<JsonEditorProps> = ({ initialized, data, onChange }) => {
return (
<React.Fragment>
<EuiFormRow
@ -171,7 +193,6 @@ function Mappings({ initialized, data, onChange }) {
defaultMessage="Mappings"
/>
}
disabled={initialized === true}
fullWidth
>
<MLJobEditor
@ -185,9 +206,9 @@ function Mappings({ initialized, data, onChange }) {
</EuiFormRow>
</React.Fragment>
);
}
};
function IngestPipeline({ initialized, data, onChange }) {
const IngestPipeline: FC<JsonEditorProps> = ({ initialized, data, onChange }) => {
return (
<React.Fragment>
<EuiFormRow
@ -197,7 +218,6 @@ function IngestPipeline({ initialized, data, onChange }) {
defaultMessage="Ingest pipeline"
/>
}
disabled={initialized === true}
fullWidth
>
<MLJobEditor
@ -211,4 +231,4 @@ function IngestPipeline({ initialized, data, onChange }) {
</EuiFormRow>
</React.Fragment>
);
}
};

View file

@ -5,14 +5,32 @@
*/
import { i18n } from '@kbn/i18n';
import React from 'react';
import React, { FC } from 'react';
import { EuiTabbedContent, EuiSpacer } from '@elastic/eui';
import { SimpleSettings } from './simple';
import { AdvancedSettings } from './advanced';
export const ImportSettings = ({
interface Props {
index: string;
indexPattern: string;
initialized: boolean;
onIndexChange(): void;
createIndexPattern: boolean;
onCreateIndexPatternChange(): void;
onIndexPatternChange(): void;
indexSettingsString: string;
mappingsString: string;
pipelineString: string;
onIndexSettingsStringChange(): void;
onMappingsStringChange(): void;
onPipelineStringChange(): void;
indexNameError: string;
indexPatternNameError: string;
}
export const ImportSettings: FC<Props> = ({
index,
indexPattern,
initialized,

View file

@ -6,11 +6,20 @@
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { FC } from 'react';
import { EuiFieldText, EuiFormRow, EuiCheckbox, EuiSpacer } from '@elastic/eui';
export const SimpleSettings = ({
interface Props {
index: string;
initialized: boolean;
onIndexChange(): void;
createIndexPattern: boolean;
onCreateIndexPatternChange(): void;
indexNameError: string;
}
export const SimpleSettings: FC<Props> = ({
index,
initialized,
onIndexChange,

View file

@ -5,11 +5,29 @@
*/
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { FC } from 'react';
import { EuiSpacer, EuiDescriptionList, EuiCallOut, EuiAccordion } from '@elastic/eui';
export function ImportSummary({
interface Props {
index: string;
indexPattern: string;
ingestPipelineId: string;
docCount: number;
importFailures: DocFailure[];
createIndexPattern: boolean;
createPipeline: boolean;
}
interface DocFailure {
item: number;
reason: string;
doc: {
message: string;
};
}
export const ImportSummary: FC<Props> = ({
index,
indexPattern,
ingestPipelineId,
@ -17,7 +35,7 @@ export function ImportSummary({
importFailures,
createIndexPattern,
createPipeline,
}) {
}) => {
const items = createDisplayItems(
index,
indexPattern,
@ -75,9 +93,13 @@ export function ImportSummary({
)}
</React.Fragment>
);
};
interface FailuresProps {
failedDocs: DocFailure[];
}
function Failures({ failedDocs }) {
const Failures: FC<FailuresProps> = ({ failedDocs }) => {
return (
<EuiAccordion
id="failureList"
@ -101,16 +123,16 @@ function Failures({ failedDocs }) {
</div>
</EuiAccordion>
);
}
};
function createDisplayItems(
index,
indexPattern,
ingestPipelineId,
docCount,
importFailures,
createIndexPattern,
createPipeline
index: string,
indexPattern: string,
ingestPipelineId: string,
docCount: number,
importFailures: DocFailure[],
createIndexPattern: boolean,
createPipeline: boolean
) {
const items = [
{

View file

@ -623,7 +623,6 @@ async function createKibanaIndexPattern(
id,
};
} catch (error) {
console.error(error);
return {
success: false,
error,

View file

@ -4,30 +4,53 @@
* 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';
import { i18n } from '@kbn/i18n';
import { ml } from '../../../../../services/ml_api_service';
import {
Doc,
ImportFailure,
ImportResponse,
Mappings,
Settings,
IngestPipeline,
} from '../../../../../../../common/types/file_datavisualizer';
const CHUNK_SIZE = 5000;
const MAX_CHUNK_CHAR_COUNT = 1000000;
const IMPORT_RETRIES = 5;
export class Importer {
constructor({ settings, mappings, pipeline }) {
this.settings = settings;
this.mappings = mappings;
this.pipeline = pipeline;
export interface ImportConfig {
settings: Settings;
mappings: Mappings;
pipeline: IngestPipeline;
}
this.data = [];
this.docArray = [];
this.docSizeArray = [];
export interface ImportResults {
success: boolean;
failures?: any[];
docCount?: number;
error?: any;
}
export class Importer {
private _settings: Settings;
private _mappings: Mappings;
private _pipeline: IngestPipeline;
protected _docArray: Doc[] = [];
constructor({ settings, mappings, pipeline }: ImportConfig) {
this._settings = settings;
this._mappings = mappings;
this._pipeline = pipeline;
}
async initializeImport(index) {
const settings = this.settings;
const mappings = this.mappings;
const pipeline = this.pipeline;
async initializeImport(index: string) {
const settings = this._settings;
const mappings = this._mappings;
const pipeline = this._pipeline;
updatePipelineTimezone(pipeline);
// if no pipeline has been supplied,
@ -52,7 +75,12 @@ export class Importer {
return createIndexResp;
}
async import(id, index, pipelineId, setImportProgress) {
async import(
id: string,
index: string,
pipelineId: string,
setImportProgress: (progress: number) => void
): Promise<ImportResults> {
if (!id || !index) {
return {
success: false,
@ -65,14 +93,14 @@ export class Importer {
};
}
const chunks = createDocumentChunks(this.docArray);
const chunks = createDocumentChunks(this._docArray);
const ingestPipeline = {
id: pipelineId,
};
let success = true;
const failures = [];
const failures: ImportFailure[] = [];
let error;
for (let i = 0; i < chunks.length; i++) {
@ -86,10 +114,13 @@ export class Importer {
};
let retries = IMPORT_RETRIES;
let resp = {
let resp: ImportResponse = {
success: false,
failures: [],
docCount: 0,
id: '',
index: '',
pipelineId: '',
};
while (resp.success === false && retries > 0) {
@ -97,12 +128,14 @@ export class Importer {
resp = await ml.fileDatavisualizer.import(aggs);
if (retries < IMPORT_RETRIES) {
// eslint-disable-next-line no-console
console.log(`Retrying import ${IMPORT_RETRIES - retries}`);
}
retries--;
} catch (err) {
resp = { success: false, error: err };
resp.success = false;
resp.error = err;
retries = 0;
}
}
@ -110,6 +143,7 @@ export class Importer {
if (resp.success) {
setImportProgress(((i + 1) / chunks.length) * 100);
} else {
// eslint-disable-next-line no-console
console.error(resp);
success = false;
error = resp.error;
@ -120,10 +154,10 @@ export class Importer {
populateFailures(resp, failures, i);
}
const result = {
const result: ImportResults = {
success,
failures,
docCount: this.docArray.length,
docCount: this._docArray.length,
};
if (success) {
@ -136,7 +170,7 @@ export class Importer {
}
}
function populateFailures(error, failures, chunkCount) {
function populateFailures(error: ImportResponse, failures: ImportFailure[], chunkCount: number) {
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
@ -155,10 +189,10 @@ function populateFailures(error, failures, chunkCount) {
// But it's not sending every single field that Filebeat would add, so the ingest pipeline
// cannot look for a event.timezone variable in each input record.
// Therefore we need to replace {{ event.timezone }} with the actual browser timezone
function updatePipelineTimezone(ingestPipeline) {
function updatePipelineTimezone(ingestPipeline: IngestPipeline) {
if (ingestPipeline !== undefined && ingestPipeline.processors && ingestPipeline.processors) {
const dateProcessor = ingestPipeline.processors.find(
p => p.date !== undefined && p.date.timezone === '{{ event.timezone }}'
(p: any) => p.date !== undefined && p.date.timezone === '{{ event.timezone }}'
);
if (dateProcessor) {
@ -167,8 +201,8 @@ function updatePipelineTimezone(ingestPipeline) {
}
}
function createDocumentChunks(docArray) {
const chunks = [];
function createDocumentChunks(docArray: Doc[]) {
const chunks: Doc[][] = [];
// chop docArray into 5000 doc chunks
const tempChunks = chunk(docArray, CHUNK_SIZE);

View file

@ -6,8 +6,14 @@
import { MessageImporter } from './message_importer';
import { NdjsonImporter } from './ndjson_importer';
import { ImportConfig } from './importer';
import { FindFileStructureResponse } from '../../../../../../../common/types/file_datavisualizer';
export function importerFactory(format, results, settings) {
export function importerFactory(
format: string,
results: FindFileStructureResponse,
settings: ImportConfig
) {
switch (format) {
// delimited and semi-structured text are both handled by splitting the
// file into messages, then sending these to ES for further processing

View file

@ -4,17 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Importer } from './importer';
import { Importer, ImportConfig } from './importer';
import {
Doc,
FindFileStructureResponse,
} from '../../../../../../../common/types/file_datavisualizer';
export class MessageImporter extends Importer {
constructor(results, settings) {
private _excludeLinesRegex: RegExp | null;
private _multilineStartRegex: RegExp | null;
constructor(results: FindFileStructureResponse, settings: ImportConfig) {
super(settings);
this.excludeLinesRegex =
this._excludeLinesRegex =
results.exclude_lines_pattern === undefined
? null
: new RegExp(results.exclude_lines_pattern);
this.multilineStartRegex =
this._multilineStartRegex =
results.multiline_start_pattern === undefined
? null
: new RegExp(results.multiline_start_pattern);
@ -26,9 +33,9 @@ export class MessageImporter extends Importer {
// 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 newline char inside a field value, so keep looking.
read(text) {
read(text: string) {
try {
const data = [];
const data: Doc[] = [];
let message = '';
let line = '';
@ -57,14 +64,12 @@ export class MessageImporter extends Importer {
data.shift();
}
this.data = data;
this.docArray = this.data;
this._docArray = data;
return {
success: true,
};
} catch (error) {
console.error(error);
return {
success: false,
error,
@ -72,9 +77,9 @@ export class MessageImporter extends Importer {
}
}
processLine(data, message, line) {
if (this.excludeLinesRegex === null || line.match(this.excludeLinesRegex) === null) {
if (this.multilineStartRegex === null || line.match(this.multilineStartRegex) !== null) {
processLine(data: Doc[], message: string, line: string) {
if (this._excludeLinesRegex === null || line.match(this._excludeLinesRegex) === null) {
if (this._multilineStartRegex === null || line.match(this._multilineStartRegex) !== null) {
this.addMessage(data, message);
message = '';
} else if (data.length === 0) {
@ -90,7 +95,7 @@ export class MessageImporter extends Importer {
return message;
}
addMessage(data, message) {
addMessage(data: Doc[], message: string) {
// if the message ended \r\n (Windows line endings)
// then omit the \r as well as the \n for consistency
message = message.replace(/\r$/, '');

View file

@ -4,18 +4,19 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Importer } from './importer';
import { Importer, ImportConfig } from './importer';
import { FindFileStructureResponse } from '../../../../../../../common/types/file_datavisualizer';
export class NdjsonImporter extends Importer {
constructor(results, settings) {
constructor(results: FindFileStructureResponse, settings: ImportConfig) {
super(settings);
}
read(json) {
read(json: string) {
try {
const splitJson = json.split(/}\s*\n/);
const ndjson = [];
const ndjson: any[] = [];
for (let i = 0; i < splitJson.length; i++) {
if (splitJson[i] !== '') {
// note the extra } at the end of the line, adding back
@ -24,7 +25,7 @@ export class NdjsonImporter extends Importer {
}
}
this.docArray = ndjson;
this._docArray = ndjson;
return {
success: true,

View file

@ -7,10 +7,10 @@
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import React, { FC } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiPage,
EuiPageBody,
EuiPageContentHeader,
@ -18,13 +18,33 @@ import {
EuiTabbedContent,
EuiSpacer,
EuiTitle,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { FindFileStructureResponse } from '../../../../../../common/types/file_datavisualizer';
import { FileContents } from '../file_contents';
import { AnalysisSummary } from '../analysis_summary';
// @ts-ignore
import { FieldsStats } from '../fields_stats';
export const ResultsView = ({ data, fileName, results, showEditFlyout }) => {
interface Props {
data: string;
fileName: string;
results: FindFileStructureResponse;
showEditFlyout(): void;
showExplanationFlyout(): void;
disableButtons: boolean;
}
export const ResultsView: FC<Props> = ({
data,
fileName,
results,
showEditFlyout,
showExplanationFlyout,
disableButtons,
}) => {
const tabs = [
{
id: 'file-stats',
@ -60,12 +80,24 @@ export const ResultsView = ({ data, fileName, results, showEditFlyout }) => {
<EuiSpacer size="m" />
<EuiButton onClick={() => showEditFlyout()}>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.resultsView.overrideSettingsButtonLabel"
defaultMessage="Override settings"
/>
</EuiButton>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton onClick={() => showEditFlyout()} disabled={disableButtons}>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.resultsView.overrideSettingsButtonLabel"
defaultMessage="Override settings"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={() => showExplanationFlyout()} disabled={disableButtons}>
<FormattedMessage
id="xpack.ml.fileDatavisualizer.resultsView.analysisExplanationButtonLabel"
defaultMessage="Analysis explanation"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
<EuiSpacer size="m" />

View file

@ -1,21 +0,0 @@
/*
* 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 DEFAULT_LINES_TO_SAMPLE = 1000;
export const overrideDefaults = {
timestampFormat: undefined,
timestampField: undefined,
format: undefined,
delimiter: undefined,
quote: undefined,
hasHeaderRow: undefined,
charset: undefined,
columnNames: undefined,
shouldTrimFields: undefined,
grokPattern: undefined,
linesToSample: undefined,
};

View file

@ -4,11 +4,27 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { overrideDefaults, DEFAULT_LINES_TO_SAMPLE } from './overrides';
import { isEqual } from 'lodash';
import { ml } from '../../../../services/ml_api_service';
import { AnalysisResult, InputOverrides } from '../../../../../../common/types/file_datavisualizer';
export function readFile(file) {
const DEFAULT_LINES_TO_SAMPLE = 1000;
const overrideDefaults = {
timestampFormat: undefined,
timestampField: undefined,
format: undefined,
delimiter: undefined,
quote: undefined,
hasHeaderRow: undefined,
charset: undefined,
columnNames: undefined,
shouldTrimFields: undefined,
grokPattern: undefined,
linesToSample: undefined,
};
export function readFile(file: File) {
return new Promise((resolve, reject) => {
if (file && file.size) {
const reader = new FileReader();
@ -23,14 +39,14 @@ export function readFile(file) {
resolve({ data });
}
};
})(file);
})();
} else {
reject();
}
});
}
export function reduceData(data, mb) {
export function reduceData(data: string, mb: number) {
// assuming ascii characters in the file where 1 char is 1 byte
// TODO - change this when other non UTF-8 formats are
// supported for the read data
@ -38,8 +54,8 @@ export function reduceData(data, mb) {
return data.length >= size ? data.slice(0, size) : data;
}
export function createUrlOverrides(overrides, originalSettings) {
const formattedOverrides = {};
export function createUrlOverrides(overrides: InputOverrides, originalSettings: InputOverrides) {
const formattedOverrides: InputOverrides = {};
for (const o in overrideDefaults) {
if (overrideDefaults.hasOwnProperty(o)) {
let value = overrides[o];
@ -93,15 +109,15 @@ export function createUrlOverrides(overrides, originalSettings) {
return formattedOverrides;
}
export function processResults(results) {
export function processResults({ results, overrides }: AnalysisResult) {
const timestampFormat =
results.java_timestamp_formats !== undefined && results.java_timestamp_formats.length
? results.java_timestamp_formats[0]
: undefined;
const linesToSample =
results.overrides !== undefined && results.overrides.lines_to_sample !== undefined
? results.overrides.lines_to_sample
overrides !== undefined && overrides.lines_to_sample !== undefined
? overrides.lines_to_sample
: DEFAULT_LINES_TO_SAMPLE;
return {
@ -125,8 +141,8 @@ export function processResults(results) {
* @param {string} indexName
* @returns {Promise<boolean>}
*/
export async function hasImportPermission(indexName) {
const priv = {
export async function hasImportPermission(indexName: string) {
const priv: { cluster: string[]; index?: any } = {
cluster: ['cluster:monitor/nodes/info', 'cluster:admin/ingest/pipeline/put'],
};

View file

@ -7,6 +7,7 @@
import { http } from '../http_service';
import { basePath } from './index';
import { ImportResponse } from '../../../../common/types/file_datavisualizer';
export const fileDatavisualizer = {
analyzeFile(file: string, params: Record<string, string> = {}) {
@ -27,7 +28,7 @@ export const fileDatavisualizer = {
mappings,
ingestPipeline,
}: {
id: string;
id: string | undefined;
index: string;
data: any;
settings: any;
@ -43,7 +44,7 @@ export const fileDatavisualizer = {
ingestPipeline,
});
return http<any>({
return http<ImportResponse>({
path: `${basePath()}/file_data_visualizer/import`,
method: 'POST',
query,

View file

@ -740,7 +740,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
urls: [
{
fmt:
'/_ml/find_file_structure?&charset=<%=charset%>&format=<%=format%>&has_header_row=<%=has_header_row%>&column_names=<%=column_names%>&delimiter=<%=delimiter%>&quote=<%=quote%>&should_trim_fields=<%=should_trim_fields%>&grok_pattern=<%=grok_pattern%>&timestamp_field=<%=timestamp_field%>&timestamp_format=<%=timestamp_format%>&lines_to_sample=<%=lines_to_sample%>',
'/_ml/find_file_structure?&explain=true&charset=<%=charset%>&format=<%=format%>&has_header_row=<%=has_header_row%>&column_names=<%=column_names%>&delimiter=<%=delimiter%>&quote=<%=quote%>&should_trim_fields=<%=should_trim_fields%>&grok_pattern=<%=grok_pattern%>&timestamp_field=<%=timestamp_field%>&timestamp_format=<%=timestamp_format%>&lines_to_sample=<%=lines_to_sample%>',
req: {
charset: {
type: 'string',
@ -778,7 +778,7 @@ export const elasticsearchJsPlugin = (Client: any, config: any, components: any)
},
},
{
fmt: '/_ml/find_file_structure',
fmt: '/_ml/find_file_structure?&explain=true',
},
],
needBody: true,

View file

@ -9,9 +9,13 @@ import { ResponseError, CustomHttpResponseOptions } from 'kibana/server';
export function wrapError(error: any): CustomHttpResponseOptions<ResponseError> {
const boom = isBoom(error) ? error : boomify(error, { statusCode: error.status });
const statusCode = boom.output.statusCode;
return {
body: boom,
body: {
message: boom,
...(statusCode !== 500 && error.body ? { attributes: { body: error.body } } : {}),
},
headers: boom.output.headers,
statusCode: boom.output.statusCode,
statusCode,
};
}

View file

@ -4,40 +4,21 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { APICaller } from 'kibana/server';
import { FindFileStructureResponse } from '../../../common/types/file_datavisualizer';
import {
AnalysisResult,
FormattedOverrides,
InputOverrides,
} from '../../../common/types/file_datavisualizer';
export type InputData = any[];
export interface InputOverrides {
[key: string]: string;
}
export type FormattedOverrides = InputOverrides & {
column_names: string[];
has_header_row: boolean;
should_trim_fields: boolean;
};
export interface AnalysisResult {
results: FindFileStructureResponse;
overrides?: FormattedOverrides;
}
export function fileDataVisualizerProvider(callAsCurrentUser: APICaller) {
async function analyzeFile(data: any, overrides: any): Promise<AnalysisResult> {
let results = [];
try {
results = await callAsCurrentUser('ml.fileStructure', {
body: data,
...overrides,
});
} catch (error) {
const err = error.message !== undefined ? error.message : error;
throw Boom.badRequest(err);
}
const results = await callAsCurrentUser('ml.fileStructure', {
body: data,
...overrides,
});
const { hasOverrides, reducedOverrides } = formatOverrides(overrides);

View file

@ -6,39 +6,24 @@
import { APICaller } from 'kibana/server';
import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_datavisualizer';
import {
ImportResponse,
ImportFailure,
Settings,
Mappings,
IngestPipelineWrapper,
} from '../../../common/types/file_datavisualizer';
import { InputData } from './file_data_visualizer';
export interface Settings {
pipeline?: string;
index: string;
body: any[];
[key: string]: any;
}
export interface Mappings {
[key: string]: any;
}
export interface InjectPipeline {
id: string;
pipeline: any;
}
interface Failure {
item: number;
reason: string;
doc: any;
}
export function importDataProvider(callAsCurrentUser: APICaller) {
async function importData(
id: string,
index: string,
settings: Settings,
mappings: Mappings,
ingestPipeline: InjectPipeline,
ingestPipeline: IngestPipelineWrapper,
data: InputData
) {
): Promise<ImportResponse> {
let createdIndex;
let createdPipelineId;
const docCount = data.length;
@ -66,7 +51,7 @@ export function importDataProvider(callAsCurrentUser: APICaller) {
createdPipelineId = pipelineId;
}
let failures: Failure[] = [];
let failures: ImportFailure[] = [];
if (data.length) {
const resp = await indexData(index, createdPipelineId, data);
if (resp.success === false) {
@ -144,7 +129,7 @@ export function importDataProvider(callAsCurrentUser: APICaller) {
};
}
} catch (error) {
let failures: Failure[] = [];
let failures: ImportFailure[] = [];
let ingestError = false;
if (error.errors !== undefined && Array.isArray(error.items)) {
// an expected error where some or all of the bulk request
@ -169,7 +154,7 @@ export function importDataProvider(callAsCurrentUser: APICaller) {
return await callAsCurrentUser('ingest.putPipeline', { id, body: pipeline });
}
function getFailures(items: any[], data: InputData): Failure[] {
function getFailures(items: any[], data: InputData): ImportFailure[] {
const failures = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];

View file

@ -4,11 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
export {
fileDataVisualizerProvider,
InputOverrides,
InputData,
AnalysisResult,
} from './file_data_visualizer';
export { fileDataVisualizerProvider, InputData } from './file_data_visualizer';
export { importDataProvider, Settings, InjectPipeline, Mappings } from './import_data';
export { importDataProvider } from './import_data';

View file

@ -7,15 +7,17 @@
import { schema } from '@kbn/config-schema';
import { RequestHandlerContext } from 'kibana/server';
import { MAX_BYTES } from '../../common/constants/file_datavisualizer';
import { wrapError } from '../client/error_wrapper';
import {
InputOverrides,
Settings,
IngestPipelineWrapper,
Mappings,
} from '../../common/types/file_datavisualizer';
import { wrapError } from '../client/error_wrapper';
import {
InputData,
fileDataVisualizerProvider,
importDataProvider,
Settings,
InjectPipeline,
Mappings,
} from '../models/file_data_visualizer';
import { RouteInitialization } from '../types';
@ -32,7 +34,7 @@ function importData(
index: string,
settings: Settings,
mappings: Mappings,
ingestPipeline: InjectPipeline,
ingestPipeline: IngestPipelineWrapper,
data: InputData
) {
const { importData: importDataFunc } = importDataProvider(context.ml!.mlClient.callAsCurrentUser);