diff --git a/x-pack/plugins/painless_lab/public/application/common/constants.tsx b/x-pack/plugins/painless_lab/public/application/common/constants.tsx index b88ff1e226d27..8ee10a7392557 100644 --- a/x-pack/plugins/painless_lab/public/application/common/constants.tsx +++ b/x-pack/plugins/painless_lab/public/application/common/constants.tsx @@ -122,5 +122,4 @@ for (int y = 0; y < height; y++) { } } -return result; -`; +return result;`; diff --git a/x-pack/plugins/painless_lab/public/application/common/types.ts b/x-pack/plugins/painless_lab/public/application/common/types.ts index 697247415c428..cf3d58d6b26d0 100644 --- a/x-pack/plugins/painless_lab/public/application/common/types.ts +++ b/x-pack/plugins/painless_lab/public/application/common/types.ts @@ -4,24 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface ContextSetup { - params?: any; - document: Record; - index: string; -} - // This should be an enumerated list export type Context = string; -export interface Script { - source: string; - params?: Record; +export interface RequestPayloadConfig { + code: string; + context: string; + parameters: string; + index: string; + document: string; } -export interface Request { - script: Script; - context?: Context; - context_setup?: ContextSetup; +export enum PayloadFormat { + UGLY = 'ugly', + PRETTY = 'pretty', } export interface Response { @@ -47,15 +43,3 @@ export interface ExecutionError { position: ExecutionErrorPosition; script: string; } - -export type JsonArray = JsonValue[]; -export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; - -export interface JsonObject { - [key: string]: JsonValue; -} - -export type ContextChangeHandler = (change: { - context?: Partial; - contextSetup?: Partial; -}) => void; diff --git a/x-pack/plugins/painless_lab/public/application/components/editor.tsx b/x-pack/plugins/painless_lab/public/application/components/editor.tsx index 8c2f07e539871..b8891ce6524f5 100644 --- a/x-pack/plugins/painless_lab/public/application/components/editor.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/editor.tsx @@ -8,10 +8,10 @@ import { CodeEditor } from '../../../../../../src/plugins/kibana_react/public'; interface Props { code: string; - setCode: (code: string) => void; + onChange: (code: string) => void; } -export function Editor({ code, setCode }: Props) { +export function Editor({ code, onChange }: Props) { return ( { + submit(state); + localStorage.setItem(PAINLESS_LAB_KEY, JSON.stringify(state)); + }, [state, submit]); - const [contextSetup, setContextSetup] = useState( - getFromLocalStorage('painlessLabContextSetup', {}, true) - ); + const onCodeChange = (newCode: string) => { + setState({ ...state, code: newCode }); + }; - const { inProgress, response, submit } = useSubmitCode(http); + const onContextChange = (newContext: string) => { + setState({ ...state, context: newContext }); + }; - // Live-update the output as the user changes the input code. - useEffect(() => { - submit(code, context, contextSetup); - }, [submit, code, context, contextSetup]); + const onParametersChange = (newParameters: string) => { + setState({ ...state, parameters: newParameters }); + }; - const toggleRequestFlyout = () => { - setRequestFlyoutOpen(!isRequestFlyoutOpen); + const onIndexChange = (newIndex: string) => { + setState({ ...state, index: newIndex }); + }; + + const onDocumentChange = (newDocument: string) => { + setState({ ...state, document: newDocument }); }; - const contextChangeHandler: ContextChangeHandler = ({ - context: nextContext, - contextSetup: nextContextSetup, - }) => { - if (nextContext) { - setContext(nextContext); - } - if (nextContextSetup) { - setContextSetup(nextContextSetup); - } + const toggleRequestFlyout = () => { + setRequestFlyoutOpen(!isRequestFlyoutOpen); }; return ( @@ -68,16 +79,21 @@ export function Main({ http }: Props) { - + @@ -86,13 +102,16 @@ export function Main({ http }: Props) { isLoading={inProgress} toggleRequestFlyout={toggleRequestFlyout} isRequestFlyoutOpen={isRequestFlyoutOpen} - reset={() => setCode(exampleScript)} + reset={() => onCodeChange(exampleScript)} /> {isRequestFlyoutOpen && ( setRequestFlyoutOpen(false)} - requestBody={formatJson(buildRequestPayload(code, context, contextSetup))} + requestBody={buildRequestPayload( + { code, context, document, index, parameters }, + PayloadFormat.PRETTY + )} response={response ? formatJson(response.result || response.error) : ''} /> )} diff --git a/x-pack/plugins/painless_lab/public/application/components/output_pane/context_tab.tsx b/x-pack/plugins/painless_lab/public/application/components/output_pane/context_tab.tsx index 619c966506ab7..4a0c18733075f 100644 --- a/x-pack/plugins/painless_lab/public/application/components/output_pane/context_tab.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/output_pane/context_tab.tsx @@ -21,15 +21,24 @@ import { i18n } from '@kbn/i18n'; import { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; import { painlessContextOptions } from '../../common/constants'; -import { ContextChangeHandler, ContextSetup } from '../../common/types'; interface Props { - context: string; - contextSetup: ContextSetup; - onContextChange: ContextChangeHandler; + context: any; + index: string; + document: string; + onContextChange: (context: string) => void; + onIndexChange: (index: string) => void; + onDocumentChange: (document: string) => void; } -export const ContextTab = ({ context, contextSetup, onContextChange }: Props) => ( +export const ContextTab = ({ + context, + index, + document, + onContextChange, + onIndexChange, + onDocumentChange, +}: Props) => ( <> onContextChange({ context: value })} + onChange={onContextChange} itemLayoutAlign="top" hasDividers fullWidth @@ -88,15 +97,7 @@ export const ContextTab = ({ context, contextSetup, onContextChange }: Props) => } fullWidth > - { - onContextChange({ - contextSetup: Object.assign({}, contextSetup, { index: e.target.value }), - }); - }} - /> + onIndexChange(e.target.value)} /> )} {['filter', 'score'].indexOf(context) !== -1 && ( @@ -122,11 +123,8 @@ export const ContextTab = ({ context, contextSetup, onContextChange }: Props) => { - const newContextSetup = Object.assign({}, contextSetup, { document: value }); - onContextChange({ contextSetup: newContextSetup }); - }} + value={document} + onChange={onDocumentChange} options={{ fontSize: 12, minimap: { diff --git a/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx b/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx index db6e58124d74a..07ed3d54cecad 100644 --- a/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx @@ -15,20 +15,36 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Response, ContextSetup, Context, ContextChangeHandler } from '../../common/types'; +import { Response } from '../../common/types'; import { OutputTab } from './output_tab'; import { ParametersTab } from './parameters_tab'; import { ContextTab } from './context_tab'; interface Props { - context: Context; - contextSetup: ContextSetup; isLoading: boolean; - onContextChange: ContextChangeHandler; response?: Response; + context: string; + parameters: string; + index: string; + document: string; + onContextChange: (change: string) => void; + onParametersChange: (change: string) => void; + onIndexChange: (change: string) => void; + onDocumentChange: (change: string) => void; } -export function OutputPane({ response, context, contextSetup, onContextChange, isLoading }: Props) { +export function OutputPane({ + isLoading, + response, + context, + parameters, + index, + document, + onContextChange, + onParametersChange, + onIndexChange, + onDocumentChange, +}: Props) { const outputTabLabel = ( @@ -67,7 +83,7 @@ export function OutputPane({ response, context, contextSetup, onContextChange, i defaultMessage: 'Parameters', }), content: ( - + ), }, { @@ -78,8 +94,11 @@ export function OutputPane({ response, context, contextSetup, onContextChange, i content: ( ), }, diff --git a/x-pack/plugins/painless_lab/public/application/components/output_pane/parameters_tab.tsx b/x-pack/plugins/painless_lab/public/application/components/output_pane/parameters_tab.tsx index 3542c99b2584f..4ed27bf47dc68 100644 --- a/x-pack/plugins/painless_lab/public/application/components/output_pane/parameters_tab.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/output_pane/parameters_tab.tsx @@ -14,16 +14,16 @@ import { EuiText, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { monaco } from '@kbn/ui-shared-deps/monaco'; import { i18n } from '@kbn/i18n'; import { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; -import { ContextChangeHandler, ContextSetup } from '../../common/types'; interface Props { - contextSetup: ContextSetup; - onContextChange: ContextChangeHandler; + parameters: string; + onParametersChange: (change: string) => void; } -export function ParametersTab({ contextSetup, onContextChange }: Props) { +export function ParametersTab({ parameters, onParametersChange }: Props) { return ( <> @@ -64,8 +64,8 @@ export function ParametersTab({ contextSetup, onContextChange }: Props) { onContextChange({ contextSetup: { params: value } })} + value={parameters} + onChange={onParametersChange} options={{ fontSize: 12, minimap: { @@ -76,6 +76,13 @@ export function ParametersTab({ contextSetup, onContextChange }: Props) { wrappingIndent: 'indent', automaticLayout: true, }} + editorDidMount={(editor: monaco.editor.IStandaloneCodeEditor) => { + // Updating tab size for the editor + const model = editor.getModel(); + if (model) { + model.updateOptions({ tabSize: 2 }); + } + }} /> diff --git a/x-pack/plugins/painless_lab/public/application/hooks/use_submit_code.ts b/x-pack/plugins/painless_lab/public/application/hooks/use_submit_code.ts index 87b2fb0a7b2bf..d4aa6c2af9f16 100644 --- a/x-pack/plugins/painless_lab/public/application/hooks/use_submit_code.ts +++ b/x-pack/plugins/painless_lab/public/application/hooks/use_submit_code.ts @@ -7,9 +7,10 @@ import { useRef, useCallback, useState } from 'react'; import { HttpSetup } from 'kibana/public'; import { debounce } from 'lodash'; -import { Response } from '../common/types'; + +import { API_BASE_PATH } from '../../../common/constants'; +import { Response, RequestPayloadConfig, PayloadFormat } from '../common/types'; import { buildRequestPayload } from '../lib/helpers'; -import { executeCode } from '../lib/execute_code'; const DEBOUNCE_MS = 800; @@ -20,7 +21,7 @@ export const useSubmitCode = (http: HttpSetup) => { const submit = useCallback( debounce( - async (code: string, context: string, contextSetup: Record) => { + async (config: RequestPayloadConfig) => { setInProgress(true); // Prevent an older request that resolves after a more recent request from clobbering it. @@ -28,10 +29,11 @@ export const useSubmitCode = (http: HttpSetup) => { const requestId = ++currentRequestIdRef.current; try { - localStorage.setItem('painlessLabCode', code); - localStorage.setItem('painlessLabContext', context); - localStorage.setItem('painlessLabContextSetup', JSON.stringify(contextSetup)); - const result = await executeCode(http, buildRequestPayload(code, context, contextSetup)); + const result = await http.post(`${API_BASE_PATH}/execute`, { + // Stringify the string, because http runs it through JSON.parse, and we want to actually + // send a JSON string. + body: JSON.stringify(buildRequestPayload(config, PayloadFormat.UGLY)), + }); if (currentRequestIdRef.current === requestId) { setResponse(result); diff --git a/x-pack/plugins/painless_lab/public/application/lib/execute_code.ts b/x-pack/plugins/painless_lab/public/application/lib/execute_code.ts deleted file mode 100644 index ea7adb79cdacb..0000000000000 --- a/x-pack/plugins/painless_lab/public/application/lib/execute_code.ts +++ /dev/null @@ -1,13 +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. - */ - -import { API_BASE_PATH } from '../../../common/constants'; - -export async function executeCode(http: any, payload: Record) { - return await http.post(`${API_BASE_PATH}/execute`, { - body: JSON.stringify(payload), - }); -} diff --git a/x-pack/plugins/painless_lab/public/application/lib/helpers.ts b/x-pack/plugins/painless_lab/public/application/lib/helpers.ts index d5c35476948d0..2152ee03a8af0 100644 --- a/x-pack/plugins/painless_lab/public/application/lib/helpers.ts +++ b/x-pack/plugins/painless_lab/public/application/lib/helpers.ts @@ -3,7 +3,8 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Response, Request, ExecutionError, JsonObject } from '../common/types'; + +import { RequestPayloadConfig, Response, ExecutionError, PayloadFormat } from '../common/types'; export function parseJSON(text: string) { try { @@ -13,51 +14,48 @@ export function parseJSON(text: string) { } } -export function buildRequestPayload( - code: string, - context: string, - contextSetup: Record -) { - const request: Request = { - script: { - source: code, - }, - }; - if (contextSetup.params) { - request.script.params = parseJSON(contextSetup?.params); - } - if (context === 'filter' || context === 'score') { - request.context = context; - request.context_setup = { - index: contextSetup.index, - document: parseJSON(contextSetup.document), - }; - return request; - } - - return request; +function prettifyPayload(payload = '', indentationLevel = 0) { + const indentation = new Array(indentationLevel + 1).join(' '); + return payload.replace(/\n/g, `\n${indentation}`); } /** - * Retrieves a value from the browsers local storage, provides a default - * if none is given. With the parse flag you can parse textual JSON to an object + * Values should be preserved as strings so that floating point precision, + * e.g. 1.0, is preserved instead of being coerced to an integer, e.g. 1. */ -export function getFromLocalStorage( - key: string, - defaultValue: string | JsonObject = '', - parse = false -) { - const value = localStorage.getItem(key); - if (value && parse) { - try { - return JSON.parse(value); - } catch (e) { - return defaultValue; - } - } else if (value) { - return value; +export function buildRequestPayload( + { code, context, parameters, index, document }: RequestPayloadConfig, + format: PayloadFormat = PayloadFormat.UGLY +): string { + const isAdvancedContext = context === 'filter' || context === 'score'; + const formattedCode = + format === PayloadFormat.UGLY ? JSON.stringify(code) : `"""${prettifyPayload(code, 4)}"""`; + const formattedParameters = + format === PayloadFormat.UGLY ? parameters : prettifyPayload(parameters, 4); + const formattedContext = format === PayloadFormat.UGLY ? context : prettifyPayload(context, 6); + const formattedIndex = format === PayloadFormat.UGLY ? index : prettifyPayload(index); + const formattedDocument = format === PayloadFormat.UGLY ? document : prettifyPayload(document, 4); + + const requestPayload = `{ + "script": { + "source": ${formattedCode}${ + parameters + ? `, + "params": ${formattedParameters}` + : `` + } + }${ + isAdvancedContext + ? `, + "context": "${formattedContext}", + "context_setup": { + "index": "${formattedIndex}", + "document": ${formattedDocument} + }` + : `` } - return defaultValue; +}`; + return requestPayload; } /** diff --git a/x-pack/plugins/painless_lab/server/routes/api/execute.ts b/x-pack/plugins/painless_lab/server/routes/api/execute.ts index caf6ce5cb9932..559d02aa08386 100644 --- a/x-pack/plugins/painless_lab/server/routes/api/execute.ts +++ b/x-pack/plugins/painless_lab/server/routes/api/execute.ts @@ -9,20 +9,7 @@ import { RouteDependencies } from '../../types'; import { API_BASE_PATH } from '../../../common/constants'; import { isEsError } from '../../lib'; -const bodySchema = schema.object({ - script: schema.object({ - source: schema.string(), - params: schema.maybe(schema.recordOf(schema.string(), schema.any())), - }), - context: schema.maybe(schema.string()), - context_setup: schema.maybe( - schema.object({ - params: schema.maybe(schema.any()), - document: schema.recordOf(schema.string(), schema.any()), - index: schema.string(), - }) - ), -}); +const bodySchema = schema.string(); export function registerExecuteRoute({ router, license }: RouteDependencies) { router.post( @@ -37,7 +24,6 @@ export function registerExecuteRoute({ router, license }: RouteDependencies) { try { const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; - const response = await callAsCurrentUser('scriptsPainlessExecute', { body, });