[ML] Support multi-line JSON notation (#58870) (#59211)

* [ML] multi-line json support for analytics job editor

* [ML] advanced editor with xjson

* [ML] add jest mock for XJsonMode

* [ML] add xJson mode to the json tab

* [ML] fix mocks
This commit is contained in:
Dima Arnautov 2020-03-04 09:28:49 +01:00 committed by GitHub
parent 8fd6f43470
commit 617c874e2a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 97 additions and 39 deletions

View file

@ -4,5 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export function MLJobEditor(props: any): any;
export const EDITOR_MODE: any;
export function XJsonMode() {}

View file

@ -5,3 +5,4 @@
*/
export { usePartialState } from './use_partial_state';
export { useXJsonMode, xJsonMode } from './use_x_json_mode';

View file

@ -0,0 +1,25 @@
/*
* 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 { useState } from 'react';
import {
collapseLiteralStrings,
expandLiteralStrings,
XJsonMode,
} from '../../../../shared_imports';
export const xJsonMode = new XJsonMode();
export const useXJsonMode = (json: string) => {
const [xJson, setXJson] = useState(expandLiteralStrings(json));
return {
xJson,
setXJson,
xJsonMode,
convertToJson: collapseLiteralStrings,
};
};

View file

@ -17,8 +17,10 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { collapseLiteralStrings } from '../../../../../../../../../../../src/plugins/es_ui_shared/console_lang/lib/json_xjson_translation_tools';
import { CreateAnalyticsFormProps } from '../../hooks/use_create_analytics_form';
import { xJsonMode } from '../../../../../components/custom_hooks';
export const CreateAnalyticsAdvancedEditor: FC<CreateAnalyticsFormProps> = ({ actions, state }) => {
const {
@ -42,7 +44,8 @@ export const CreateAnalyticsAdvancedEditor: FC<CreateAnalyticsFormProps> = ({ ac
const onChange = (str: string) => {
setAdvancedEditorRawString(str);
try {
setJobConfig(JSON.parse(str));
const resultJobConfig = JSON.parse(collapseLiteralStrings(str));
setJobConfig(resultJobConfig);
} catch (e) {
resetAdvancedEditorMessages();
}
@ -119,7 +122,7 @@ export const CreateAnalyticsAdvancedEditor: FC<CreateAnalyticsFormProps> = ({ ac
style={{ maxWidth: '100%' }}
>
<EuiCodeEditor
mode="json"
mode={xJsonMode}
width="100%"
value={advancedEditorRawString}
onChange={onChange}

View file

@ -29,6 +29,8 @@ jest.mock('react', () => {
return { ...r, memo: (x: any) => x };
});
jest.mock('../../../../../../../shared_imports');
describe('Data Frame Analytics: <CreateAnalyticsButton />', () => {
test('Minimal initialization', () => {
const { getLastHookValue } = getMountedHook();

View file

@ -29,7 +29,7 @@ export const CreateAnalyticsFlyout: FC<CreateAnalyticsFormProps> = ({
const { isJobCreated, isJobStarted, isModalButtonDisabled, isValid } = state;
return (
<EuiFlyout size="s" onClose={closeModal} data-test-subj="mlAnalyticsCreateJobFlyout">
<EuiFlyout size="m" onClose={closeModal} data-test-subj="mlAnalyticsCreateJobFlyout">
<EuiFlyoutHeader>
<EuiTitle>
<h2 data-test-subj="mlDataFrameAnalyticsFlyoutHeaderTitle">

View file

@ -9,12 +9,12 @@ import React from 'react';
import { EuiTitle, EuiSpacer } from '@elastic/eui';
import { MLJobEditor, EDITOR_MODE } from '../../../../jobs/jobs_list/components/ml_job_editor';
import { MLJobEditor, ML_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;
let mode = ML_EDITOR_MODE.TEXT;
if (format === ML_EDITOR_MODE.JSON) {
mode = ML_EDITOR_MODE.JSON;
}
const formattedData = limitByNumberOfLines(data, numberOfLines);

View file

@ -17,7 +17,7 @@ import {
EuiFlexItem,
} from '@elastic/eui';
import { MLJobEditor, EDITOR_MODE } from '../../../../jobs/jobs_list/components/ml_job_editor';
import { MLJobEditor, ML_EDITOR_MODE } from '../../../../jobs/jobs_list/components/ml_job_editor';
const EDITOR_HEIGHT = '300px';
export function AdvancedSettings({
@ -149,7 +149,7 @@ function IndexSettings({ initialized, data, onChange }) {
fullWidth
>
<MLJobEditor
mode={EDITOR_MODE.JSON}
mode={ML_EDITOR_MODE.JSON}
readOnly={initialized === true}
value={data}
height={EDITOR_HEIGHT}
@ -175,7 +175,7 @@ function Mappings({ initialized, data, onChange }) {
fullWidth
>
<MLJobEditor
mode={EDITOR_MODE.JSON}
mode={ML_EDITOR_MODE.JSON}
readOnly={initialized === true}
value={data}
height={EDITOR_HEIGHT}
@ -201,7 +201,7 @@ function IngestPipeline({ initialized, data, onChange }) {
fullWidth
>
<MLJobEditor
mode={EDITOR_MODE.JSON}
mode={ML_EDITOR_MODE.JSON}
readOnly={initialized === true}
value={data}
height={EDITOR_HEIGHT}

View file

@ -30,6 +30,7 @@ import { mlMessageBarService } from '../../../../components/messagebar';
import { withKibana } from '../../../../../../../../../../src/plugins/kibana_react/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { collapseLiteralStrings } from '../../../../../../shared_imports';
export class EditJobFlyoutUI extends Component {
_initialJobFormState = null;
@ -225,7 +226,7 @@ export class EditJobFlyoutUI extends Component {
groups: this.state.jobGroups,
mml: this.state.jobModelMemoryLimit,
detectorDescriptions: this.state.jobDetectorDescriptions,
datafeedQuery: this.state.datafeedQuery,
datafeedQuery: collapseLiteralStrings(this.state.datafeedQuery),
datafeedQueryDelay: this.state.datafeedQueryDelay,
datafeedFrequency: this.state.datafeedFrequency,
datafeedScrollSize: this.state.datafeedScrollSize,

View file

@ -12,7 +12,7 @@ import { EuiFieldText, EuiForm, EuiFormRow, EuiSpacer, EuiFieldNumber } from '@e
import { calculateDatafeedFrequencyDefaultSeconds } from '../../../../../../../common/util/job_utils';
import { getNewJobDefaults } from '../../../../../services/ml_server_info';
import { parseInterval } from '../../../../../../../common/util/parse_interval';
import { MLJobEditor } from '../../ml_job_editor';
import { MLJobEditor, ML_EDITOR_MODE } from '../../ml_job_editor';
import { FormattedMessage } from '@kbn/i18n/react';
function getDefaults(bucketSpan, jobDefaults) {
@ -85,7 +85,12 @@ export class Datafeed extends Component {
}
style={{ maxWidth: 'inherit' }}
>
<MLJobEditor value={query} onChange={this.onQueryChange} height="200px" />
<MLJobEditor
mode={ML_EDITOR_MODE.XJSON}
value={query}
onChange={this.onQueryChange}
height="200px"
/>
</EuiFormRow>
<EuiFormRow
label={

View file

@ -9,14 +9,14 @@ import React from 'react';
import { EuiSpacer } from '@elastic/eui';
import { MLJobEditor } from '../ml_job_editor';
import { ML_EDITOR_MODE, MLJobEditor } from '../ml_job_editor';
export function JsonPane({ job }) {
const json = JSON.stringify(job, null, 2);
return (
<React.Fragment>
<EuiSpacer size="s" />
<MLJobEditor value={json} readOnly={true} />
<MLJobEditor value={json} readOnly={true} mode={ML_EDITOR_MODE.XJSON} />
</React.Fragment>
);
}

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { MLJobEditor, EDITOR_MODE } from './ml_job_editor';
export { MLJobEditor, ML_EDITOR_MODE } from './ml_job_editor';

View file

@ -4,23 +4,38 @@
* you may not use this file except in compliance with the Elastic License.
*/
import PropTypes from 'prop-types';
import React from 'react';
import React, { FC } from 'react';
import { EuiCodeEditor } from '@elastic/eui';
import { expandLiteralStrings } from '../../../../../../shared_imports';
import { xJsonMode } from '../../../../components/custom_hooks';
export const EDITOR_MODE = { TEXT: 'text', JSON: 'json' };
export const ML_EDITOR_MODE = { TEXT: 'text', JSON: 'json', XJSON: xJsonMode };
export function MLJobEditor({
interface MlJobEditorProps {
value: string;
height?: string;
width?: string;
mode?: typeof ML_EDITOR_MODE[keyof typeof ML_EDITOR_MODE];
readOnly?: boolean;
syntaxChecking?: boolean;
theme?: string;
onChange?: Function;
}
export const MLJobEditor: FC<MlJobEditorProps> = ({
value,
height = '500px',
width = '100%',
mode = EDITOR_MODE.JSON,
mode = ML_EDITOR_MODE.JSON,
readOnly = false,
syntaxChecking = true,
theme = 'textmate',
onChange = () => {},
}) {
}) => {
if (mode === ML_EDITOR_MODE.XJSON) {
value = expandLiteralStrings(value);
}
return (
<EuiCodeEditor
value={value}
@ -40,14 +55,4 @@ export function MLJobEditor({
onChange={onChange}
/>
);
}
MLJobEditor.propTypes = {
value: PropTypes.string.isRequired,
height: PropTypes.string,
width: PropTypes.string,
mode: PropTypes.string,
readOnly: PropTypes.bool,
syntaxChecking: PropTypes.bool,
theme: PropTypes.string,
onChange: PropTypes.func,
};

View file

@ -18,8 +18,9 @@ import {
EuiFlyoutBody,
EuiSpacer,
} from '@elastic/eui';
import { collapseLiteralStrings } from '../../../../../../../../shared_imports';
import { Datafeed } from '../../../../common/job_creator/configs';
import { MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor';
import { ML_EDITOR_MODE, MLJobEditor } from '../../../../../jobs_list/components/ml_job_editor';
import { isValidJson } from '../../../../../../../../common/util/validation_utils';
import { JobCreatorContext } from '../../job_creator_context';
@ -68,10 +69,11 @@ export const JsonEditorFlyout: FC<Props> = ({ isDisabled, jobEditorMode, datafee
function onDatafeedChange(json: string) {
setDatafeedConfigString(json);
let valid = isValidJson(json);
const jsonValue = collapseLiteralStrings(json);
let valid = isValidJson(jsonValue);
if (valid) {
// ensure that the user hasn't altered the indices list in the json.
const { indices }: Datafeed = JSON.parse(json);
const { indices }: Datafeed = JSON.parse(jsonValue);
const originalIndices = jobCreator.indices.sort();
valid =
originalIndices.length === indices.length &&
@ -82,7 +84,7 @@ export const JsonEditorFlyout: FC<Props> = ({ isDisabled, jobEditorMode, datafee
function onSave() {
const jobConfig = JSON.parse(jobConfigString);
const datafeedConfig = JSON.parse(datafeedConfigString);
const datafeedConfig = JSON.parse(collapseLiteralStrings(datafeedConfigString));
jobCreator.cloneFromExistingJob(jobConfig, datafeedConfig);
jobCreatorUpdate();
setShowJsonFlyout(false);
@ -191,6 +193,7 @@ const Contents: FC<{
<MLJobEditor
value={value}
height={EDITOR_HEIGHT}
mode={ML_EDITOR_MODE.XJSON}
readOnly={editJson === false}
onChange={onChange}
/>

View file

@ -52,6 +52,8 @@ jest.mock('../../util/dependency_cache', () => ({
getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }),
}));
jest.mock('../../../../shared_imports');
describe('TimeSeriesExplorerUrlStateManager', () => {
test('Initial render shows "No single metric jobs found"', () => {
const props = {

View file

@ -0,0 +1,12 @@
/*
* 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 { XJsonMode } from '../../../plugins/es_ui_shared/console_lang/ace/modes/x_json';
export {
collapseLiteralStrings,
expandLiteralStrings,
} from '../../../../src/plugins/es_ui_shared/console_lang/lib';