From 530b40921c6417ac4c18cc14a11b08f7eeecd2bf Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 13 Feb 2020 09:40:24 +0100 Subject: [PATCH 01/23] Create Painless Playground app (#54578) --- x-pack/.i18nrc.json | 1 + x-pack/index.js | 2 + .../painless_playground/common/constants.ts | 11 ++ .../plugins/painless_playground/index.ts | 50 +++++ .../public/common/constants.tsx | 63 ++++++ .../public/common/types.ts | 39 ++++ .../public/components/editor.tsx | 36 ++++ .../public/components/main_controls.tsx | 141 ++++++++++++++ .../components/output_pane/context_tab.tsx | 149 ++++++++++++++ .../public/components/output_pane/index.ts | 7 + .../components/output_pane/output_pane.tsx | 97 ++++++++++ .../components/output_pane/output_tab.tsx | 27 +++ .../components/output_pane/parameters_tab.tsx | 87 +++++++++ .../public/components/painless_playground.tsx | 176 +++++++++++++++++ .../public/components/request_flyout.tsx | 93 +++++++++ .../painless_playground/public/index.scss | 33 ++++ .../public/lib/execute_code.ts | 13 ++ .../painless_playground/public/lib/helpers.ts | 98 ++++++++++ .../painless_playground/public/register.tsx | 78 ++++++++ .../public/register_painless.ts | 182 ++++++++++++++++++ .../painless_playground/public/render_app.tsx | 27 +++ .../server/lib/check_license.ts | 46 +++++ .../server/lib/license_pre_routing_factory.ts | 24 +++ .../server/register_execute_route.ts | 31 +++ .../server/register_license_checker.ts | 21 ++ 25 files changed, 1532 insertions(+) create mode 100644 x-pack/legacy/plugins/painless_playground/common/constants.ts create mode 100644 x-pack/legacy/plugins/painless_playground/index.ts create mode 100644 x-pack/legacy/plugins/painless_playground/public/common/constants.tsx create mode 100644 x-pack/legacy/plugins/painless_playground/public/common/types.ts create mode 100644 x-pack/legacy/plugins/painless_playground/public/components/editor.tsx create mode 100644 x-pack/legacy/plugins/painless_playground/public/components/main_controls.tsx create mode 100644 x-pack/legacy/plugins/painless_playground/public/components/output_pane/context_tab.tsx create mode 100644 x-pack/legacy/plugins/painless_playground/public/components/output_pane/index.ts create mode 100644 x-pack/legacy/plugins/painless_playground/public/components/output_pane/output_pane.tsx create mode 100644 x-pack/legacy/plugins/painless_playground/public/components/output_pane/output_tab.tsx create mode 100644 x-pack/legacy/plugins/painless_playground/public/components/output_pane/parameters_tab.tsx create mode 100644 x-pack/legacy/plugins/painless_playground/public/components/painless_playground.tsx create mode 100644 x-pack/legacy/plugins/painless_playground/public/components/request_flyout.tsx create mode 100644 x-pack/legacy/plugins/painless_playground/public/index.scss create mode 100644 x-pack/legacy/plugins/painless_playground/public/lib/execute_code.ts create mode 100644 x-pack/legacy/plugins/painless_playground/public/lib/helpers.ts create mode 100644 x-pack/legacy/plugins/painless_playground/public/register.tsx create mode 100644 x-pack/legacy/plugins/painless_playground/public/register_painless.ts create mode 100644 x-pack/legacy/plugins/painless_playground/public/render_app.tsx create mode 100644 x-pack/legacy/plugins/painless_playground/server/lib/check_license.ts create mode 100644 x-pack/legacy/plugins/painless_playground/server/lib/license_pre_routing_factory.ts create mode 100644 x-pack/legacy/plugins/painless_playground/server/register_execute_route.ts create mode 100644 x-pack/legacy/plugins/painless_playground/server/register_license_checker.ts diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index f0b590f7ffd6c..83a418f09682c 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -29,6 +29,7 @@ "xpack.ml": "legacy/plugins/ml", "xpack.monitoring": "legacy/plugins/monitoring", "xpack.remoteClusters": "plugins/remote_clusters", + "xpack.painless_playground": "legacy/plugins/painless_playground", "xpack.reporting": ["plugins/reporting", "legacy/plugins/reporting"], "xpack.rollupJobs": "legacy/plugins/rollup", "xpack.searchProfiler": "plugins/searchprofiler", diff --git a/x-pack/index.js b/x-pack/index.js index f3f569e021070..f6368aeb8137e 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -12,6 +12,7 @@ import { security } from './legacy/plugins/security'; import { ml } from './legacy/plugins/ml'; import { tilemap } from './legacy/plugins/tilemap'; import { grokdebugger } from './legacy/plugins/grokdebugger'; +import { painlessPlayground } from './legacy/plugins/painless_playground'; import { dashboardMode } from './legacy/plugins/dashboard_mode'; import { logstash } from './legacy/plugins/logstash'; import { beats } from './legacy/plugins/beats_management'; @@ -51,6 +52,7 @@ module.exports = function(kibana) { ml(kibana), tilemap(kibana), grokdebugger(kibana), + painlessPlayground(kibana), dashboardMode(kibana), logstash(kibana), beats(kibana), diff --git a/x-pack/legacy/plugins/painless_playground/common/constants.ts b/x-pack/legacy/plugins/painless_playground/common/constants.ts new file mode 100644 index 0000000000000..d8807fcccccf0 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/common/constants.ts @@ -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 PLUGIN_ID = 'painless_playground'; + +export const API_ROUTE_EXECUTE = '/api/painless_playground/execute'; + +export const ADVANCED_SETTINGS_FLAG_NAME = 'devTools:enablePainlessPlayground'; diff --git a/x-pack/legacy/plugins/painless_playground/index.ts b/x-pack/legacy/plugins/painless_playground/index.ts new file mode 100644 index 0000000000000..caa2cff4eb826 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/index.ts @@ -0,0 +1,50 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { resolve } from 'path'; +import { PLUGIN_ID, ADVANCED_SETTINGS_FLAG_NAME } from './common/constants'; + +import { registerLicenseChecker } from './server/register_license_checker'; +import { registerExecuteRoute } from './server/register_execute_route'; +import { Legacy } from '../../../../kibana'; + +export const painlessPlayground = (kibana: any) => + new kibana.Plugin({ + id: PLUGIN_ID, + publicDir: resolve(__dirname, 'public'), + require: ['kibana', 'elasticsearch', 'xpack_main'], + configPrefix: 'xpack.painless_playground', + config(Joi: any) { + return Joi.object({ + enabled: Joi.boolean().default(true), + }).default(); + }, + uiExports: { + styleSheetPaths: resolve(__dirname, 'public/index.scss'), + devTools: [resolve(__dirname, 'public/register')], + }, + init: (server: Legacy.Server) => { + // Register feature flag + server.newPlatform.setup.core.uiSettings.register({ + [ADVANCED_SETTINGS_FLAG_NAME]: { + name: i18n.translate('xpack.painless_playground.devTools.painlessPlaygroundTitle', { + defaultMessage: 'Painless Playground', + }), + description: i18n.translate( + 'xpack.painless_playground.devTools.painlessPlaygroundDescription', + { + defaultMessage: 'Enable experimental Painless Playground.', + } + ), + value: false, + category: ['Dev Tools'], + }, + }); + + registerLicenseChecker(server); + registerExecuteRoute(server); + }, + }); diff --git a/x-pack/legacy/plugins/painless_playground/public/common/constants.tsx b/x-pack/legacy/plugins/painless_playground/public/common/constants.tsx new file mode 100644 index 0000000000000..ef3feb1738118 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/common/constants.tsx @@ -0,0 +1,63 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const defaultLabel = i18n.translate('xpack.painless_playground.contextDefaultLabel', { + defaultMessage: 'Basic', +}); + +const filterLabel = i18n.translate('xpack.painless_playground.contextFilterLabel', { + defaultMessage: 'Filter', +}); + +const scoreLabel = i18n.translate('xpack.painless_playground.contextScoreLabel', { + defaultMessage: 'Score', +}); + +export const painlessContextOptions = [ + { + value: 'painless_test', + inputDisplay: defaultLabel, + dropdownDisplay: ( + <> + {defaultLabel} + +

The script result will be converted to a string

+
+ + ), + }, + { + value: 'filter', + inputDisplay: filterLabel, + dropdownDisplay: ( + <> + {filterLabel} + +

Use the context of a filter’s script query

+
+ + ), + }, + { + value: 'score', + inputDisplay: scoreLabel, + dropdownDisplay: ( + <> + {scoreLabel} + +

+ Use the context of a cript_score function in function_score query +

+
+ + ), + }, +]; diff --git a/x-pack/legacy/plugins/painless_playground/public/common/types.ts b/x-pack/legacy/plugins/painless_playground/public/common/types.ts new file mode 100644 index 0000000000000..825b0a7848136 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/common/types.ts @@ -0,0 +1,39 @@ +/* + * 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 interface Request { + script: { + source: string; + params?: Record; + }; + context?: string; + context_setup?: { + document: Record; + index: string; + }; +} + +export interface Response { + error?: ExecutionError; + result?: string; +} + +export type ExecutionErrorScriptStack = string[]; + +export interface ExecutionError { + script_stack?: ExecutionErrorScriptStack; + caused_by?: { + type: string; + reason: string; + }; + message?: string; +} + +export type JsonArray = JsonValue[]; +export type JsonValue = null | boolean | number | string | JsonObject | JsonArray; + +export interface JsonObject { + [key: string]: JsonValue; +} diff --git a/x-pack/legacy/plugins/painless_playground/public/components/editor.tsx b/x-pack/legacy/plugins/painless_playground/public/components/editor.tsx new file mode 100644 index 0000000000000..f77f4ab888be6 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/components/editor.tsx @@ -0,0 +1,36 @@ +/* + * 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, EuiPageContent } from '@elastic/eui'; +import { CodeEditor } from '../../../../../../src/plugins/kibana_react/public'; + +interface Props { + code: string; + setCode: (code: string) => void; +} + +export function Editor({ code, setCode }: Props) { + return ( + + ); +} diff --git a/x-pack/legacy/plugins/painless_playground/public/components/main_controls.tsx b/x-pack/legacy/plugins/painless_playground/public/components/main_controls.tsx new file mode 100644 index 0000000000000..98e8f25f171b1 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/components/main_controls.tsx @@ -0,0 +1,141 @@ +/* + * 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, { useState } from 'react'; +import { + EuiPopover, + EuiBottomBar, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +interface Props { + toggleRequestFlyout: () => void; + isRequestFlyoutOpen: boolean; + isLoading: boolean; + reset: () => void; +} + +export function MainControls({ + toggleRequestFlyout, + isRequestFlyoutOpen, + isLoading, + reset, +}: Props) { + const [isHelpOpen, setIsHelpOpen] = useState(false); + + const items = [ + setIsHelpOpen(false)} + > + {i18n.translate('xpack.painless_playground.walkthroughButtonLabel', { + defaultMessage: 'Walkthrough', + })} + , + + setIsHelpOpen(false)} + > + {i18n.translate('xpack.painless_playground.apiReferenceButtonLabel', { + defaultMessage: 'API reference', + })} + , + + setIsHelpOpen(false)} + > + {i18n.translate('xpack.painless_playground.languageSpecButtonLabel', { + defaultMessage: 'Language spec', + })} + , + + { + reset(); + setIsHelpOpen(false); + }} + > + {i18n.translate('xpack.painless_playground.resetButtonLabel', { + defaultMessage: 'Reset script', + })} + , + ]; + + return ( + <> +
+ + + + + + + setIsHelpOpen(!isHelpOpen)} + > + {i18n.translate('xpack.painless_playground.helpButtonLabel', { + defaultMessage: 'Help', + })} + + } + isOpen={isHelpOpen} + closePopover={() => setIsHelpOpen(false)} + panelPaddingSize="none" + withTitle + anchorPosition="upRight" + > + + + + + + + + {isRequestFlyoutOpen + ? i18n.translate('xpack.painless_playground.hideRequestButtonLabel', { + defaultMessage: 'Hide API request', + }) + : i18n.translate('xpack.painless_playground.showRequestButtonLabel', { + defaultMessage: 'Show API request', + })} + + + + + + ); +} diff --git a/x-pack/legacy/plugins/painless_playground/public/components/output_pane/context_tab.tsx b/x-pack/legacy/plugins/painless_playground/public/components/output_pane/context_tab.tsx new file mode 100644 index 0000000000000..ec0f27c005b96 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/components/output_pane/context_tab.tsx @@ -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 React from 'react'; +import { + EuiFieldText, + EuiFormRow, + EuiPanel, + EuiSpacer, + EuiIcon, + EuiToolTip, + EuiLink, + EuiText, + EuiSuperSelect, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; + +import { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; +import { painlessContextOptions } from '../../common/constants'; + +interface Props { + context: string; + contextSetup: Record; + setContext: (context: string) => void; + setContextSetup: (contextSetup: Record) => void; + renderMainControls: () => React.ReactElement; +} + +export function ContextTab({ context, contextSetup, setContext, setContextSetup }: Props) { + return ( + <> + + + + {' '} + + + + } + labelAppend={ + + + {i18n.translate('xpack.painless_playground.contextFieldDocLinkText', { + defaultMessage: 'Context docs', + })} + + + } + fullWidth + > + setContext(value)} + itemLayoutAlign="top" + hasDividers + fullWidth + /> + + + {['filter', 'score'].indexOf(context) !== -1 && ( + + + {' '} + + + + } + fullWidth + > + + setContextSetup(Object.assign({}, contextSetup, { index: e.target.value })) + } + /> + + )} + {['filter', 'score'].indexOf(context) !== -1 && ( + + + {' '} + + + + } + fullWidth + > + + + setContextSetup(Object.assign({}, contextSetup, { document: value })) + } + options={{ + fontSize: 12, + minimap: { + enabled: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + automaticLayout: true, + }} + /> + + + )} + + ); +} diff --git a/x-pack/legacy/plugins/painless_playground/public/components/output_pane/index.ts b/x-pack/legacy/plugins/painless_playground/public/components/output_pane/index.ts new file mode 100644 index 0000000000000..85b7a7816b5aa --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/components/output_pane/index.ts @@ -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 { OutputPane } from './output_pane'; diff --git a/x-pack/legacy/plugins/painless_playground/public/components/output_pane/output_pane.tsx b/x-pack/legacy/plugins/painless_playground/public/components/output_pane/output_pane.tsx new file mode 100644 index 0000000000000..69393ca47a66c --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/components/output_pane/output_pane.tsx @@ -0,0 +1,97 @@ +/* + * 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 { + EuiIcon, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPanel, + EuiTabbedContent, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { formatJson, formatResponse } from '../../lib/helpers'; +import { Response } from '../../common/types'; +import { OutputTab } from './output_tab'; +import { ParametersTab } from './parameters_tab'; +import { ContextTab } from './context_tab'; + +export function OutputPane({ + response, + context, + contextSetup, + setContext, + setContextSetup, + isLoading, +}: { + response?: Response; +}) { + const outputTabLabel = ( + + + {isLoading ? ( + + ) : response.error ? ( + + ) : ( + + )} + + + + {i18n.translate('xpack.painless_playground.outputTabLabel', { + defaultMessage: 'Output', + })} + + + ); + + return ( + + , + }, + { + id: 'parameters', + name: i18n.translate('xpack.painless_playground.parametersTabLabel', { + defaultMessage: 'Parameters', + }), + content: ( + + ), + }, + { + id: 'context', + name: i18n.translate('xpack.painless_playground.contextTabLabel', { + defaultMessage: 'Context', + }), + content: ( + + ), + }, + ]} + /> + + ); +} diff --git a/x-pack/legacy/plugins/painless_playground/public/components/output_pane/output_tab.tsx b/x-pack/legacy/plugins/painless_playground/public/components/output_pane/output_tab.tsx new file mode 100644 index 0000000000000..4b69e7969c924 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/components/output_pane/output_tab.tsx @@ -0,0 +1,27 @@ +/* + * 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 { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { formatResponse } from '../../lib/helpers'; +import { Response } from '../../common/types'; + +interface Props { + response?: Response; +} + +export function OutputTab({ response = {} }: Props) { + return ( + <> + + + {formatResponse(response.success || response.error)} + + + ); +} diff --git a/x-pack/legacy/plugins/painless_playground/public/components/output_pane/parameters_tab.tsx b/x-pack/legacy/plugins/painless_playground/public/components/output_pane/parameters_tab.tsx new file mode 100644 index 0000000000000..02dd5b0971e2c --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/components/output_pane/parameters_tab.tsx @@ -0,0 +1,87 @@ +/* + * 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 { + EuiFormRow, + EuiPanel, + EuiSpacer, + EuiIcon, + EuiToolTip, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; +import { painlessContextOptions } from '../../common/constants'; + +interface Props { + context: string; + contextSetup: Record; + setContext: (context: string) => void; + setContextSetup: (contextSetup: Record) => void; + renderMainControls: () => React.ReactElement; +} + +export function ParametersTab({ context, contextSetup, setContext, setContextSetup }: Props) { + return ( + <> + + + + {' '} + + + + } + fullWidth + labelAppend={ + + + {i18n.translate('xpack.painless_playground.parametersFieldDocLinkText', { + defaultMessage: 'Parameters docs', + })} + + + } + helpText={i18n.translate('xpack.painless_playground.helpIconAriaLabel', { + defaultMessage: 'Use JSON format', + })} + > + + setContextSetup({ params: value })} + options={{ + fontSize: 12, + minimap: { + enabled: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + automaticLayout: true, + }} + /> + + + + ); +} diff --git a/x-pack/legacy/plugins/painless_playground/public/components/painless_playground.tsx b/x-pack/legacy/plugins/painless_playground/public/components/painless_playground.tsx new file mode 100644 index 0000000000000..1a94d42606120 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/components/painless_playground.tsx @@ -0,0 +1,176 @@ +/* + * 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, { useState, useEffect } from 'react'; +import { debounce } from 'lodash'; +import { + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiTabbedContent, + EuiTitle, + EuiSpacer, + EuiPageContent, + EuiFlyout, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { buildRequestPayload, formatJson, getFromLocalStorage } from '../lib/helpers'; +import { Request, Response } from '../common/types'; +import { OutputPane } from './output_pane'; +import { MainControls } from './main_controls'; +import { Editor } from './editor'; +import { RequestFlyout } from './request_flyout'; + +let _mostRecentRequestId = 0; + +const submit = async (code, context, contextSetup, executeCode, setResponse, setIsLoading) => { + // Prevent an older request that resolves after a more recent request from clobbering it. + // We store the resulting ID in this closure for comparison when the request resolves. + const requestId = ++_mostRecentRequestId; + setIsLoading(true); + + try { + localStorage.setItem('painlessPlaygroundCode', code); + localStorage.setItem('painlessPlaygroundContext', context); + localStorage.setItem('painlessPlaygroundContextSetup', JSON.stringify(contextSetup)); + const response = await executeCode(buildRequestPayload(code, context, contextSetup)); + + if (_mostRecentRequestId === requestId) { + if (response.error) { + setResponse({ + success: undefined, + error: response.error, + }); + } else { + setResponse({ + success: response, + error: undefined, + }); + } + setIsLoading(false); + } + } catch (error) { + if (_mostRecentRequestId === requestId) { + setResponse({ + success: undefined, + error: { error }, + }); + setIsLoading(false); + } + } +}; + +const debouncedSubmit = debounce(submit, 800); + +// Render a heart as an example. +const exampleScript = ` +def result = ''; +int charCount = 0; + +int n = 10; +int threshold = n*n/2; +int dimension = 3*n/2; + +for (int i = -dimension; i <= n; i++) { + int a = -n/2-i; + + for (int j = -dimension; j <= dimension; j++) { + int b = n/2-j; + int c = -n/2-j; + + def isHeartVentricles = (Math.abs(i) + Math.abs(j) < n); + def isRightAtrium = ((a * a) + (b * b) <= threshold); + def isLeftAtrium = ((a * a) + (c * c) <= threshold); + + if (isHeartVentricles || isRightAtrium || isLeftAtrium) { + result += "* "; + } else { + result += ". "; + } + + // Make sure the heart doesn't deform as the container changes width. + charCount++; + if (charCount % 31 === 0) { + result += "\\\\n"; + } + } +} + +return result; +`; + +export function PainlessPlayground({ + executeCode, +}: { + executeCode: (payload: Request) => Promise; +}) { + const [code, setCode] = useState(getFromLocalStorage('painlessPlaygroundCode', exampleScript)); + const [response, setResponse] = useState({ error: undefined, success: undefined }); + const [isRequestFlyoutOpen, setRequestFlyoutOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const [context, setContext] = useState( + getFromLocalStorage('painlessPlaygroundContext', 'painless_test_without_params') + ); + + const [contextSetup, setContextSetup] = useState( + getFromLocalStorage('painlessPlaygroundContextSetup', {}, true) + ); + + // Live-update the output as the user changes the input code. + useEffect(() => { + debouncedSubmit(code, context, contextSetup, executeCode, setResponse, setIsLoading); + }, [code, context, contextSetup, executeCode]); + + const toggleRequestFlyout = () => { + setRequestFlyoutOpen(!isRequestFlyoutOpen); + }; + + return ( + <> + + + +

+ {i18n.translate('xpack.painless_playground.title', { + defaultMessage: 'Painless Playground', + })} +

+
+ + +
+ + + + +
+ + submit(code, context, contextSetup, executeCode, setResponse)} + isLoading={isLoading} + toggleRequestFlyout={toggleRequestFlyout} + isRequestFlyoutOpen={isRequestFlyoutOpen} + reset={() => setCode(exampleScript)} + /> + + {isRequestFlyoutOpen && ( + setRequestFlyoutOpen(false)} + requestBody={formatJson(buildRequestPayload(code, context, contextSetup))} + response={formatJson(response.success || response.error)} + /> + )} + + ); +} diff --git a/x-pack/legacy/plugins/painless_playground/public/components/request_flyout.tsx b/x-pack/legacy/plugins/painless_playground/public/components/request_flyout.tsx new file mode 100644 index 0000000000000..60ecdd5d4799b --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/components/request_flyout.tsx @@ -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, { useState, useEffect } from 'react'; +import { + EuiCodeBlock, + EuiTabbedContent, + EuiTitle, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export function RequestFlyout({ + onClose, + requestBody, + response, +}: { + onClose: any; + requestBody: string; + response?: string; +}) { + return ( + + + + + {/* We need an extra div to get out of flex grow */} +
+ +

+ {i18n.translate('xpack.painless_playground.flyoutTitle', { + defaultMessage: 'API request', + })} +

+
+
+
+ + + + {i18n.translate('xpack.painless_playground.flyoutDocLink', { + defaultMessage: 'API documentation', + })} + + +
+
+ + + + {'POST _scripts/painless/_execute\n'} + {requestBody} + + ), + }, + { + id: 'response', + name: 'Response', + content: ( + + {response} + + ), + }, + ]} + /> + +
+ + + ); +} diff --git a/x-pack/legacy/plugins/painless_playground/public/index.scss b/x-pack/legacy/plugins/painless_playground/public/index.scss new file mode 100644 index 0000000000000..46b834c83786c --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/index.scss @@ -0,0 +1,33 @@ +// Import the EUI global scope so we can use EUI constants +@import 'src/legacy/ui/public/styles/_styling_constants'; + +/** + * 1. This is a very brittle way of preventing the editor and other content from disappearing + * behind the bottom bar. + */ +.painlessPlaygroundBottomBarPlaceholder { + height: $euiSize * 2; /* [1] */ +} + +.painlessPlaygroundRightPane { + border-right: none; + border-top: none; + border-bottom: none; + border-radius: 0; + padding-top: 0; + height: 100%; +} + +.painlessPlaygroundRightPane__tabs { + display: flex; + flex-direction: column; + height: 100%; + + [role="tabpanel"] { + height: 100%; + } +} + +.painlessPlayground__betaLabelContainer { + line-height: 0; +} diff --git a/x-pack/legacy/plugins/painless_playground/public/lib/execute_code.ts b/x-pack/legacy/plugins/painless_playground/public/lib/execute_code.ts new file mode 100644 index 0000000000000..c8b9f2724d38b --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/lib/execute_code.ts @@ -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. + */ + +import { API_ROUTE_EXECUTE } from '../../common/constants'; + +export async function executeCode(http: any, payload: Record) { + return await http.post(API_ROUTE_EXECUTE, { + body: JSON.stringify(payload), + }); +} diff --git a/x-pack/legacy/plugins/painless_playground/public/lib/helpers.ts b/x-pack/legacy/plugins/painless_playground/public/lib/helpers.ts new file mode 100644 index 0000000000000..ddc633b2e99d7 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/lib/helpers.ts @@ -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 { Response, Request, ExecutionError, JsonObject } from '../common/types'; + +export function parseJSON(text: string) { + try { + return JSON.parse(text); + } catch (e) { + return {}; + } +} + +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; +} + +/** + * 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 + */ +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; + } + return defaultValue; +} + +/** + * Stringify a given object to JSON in a formatted way + */ +export function formatJson(json: unknown): string { + try { + return JSON.stringify(json, null, 2); + } catch (e) { + return `Invalid JSON ${String(json)}`; + } +} + +export function formatResponse(response?: Response): string { + if (!response) { + return ''; + } + + if (typeof response.result === 'string') { + return response.result.replace(/\\n/g, '\n'); + } else if (response.error) { + return formatExecutionError(response.error); + } + return formatJson(response); +} + +export function formatExecutionError(json: ExecutionError): string { + if (json.script_stack && json.caused_by) { + return `Unhandled Exception ${json.caused_by.type} + +${json.caused_by.reason} + +Located at: +${formatJson(json.script_stack)} +`; + } + return formatJson(json); +} diff --git a/x-pack/legacy/plugins/painless_playground/public/register.tsx b/x-pack/legacy/plugins/painless_playground/public/register.tsx new file mode 100644 index 0000000000000..4908eeb758114 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/register.tsx @@ -0,0 +1,78 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +// @ts-ignore +import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; +import { npSetup, npStart } from 'ui/new_platform'; +import { registerPainless } from './register_painless'; +import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; +import { ADVANCED_SETTINGS_FLAG_NAME } from '../common/constants'; + +npSetup.plugins.home.featureCatalogue.register({ + id: 'painless_playground', + title: i18n.translate('xpack.painless_playground.registryProviderTitle', { + defaultMessage: 'Painless Playground (beta)', + }), + description: i18n.translate('xpack.painless_playground.registryProviderDescription', { + defaultMessage: 'Simulate and debug painless code', + }), + icon: '', + path: '/app/kibana#/dev_tools/painless_playground', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, +}); + +npSetup.core.uiSettings.get$(ADVANCED_SETTINGS_FLAG_NAME, false).subscribe(value => { + // eslint-disable-next-line + console.log('use this to figure out whether we should register', value); +}); + +npSetup.plugins.devTools.register({ + order: 7, + title: ( + + + {i18n.translate('xpack.painless_playground.displayName', { + defaultMessage: 'Painless Playground', + })} + + + + + + + ), + enableRouting: false, + disabled: false, + tooltipContent: xpackInfo.get('features.painlessPlayground.message'), + async mount(context, { element }) { + registerPainless(); + + const licenseCheck = { + showPage: xpackInfo.get('features.painlessPlayground.enableLink'), + message: xpackInfo.get('features.painlessPlayground.message'), + }; + + if (!licenseCheck.showPage) { + npStart.core.notifications.toasts.addDanger(licenseCheck.message); + window.location.hash = '/dev_tools'; + return () => {}; + } + + const { renderApp } = await import('./render_app'); + return renderApp(element, npStart.core); + }, +}); diff --git a/x-pack/legacy/plugins/painless_playground/public/register_painless.ts b/x-pack/legacy/plugins/painless_playground/public/register_painless.ts new file mode 100644 index 0000000000000..700e6d93dad2a --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/public/register_painless.ts @@ -0,0 +1,182 @@ +/* + * 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 * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; +export const LANGUAGE_ID = 'painless'; + +/** + * Extends the default type for a Monarch language so we can use + * attribute references (like @keywords to reference the keywords list) + * in the defined tokenizer + */ +interface Language extends monaco.languages.IMonarchLanguage { + default: string; + brackets: any; + keywords: string[]; + symbols: RegExp; + escapes: RegExp; + digits: RegExp; + primitives: string[]; + octaldigits: RegExp; + binarydigits: RegExp; + constants: string[]; + operators: string[]; +} + +function getPainlessLanguage() { + return { + default: '', + // painless does not use < >, so we define our own + brackets: [ + ['{', '}', 'delimiter.curly'], + ['[', ']', 'delimiter.square'], + ['(', ')', 'delimiter.parenthesis'], + ], + keywords: [ + 'if', + 'in', + 'else', + 'while', + 'do', + 'for', + 'continue', + 'break', + 'return', + 'new', + 'try', + 'catch', + 'throw', + 'this', + 'instanceof', + ], + primitives: ['void', 'boolean', 'byte', 'short', 'char', 'int', 'long', 'float', 'double'], + constants: ['true', 'false'], + operators: [ + '=', + '>', + '<', + '!', + '~', + '?', + '?:', + '?.', + ':', + '==', + '===', + '<=', + '>=', + '!=', + '!==', + '&&', + '||', + '++', + '--', + '+', + '-', + '*', + '/', + '&', + '|', + '^', + '%', + '<<', + '>>', + '>>>', + '+=', + '-=', + '*=', + '/=', + '&=', + '|=', + '^=', + '%=', + '<<=', + '>>=', + '>>>=', + '->', + '::', + '=~', + '==~', + ], + symbols: /[=> + + executeCode(http, payload)} /> + + , + element + ); + return () => unmountComponentAtNode(element); +} diff --git a/x-pack/legacy/plugins/painless_playground/server/lib/check_license.ts b/x-pack/legacy/plugins/painless_playground/server/lib/check_license.ts new file mode 100644 index 0000000000000..f6a815abe9a47 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/server/lib/check_license.ts @@ -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 { i18n } from '@kbn/i18n'; + +export function checkLicense(xpackLicenseInfo: any) { + // If, for some reason, we cannot get the license information + // from Elasticsearch, assume worst case and disable the Watcher UI + if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { + return { + enableLink: false, + enableAPIRoute: false, + message: i18n.translate('xpack.painless_playground.unavailableLicenseInformationMessage', { + defaultMessage: + 'You cannot use the Painless Playground because license information is not available at this time.', + }), + }; + } + + const isLicenseActive = xpackLicenseInfo.license.isActive(); + const licenseType = xpackLicenseInfo.license.getType(); + + // License is not valid + if (!isLicenseActive) { + return { + enableLink: false, + enableAPIRoute: false, + message: i18n.translate('xpack.painless_playground.licenseHasExpiredMessage', { + defaultMessage: + 'You cannot use the Painless Playground because your {licenseType} license has expired.', + values: { + licenseType, + }, + }), + }; + } + + // License is valid and active + return { + enableLink: true, + enableAPIRoute: true, + }; +} diff --git a/x-pack/legacy/plugins/painless_playground/server/lib/license_pre_routing_factory.ts b/x-pack/legacy/plugins/painless_playground/server/lib/license_pre_routing_factory.ts new file mode 100644 index 0000000000000..387a263114a6e --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/server/lib/license_pre_routing_factory.ts @@ -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 Boom from 'boom'; +import { PLUGIN_ID } from '../../common/constants'; +import { ServerFacade } from '../../../index_management'; + +export const licensePreRoutingFactory = (server: ServerFacade) => { + const xpackMainPlugin = server.plugins.xpack_main; + + // License checking and enable/disable logic + function licensePreRouting() { + const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN_ID).getLicenseCheckResults(); + if (!licenseCheckResults.enableAPIRoute) { + throw Boom.forbidden(licenseCheckResults.message); + } + + return null; + } + + return licensePreRouting; +}; diff --git a/x-pack/legacy/plugins/painless_playground/server/register_execute_route.ts b/x-pack/legacy/plugins/painless_playground/server/register_execute_route.ts new file mode 100644 index 0000000000000..e4ffad9c21d60 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/server/register_execute_route.ts @@ -0,0 +1,31 @@ +/* + * 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 { ServerRoute } from 'hapi'; +import { licensePreRoutingFactory } from './lib/license_pre_routing_factory'; +import { Legacy } from '../../../../../kibana'; +import { API_ROUTE_EXECUTE } from '../common/constants'; + +export function registerExecuteRoute(server: any) { + const licensePreRouting = licensePreRoutingFactory(server); + + server.route({ + path: API_ROUTE_EXECUTE, + method: 'POST', + handler: (request: Legacy.Request) => { + const cluster = server.plugins.elasticsearch.getCluster('data'); + return cluster + .callWithRequest(request, 'scriptsPainlessExecute', { + body: request.payload, + }) + .catch((e: any) => { + return e.body; + }); + }, + config: { + pre: [licensePreRouting], + }, + } as ServerRoute); +} diff --git a/x-pack/legacy/plugins/painless_playground/server/register_license_checker.ts b/x-pack/legacy/plugins/painless_playground/server/register_license_checker.ts new file mode 100644 index 0000000000000..1ec5b33c4d9f9 --- /dev/null +++ b/x-pack/legacy/plugins/painless_playground/server/register_license_checker.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ +// @ts-ignore +import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; +import { checkLicense } from './lib/check_license'; +import { PLUGIN_ID } from '../common/constants'; + +export function registerLicenseChecker(server: any) { + const xpackMainPlugin = server.plugins.xpack_main; + const plugin = server.plugins[PLUGIN_ID]; + + mirrorPluginStatus(xpackMainPlugin, plugin); + xpackMainPlugin.status.once('green', () => { + // Register a function that is called whenever the xpack info changes, + // to re-compute the license check results for this plugin + xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense); + }); +} From 455ce24154f161e23fd3109d781f92443e28a027 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Tue, 18 Feb 2020 17:56:00 -0800 Subject: [PATCH 02/23] Replace heart script with smiley face script. (#57755) --- .../public/components/painless_playground.tsx | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/x-pack/legacy/plugins/painless_playground/public/components/painless_playground.tsx b/x-pack/legacy/plugins/painless_playground/public/components/painless_playground.tsx index 1a94d42606120..b927637cb0418 100644 --- a/x-pack/legacy/plugins/painless_playground/public/components/painless_playground.tsx +++ b/x-pack/legacy/plugins/painless_playground/public/components/painless_playground.tsx @@ -67,33 +67,49 @@ const debouncedSubmit = debounce(submit, 800); // Render a heart as an example. const exampleScript = ` +boolean isInCircle(def posX, def posY, def circleX, def circleY, def radius) { + double distanceFromCircleCenter = Math.sqrt(Math.pow(circleX - posX, 2) + Math.pow(circleY - posY, 2)); + return distanceFromCircleCenter <= radius; +} + +boolean isOnCircle(def posX, def posY, def circleX, def circleY, def radius, def thickness) { + double distanceFromCircleCenter = Math.sqrt(Math.pow(circleX - posX, 2) + Math.pow(circleY - posY, 2)); + return ( + distanceFromCircleCenter >= radius - thickness + && distanceFromCircleCenter <= radius + thickness + ); +} + def result = ''; int charCount = 0; -int n = 10; -int threshold = n*n/2; -int dimension = 3*n/2; +int width = 31; +int height = 25; -for (int i = -dimension; i <= n; i++) { - int a = -n/2-i; +int eyePositionX = 8; +int eyePositionY = 6; +int eyeSize = 3; +int mouthSize = 11; +int mouthPositionX = width / 2; +int mouthPositionY = 9; - for (int j = -dimension; j <= dimension; j++) { - int b = n/2-j; - int c = -n/2-j; +for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + boolean isLeftEye = isInCircle(x, y, eyePositionX, eyePositionY, eyeSize); + boolean isRightEye = isInCircle(x, y, width - eyePositionX - 1, eyePositionY, eyeSize); + boolean isMouth = isOnCircle(x, y, mouthPositionX, mouthPositionY, mouthSize, 1) && y > mouthPositionY + 3; - def isHeartVentricles = (Math.abs(i) + Math.abs(j) < n); - def isRightAtrium = ((a * a) + (b * b) <= threshold); - def isLeftAtrium = ((a * a) + (c * c) <= threshold); - - if (isHeartVentricles || isRightAtrium || isLeftAtrium) { - result += "* "; + if (isLeftEye || isRightEye || isMouth) { + result += "*"; } else { - result += ". "; + result += "."; } - // Make sure the heart doesn't deform as the container changes width. + result += " "; + + // Make sure the smiley face doesn't deform as the container changes width. charCount++; - if (charCount % 31 === 0) { + if (charCount % width === 0) { result += "\\\\n"; } } From b8bd2e63c84c033f917af75c66347723c8527ab3 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 19 Feb 2020 16:10:45 -0800 Subject: [PATCH 03/23] Rename Painless Playground -> Painless Lab. (#57545) --- x-pack/index.js | 4 +-- .../common/constants.ts | 6 ++--- .../index.ts | 17 +++++------- .../public/common/constants.tsx | 6 ++--- .../public/common/types.ts | 0 .../public/components/editor.tsx | 0 .../public/components/main.tsx} | 22 +++++++--------- .../public/components/main_controls.tsx | 18 ++++++------- .../components/output_pane/context_tab.tsx | 17 +++++------- .../public/components/output_pane/index.ts | 0 .../components/output_pane/output_pane.tsx | 10 +++---- .../components/output_pane/output_tab.tsx | 0 .../components/output_pane/parameters_tab.tsx | 8 +++--- .../public/components/request_flyout.tsx | 6 ++--- .../public/index.scss | 8 +++--- .../public/lib/execute_code.ts | 0 .../public/lib/helpers.ts | 0 .../public/register.tsx | 26 +++++++++---------- .../public/register_painless.ts | 0 .../public/render_app.tsx | 4 +-- .../server/lib/check_license.ts | 8 +++--- .../server/lib/license_pre_routing_factory.ts | 0 .../server/register_execute_route.ts | 0 .../server/register_license_checker.ts | 0 24 files changed, 75 insertions(+), 85 deletions(-) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/common/constants.ts (69%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/index.ts (73%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/common/constants.tsx (85%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/common/types.ts (100%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/components/editor.tsx (100%) rename x-pack/legacy/plugins/{painless_playground/public/components/painless_playground.tsx => painless_lab/public/components/main.tsx} (89%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/components/main_controls.tsx (84%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/components/output_pane/context_tab.tsx (85%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/components/output_pane/index.ts (100%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/components/output_pane/output_pane.tsx (87%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/components/output_pane/output_tab.tsx (100%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/components/output_pane/parameters_tab.tsx (88%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/components/request_flyout.tsx (91%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/index.scss (77%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/lib/execute_code.ts (100%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/lib/helpers.ts (100%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/register.tsx (68%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/register_painless.ts (100%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/public/render_app.tsx (85%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/server/lib/check_license.ts (73%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/server/lib/license_pre_routing_factory.ts (100%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/server/register_execute_route.ts (100%) rename x-pack/legacy/plugins/{painless_playground => painless_lab}/server/register_license_checker.ts (100%) diff --git a/x-pack/index.js b/x-pack/index.js index f6368aeb8137e..c06e2536b7328 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -12,7 +12,7 @@ import { security } from './legacy/plugins/security'; import { ml } from './legacy/plugins/ml'; import { tilemap } from './legacy/plugins/tilemap'; import { grokdebugger } from './legacy/plugins/grokdebugger'; -import { painlessPlayground } from './legacy/plugins/painless_playground'; +import { painlessLab } from './legacy/plugins/painless_lab'; import { dashboardMode } from './legacy/plugins/dashboard_mode'; import { logstash } from './legacy/plugins/logstash'; import { beats } from './legacy/plugins/beats_management'; @@ -52,7 +52,7 @@ module.exports = function(kibana) { ml(kibana), tilemap(kibana), grokdebugger(kibana), - painlessPlayground(kibana), + painlessLab(kibana), dashboardMode(kibana), logstash(kibana), beats(kibana), diff --git a/x-pack/legacy/plugins/painless_playground/common/constants.ts b/x-pack/legacy/plugins/painless_lab/common/constants.ts similarity index 69% rename from x-pack/legacy/plugins/painless_playground/common/constants.ts rename to x-pack/legacy/plugins/painless_lab/common/constants.ts index d8807fcccccf0..771d4979143fd 100644 --- a/x-pack/legacy/plugins/painless_playground/common/constants.ts +++ b/x-pack/legacy/plugins/painless_lab/common/constants.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export const PLUGIN_ID = 'painless_playground'; +export const PLUGIN_ID = 'painlessLab'; -export const API_ROUTE_EXECUTE = '/api/painless_playground/execute'; +export const API_ROUTE_EXECUTE = '/api/painless_lab/execute'; -export const ADVANCED_SETTINGS_FLAG_NAME = 'devTools:enablePainlessPlayground'; +export const ADVANCED_SETTINGS_FLAG_NAME = 'devTools:enablePainlessLab'; diff --git a/x-pack/legacy/plugins/painless_playground/index.ts b/x-pack/legacy/plugins/painless_lab/index.ts similarity index 73% rename from x-pack/legacy/plugins/painless_playground/index.ts rename to x-pack/legacy/plugins/painless_lab/index.ts index caa2cff4eb826..3be4a25508bad 100644 --- a/x-pack/legacy/plugins/painless_playground/index.ts +++ b/x-pack/legacy/plugins/painless_lab/index.ts @@ -11,12 +11,12 @@ import { registerLicenseChecker } from './server/register_license_checker'; import { registerExecuteRoute } from './server/register_execute_route'; import { Legacy } from '../../../../kibana'; -export const painlessPlayground = (kibana: any) => +export const painlessLab = (kibana: any) => new kibana.Plugin({ id: PLUGIN_ID, publicDir: resolve(__dirname, 'public'), require: ['kibana', 'elasticsearch', 'xpack_main'], - configPrefix: 'xpack.painless_playground', + configPrefix: 'xpack.painless_lab', config(Joi: any) { return Joi.object({ enabled: Joi.boolean().default(true), @@ -30,15 +30,12 @@ export const painlessPlayground = (kibana: any) => // Register feature flag server.newPlatform.setup.core.uiSettings.register({ [ADVANCED_SETTINGS_FLAG_NAME]: { - name: i18n.translate('xpack.painless_playground.devTools.painlessPlaygroundTitle', { - defaultMessage: 'Painless Playground', + name: i18n.translate('xpack.painlessLab.devTools.painlessLabTitle', { + defaultMessage: 'Painless Lab', + }), + description: i18n.translate('xpack.painlessLab.devTools.painlessLabDescription', { + defaultMessage: 'Enable experimental Painless Lab.', }), - description: i18n.translate( - 'xpack.painless_playground.devTools.painlessPlaygroundDescription', - { - defaultMessage: 'Enable experimental Painless Playground.', - } - ), value: false, category: ['Dev Tools'], }, diff --git a/x-pack/legacy/plugins/painless_playground/public/common/constants.tsx b/x-pack/legacy/plugins/painless_lab/public/common/constants.tsx similarity index 85% rename from x-pack/legacy/plugins/painless_playground/public/common/constants.tsx rename to x-pack/legacy/plugins/painless_lab/public/common/constants.tsx index ef3feb1738118..4ff1f9fe340bf 100644 --- a/x-pack/legacy/plugins/painless_playground/public/common/constants.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/common/constants.tsx @@ -9,15 +9,15 @@ import React from 'react'; import { EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -const defaultLabel = i18n.translate('xpack.painless_playground.contextDefaultLabel', { +const defaultLabel = i18n.translate('xpack.painlessLab.contextDefaultLabel', { defaultMessage: 'Basic', }); -const filterLabel = i18n.translate('xpack.painless_playground.contextFilterLabel', { +const filterLabel = i18n.translate('xpack.painlessLab.contextFilterLabel', { defaultMessage: 'Filter', }); -const scoreLabel = i18n.translate('xpack.painless_playground.contextScoreLabel', { +const scoreLabel = i18n.translate('xpack.painlessLab.contextScoreLabel', { defaultMessage: 'Score', }); diff --git a/x-pack/legacy/plugins/painless_playground/public/common/types.ts b/x-pack/legacy/plugins/painless_lab/public/common/types.ts similarity index 100% rename from x-pack/legacy/plugins/painless_playground/public/common/types.ts rename to x-pack/legacy/plugins/painless_lab/public/common/types.ts diff --git a/x-pack/legacy/plugins/painless_playground/public/components/editor.tsx b/x-pack/legacy/plugins/painless_lab/public/components/editor.tsx similarity index 100% rename from x-pack/legacy/plugins/painless_playground/public/components/editor.tsx rename to x-pack/legacy/plugins/painless_lab/public/components/editor.tsx diff --git a/x-pack/legacy/plugins/painless_playground/public/components/painless_playground.tsx b/x-pack/legacy/plugins/painless_lab/public/components/main.tsx similarity index 89% rename from x-pack/legacy/plugins/painless_playground/public/components/painless_playground.tsx rename to x-pack/legacy/plugins/painless_lab/public/components/main.tsx index b927637cb0418..f1cdf8820c2b7 100644 --- a/x-pack/legacy/plugins/painless_playground/public/components/painless_playground.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/main.tsx @@ -33,9 +33,9 @@ const submit = async (code, context, contextSetup, executeCode, setResponse, set setIsLoading(true); try { - localStorage.setItem('painlessPlaygroundCode', code); - localStorage.setItem('painlessPlaygroundContext', context); - localStorage.setItem('painlessPlaygroundContextSetup', JSON.stringify(contextSetup)); + localStorage.setItem('painlessLabCode', code); + localStorage.setItem('painlessLabContext', context); + localStorage.setItem('painlessLabContextSetup', JSON.stringify(contextSetup)); const response = await executeCode(buildRequestPayload(code, context, contextSetup)); if (_mostRecentRequestId === requestId) { @@ -118,22 +118,18 @@ for (int y = 0; y < height; y++) { return result; `; -export function PainlessPlayground({ - executeCode, -}: { - executeCode: (payload: Request) => Promise; -}) { - const [code, setCode] = useState(getFromLocalStorage('painlessPlaygroundCode', exampleScript)); +export function Main({ executeCode }: { executeCode: (payload: Request) => Promise }) { + const [code, setCode] = useState(getFromLocalStorage('painlessLabCode', exampleScript)); const [response, setResponse] = useState({ error: undefined, success: undefined }); const [isRequestFlyoutOpen, setRequestFlyoutOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [context, setContext] = useState( - getFromLocalStorage('painlessPlaygroundContext', 'painless_test_without_params') + getFromLocalStorage('painlessLabContext', 'painless_test_without_params') ); const [contextSetup, setContextSetup] = useState( - getFromLocalStorage('painlessPlaygroundContextSetup', {}, true) + getFromLocalStorage('painlessLabContextSetup', {}, true) ); // Live-update the output as the user changes the input code. @@ -151,8 +147,8 @@ export function PainlessPlayground({

- {i18n.translate('xpack.painless_playground.title', { - defaultMessage: 'Painless Playground', + {i18n.translate('xpack.painlessLab.title', { + defaultMessage: 'Painless Lab', })}

diff --git a/x-pack/legacy/plugins/painless_playground/public/components/main_controls.tsx b/x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx similarity index 84% rename from x-pack/legacy/plugins/painless_playground/public/components/main_controls.tsx rename to x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx index 98e8f25f171b1..31348dcd31076 100644 --- a/x-pack/legacy/plugins/painless_playground/public/components/main_controls.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx @@ -40,7 +40,7 @@ export function MainControls({ target="_blank" onClick={() => setIsHelpOpen(false)} > - {i18n.translate('xpack.painless_playground.walkthroughButtonLabel', { + {i18n.translate('xpack.painlessLab.walkthroughButtonLabel', { defaultMessage: 'Walkthrough', })} , @@ -52,7 +52,7 @@ export function MainControls({ target="_blank" onClick={() => setIsHelpOpen(false)} > - {i18n.translate('xpack.painless_playground.apiReferenceButtonLabel', { + {i18n.translate('xpack.painlessLab.apiReferenceButtonLabel', { defaultMessage: 'API reference', })} , @@ -64,7 +64,7 @@ export function MainControls({ target="_blank" onClick={() => setIsHelpOpen(false)} > - {i18n.translate('xpack.painless_playground.languageSpecButtonLabel', { + {i18n.translate('xpack.painlessLab.languageSpecButtonLabel', { defaultMessage: 'Language spec', })} , @@ -77,7 +77,7 @@ export function MainControls({ setIsHelpOpen(false); }} > - {i18n.translate('xpack.painless_playground.resetButtonLabel', { + {i18n.translate('xpack.painlessLab.resetButtonLabel', { defaultMessage: 'Reset script', })} , @@ -85,7 +85,7 @@ export function MainControls({ return ( <> -
+
@@ -93,7 +93,7 @@ export function MainControls({ setIsHelpOpen(!isHelpOpen)} > - {i18n.translate('xpack.painless_playground.helpButtonLabel', { + {i18n.translate('xpack.painlessLab.helpButtonLabel', { defaultMessage: 'Help', })} @@ -126,10 +126,10 @@ export function MainControls({ data-test-subj="btnViewRequest" > {isRequestFlyoutOpen - ? i18n.translate('xpack.painless_playground.hideRequestButtonLabel', { + ? i18n.translate('xpack.painlessLab.hideRequestButtonLabel', { defaultMessage: 'Hide API request', }) - : i18n.translate('xpack.painless_playground.showRequestButtonLabel', { + : i18n.translate('xpack.painlessLab.showRequestButtonLabel', { defaultMessage: 'Show API request', })} diff --git a/x-pack/legacy/plugins/painless_playground/public/components/output_pane/context_tab.tsx b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/context_tab.tsx similarity index 85% rename from x-pack/legacy/plugins/painless_playground/public/components/output_pane/context_tab.tsx rename to x-pack/legacy/plugins/painless_lab/public/components/output_pane/context_tab.tsx index ec0f27c005b96..12c56577b8db6 100644 --- a/x-pack/legacy/plugins/painless_playground/public/components/output_pane/context_tab.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/context_tab.tsx @@ -37,13 +37,13 @@ export function ContextTab({ context, contextSetup, setContext, setContextSetup {' '} @@ -56,7 +56,7 @@ export function ContextTab({ context, contextSetup, setContext, setContextSetup href="https://www.elastic.co/guide/en/elasticsearch/painless/current/painless-execute-api.html" target="_blank" > - {i18n.translate('xpack.painless_playground.contextFieldDocLinkText', { + {i18n.translate('xpack.painlessLab.contextFieldDocLinkText', { defaultMessage: 'Context docs', })} @@ -78,16 +78,13 @@ export function ContextTab({ context, contextSetup, setContext, setContextSetup - {' '} + {' '} @@ -107,13 +104,13 @@ export function ContextTab({ context, contextSetup, setContext, setContextSetup {' '} diff --git a/x-pack/legacy/plugins/painless_playground/public/components/output_pane/index.ts b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/index.ts similarity index 100% rename from x-pack/legacy/plugins/painless_playground/public/components/output_pane/index.ts rename to x-pack/legacy/plugins/painless_lab/public/components/output_pane/index.ts diff --git a/x-pack/legacy/plugins/painless_playground/public/components/output_pane/output_pane.tsx b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_pane.tsx similarity index 87% rename from x-pack/legacy/plugins/painless_playground/public/components/output_pane/output_pane.tsx rename to x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_pane.tsx index 69393ca47a66c..55aa8f5b220e8 100644 --- a/x-pack/legacy/plugins/painless_playground/public/components/output_pane/output_pane.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_pane.tsx @@ -44,7 +44,7 @@ export function OutputPane({ - {i18n.translate('xpack.painless_playground.outputTabLabel', { + {i18n.translate('xpack.painlessLab.outputTabLabel', { defaultMessage: 'Output', })} @@ -52,9 +52,9 @@ export function OutputPane({ ); return ( - + {' '} @@ -53,13 +53,13 @@ export function ParametersTab({ context, contextSetup, setContext, setContextSet href="https://www.elastic.co/guide/en/elasticsearch/reference/master/modules-scripting-using.html#prefer-params" target="_blank" > - {i18n.translate('xpack.painless_playground.parametersFieldDocLinkText', { + {i18n.translate('xpack.painlessLab.parametersFieldDocLinkText', { defaultMessage: 'Parameters docs', })} } - helpText={i18n.translate('xpack.painless_playground.helpIconAriaLabel', { + helpText={i18n.translate('xpack.painlessLab.helpIconAriaLabel', { defaultMessage: 'Use JSON format', })} > diff --git a/x-pack/legacy/plugins/painless_playground/public/components/request_flyout.tsx b/x-pack/legacy/plugins/painless_lab/public/components/request_flyout.tsx similarity index 91% rename from x-pack/legacy/plugins/painless_playground/public/components/request_flyout.tsx rename to x-pack/legacy/plugins/painless_lab/public/components/request_flyout.tsx index 60ecdd5d4799b..def4ae2876bb6 100644 --- a/x-pack/legacy/plugins/painless_playground/public/components/request_flyout.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/request_flyout.tsx @@ -36,7 +36,7 @@ export function RequestFlyout({

- {i18n.translate('xpack.painless_playground.flyoutTitle', { + {i18n.translate('xpack.painlessLab.flyoutTitle', { defaultMessage: 'API request', })}

@@ -52,7 +52,7 @@ export function RequestFlyout({ target="_blank" iconType="help" > - {i18n.translate('xpack.painless_playground.flyoutDocLink', { + {i18n.translate('xpack.painlessLab.flyoutDocLink', { defaultMessage: 'API documentation', })} @@ -86,7 +86,7 @@ export function RequestFlyout({ ]} /> -
+
); diff --git a/x-pack/legacy/plugins/painless_playground/public/index.scss b/x-pack/legacy/plugins/painless_lab/public/index.scss similarity index 77% rename from x-pack/legacy/plugins/painless_playground/public/index.scss rename to x-pack/legacy/plugins/painless_lab/public/index.scss index 46b834c83786c..5c86be1832ff7 100644 --- a/x-pack/legacy/plugins/painless_playground/public/index.scss +++ b/x-pack/legacy/plugins/painless_lab/public/index.scss @@ -5,11 +5,11 @@ * 1. This is a very brittle way of preventing the editor and other content from disappearing * behind the bottom bar. */ -.painlessPlaygroundBottomBarPlaceholder { +.painlessLabBottomBarPlaceholder { height: $euiSize * 2; /* [1] */ } -.painlessPlaygroundRightPane { +.painlessLabRightPane { border-right: none; border-top: none; border-bottom: none; @@ -18,7 +18,7 @@ height: 100%; } -.painlessPlaygroundRightPane__tabs { +.painlessLabRightPane__tabs { display: flex; flex-direction: column; height: 100%; @@ -28,6 +28,6 @@ } } -.painlessPlayground__betaLabelContainer { +.painlessLab__betaLabelContainer { line-height: 0; } diff --git a/x-pack/legacy/plugins/painless_playground/public/lib/execute_code.ts b/x-pack/legacy/plugins/painless_lab/public/lib/execute_code.ts similarity index 100% rename from x-pack/legacy/plugins/painless_playground/public/lib/execute_code.ts rename to x-pack/legacy/plugins/painless_lab/public/lib/execute_code.ts diff --git a/x-pack/legacy/plugins/painless_playground/public/lib/helpers.ts b/x-pack/legacy/plugins/painless_lab/public/lib/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/painless_playground/public/lib/helpers.ts rename to x-pack/legacy/plugins/painless_lab/public/lib/helpers.ts diff --git a/x-pack/legacy/plugins/painless_playground/public/register.tsx b/x-pack/legacy/plugins/painless_lab/public/register.tsx similarity index 68% rename from x-pack/legacy/plugins/painless_playground/public/register.tsx rename to x-pack/legacy/plugins/painless_lab/public/register.tsx index 4908eeb758114..3f3561e1f52bb 100644 --- a/x-pack/legacy/plugins/painless_playground/public/register.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/register.tsx @@ -15,15 +15,15 @@ import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public import { ADVANCED_SETTINGS_FLAG_NAME } from '../common/constants'; npSetup.plugins.home.featureCatalogue.register({ - id: 'painless_playground', - title: i18n.translate('xpack.painless_playground.registryProviderTitle', { - defaultMessage: 'Painless Playground (beta)', + id: 'painlessLab', + title: i18n.translate('xpack.painlessLab.registryProviderTitle', { + defaultMessage: 'Painless Lab (beta)', }), - description: i18n.translate('xpack.painless_playground.registryProviderDescription', { + description: i18n.translate('xpack.painlessLab.registryProviderDescription', { defaultMessage: 'Simulate and debug painless code', }), icon: '', - path: '/app/kibana#/dev_tools/painless_playground', + path: '/app/kibana#/dev_tools/painless_lab', showOnHomePage: false, category: FeatureCatalogueCategory.ADMIN, }); @@ -38,17 +38,17 @@ npSetup.plugins.devTools.register({ title: ( - {i18n.translate('xpack.painless_playground.displayName', { - defaultMessage: 'Painless Playground', + {i18n.translate('xpack.painlessLab.displayName', { + defaultMessage: 'Painless Lab', })} - + @@ -57,13 +57,13 @@ npSetup.plugins.devTools.register({ ), enableRouting: false, disabled: false, - tooltipContent: xpackInfo.get('features.painlessPlayground.message'), + tooltipContent: xpackInfo.get('features.painlessLab.message'), async mount(context, { element }) { registerPainless(); const licenseCheck = { - showPage: xpackInfo.get('features.painlessPlayground.enableLink'), - message: xpackInfo.get('features.painlessPlayground.message'), + showPage: xpackInfo.get('features.painlessLab.enableLink'), + message: xpackInfo.get('features.painlessLab.message'), }; if (!licenseCheck.showPage) { diff --git a/x-pack/legacy/plugins/painless_playground/public/register_painless.ts b/x-pack/legacy/plugins/painless_lab/public/register_painless.ts similarity index 100% rename from x-pack/legacy/plugins/painless_playground/public/register_painless.ts rename to x-pack/legacy/plugins/painless_lab/public/register_painless.ts diff --git a/x-pack/legacy/plugins/painless_playground/public/render_app.tsx b/x-pack/legacy/plugins/painless_lab/public/render_app.tsx similarity index 85% rename from x-pack/legacy/plugins/painless_playground/public/render_app.tsx rename to x-pack/legacy/plugins/painless_lab/public/render_app.tsx index 75adb3bbc382a..f6c0a5b6069b0 100644 --- a/x-pack/legacy/plugins/painless_playground/public/render_app.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/render_app.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { CoreStart } from 'kibana/public'; import { render, unmountComponentAtNode } from 'react-dom'; -import { PainlessPlayground } from './components/painless_playground'; +import { Main } from './components/main'; import { createKibanaReactContext } from '../../../../../src/plugins/kibana_react/public'; import { executeCode } from './lib/execute_code'; @@ -18,7 +18,7 @@ export function renderApp(element: any, { http, i18n, uiSettings }: CoreStart) { render( - executeCode(http, payload)} /> +
executeCode(http, payload)} /> , element diff --git a/x-pack/legacy/plugins/painless_playground/server/lib/check_license.ts b/x-pack/legacy/plugins/painless_lab/server/lib/check_license.ts similarity index 73% rename from x-pack/legacy/plugins/painless_playground/server/lib/check_license.ts rename to x-pack/legacy/plugins/painless_lab/server/lib/check_license.ts index f6a815abe9a47..5cbed532ca56a 100644 --- a/x-pack/legacy/plugins/painless_playground/server/lib/check_license.ts +++ b/x-pack/legacy/plugins/painless_lab/server/lib/check_license.ts @@ -13,9 +13,9 @@ export function checkLicense(xpackLicenseInfo: any) { return { enableLink: false, enableAPIRoute: false, - message: i18n.translate('xpack.painless_playground.unavailableLicenseInformationMessage', { + message: i18n.translate('xpack.painlessLab.unavailableLicenseInformationMessage', { defaultMessage: - 'You cannot use the Painless Playground because license information is not available at this time.', + 'You cannot use the Painless Lab because license information is not available at this time.', }), }; } @@ -28,9 +28,9 @@ export function checkLicense(xpackLicenseInfo: any) { return { enableLink: false, enableAPIRoute: false, - message: i18n.translate('xpack.painless_playground.licenseHasExpiredMessage', { + message: i18n.translate('xpack.painlessLab.licenseHasExpiredMessage', { defaultMessage: - 'You cannot use the Painless Playground because your {licenseType} license has expired.', + 'You cannot use the Painless Lab because your {licenseType} license has expired.', values: { licenseType, }, diff --git a/x-pack/legacy/plugins/painless_playground/server/lib/license_pre_routing_factory.ts b/x-pack/legacy/plugins/painless_lab/server/lib/license_pre_routing_factory.ts similarity index 100% rename from x-pack/legacy/plugins/painless_playground/server/lib/license_pre_routing_factory.ts rename to x-pack/legacy/plugins/painless_lab/server/lib/license_pre_routing_factory.ts diff --git a/x-pack/legacy/plugins/painless_playground/server/register_execute_route.ts b/x-pack/legacy/plugins/painless_lab/server/register_execute_route.ts similarity index 100% rename from x-pack/legacy/plugins/painless_playground/server/register_execute_route.ts rename to x-pack/legacy/plugins/painless_lab/server/register_execute_route.ts diff --git a/x-pack/legacy/plugins/painless_playground/server/register_license_checker.ts b/x-pack/legacy/plugins/painless_lab/server/register_license_checker.ts similarity index 100% rename from x-pack/legacy/plugins/painless_playground/server/register_license_checker.ts rename to x-pack/legacy/plugins/painless_lab/server/register_license_checker.ts From e683bc6b104f71f65c60099dc7523b79ab018879 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 19 Feb 2020 16:43:12 -0800 Subject: [PATCH 04/23] Fix i18n namespace. --- x-pack/.i18nrc.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 83a418f09682c..2ff279e8d6d49 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -29,7 +29,7 @@ "xpack.ml": "legacy/plugins/ml", "xpack.monitoring": "legacy/plugins/monitoring", "xpack.remoteClusters": "plugins/remote_clusters", - "xpack.painless_playground": "legacy/plugins/painless_playground", + "xpack.painlessLab": "legacy/plugins/painless_lab", "xpack.reporting": ["plugins/reporting", "legacy/plugins/reporting"], "xpack.rollupJobs": "legacy/plugins/rollup", "xpack.searchProfiler": "plugins/searchprofiler", From 74cd1c61a2362e2851e385d7f479f052bc1a594a Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Wed, 19 Feb 2020 21:50:20 -0800 Subject: [PATCH 05/23] Improve smiley face proportions. - Add def keyword to Painless spec. - Temporarily fix broken highlighting. - Add small padding to main controls. --- .../painless_lab/public/components/main.tsx | 35 ++++++++++++------- .../public/components/main_controls.tsx | 2 +- .../plugins/painless_lab/public/index.scss | 2 +- .../painless_lab/public/register_painless.ts | 19 ++++++++-- 4 files changed, 41 insertions(+), 17 deletions(-) diff --git a/x-pack/legacy/plugins/painless_lab/public/components/main.tsx b/x-pack/legacy/plugins/painless_lab/public/components/main.tsx index f1cdf8820c2b7..a1c6a9f0080f7 100644 --- a/x-pack/legacy/plugins/painless_lab/public/components/main.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/main.tsx @@ -65,15 +65,15 @@ const submit = async (code, context, contextSetup, executeCode, setResponse, set const debouncedSubmit = debounce(submit, 800); -// Render a heart as an example. +// Render a smiley face as an example. const exampleScript = ` boolean isInCircle(def posX, def posY, def circleX, def circleY, def radius) { double distanceFromCircleCenter = Math.sqrt(Math.pow(circleX - posX, 2) + Math.pow(circleY - posY, 2)); return distanceFromCircleCenter <= radius; } -boolean isOnCircle(def posX, def posY, def circleX, def circleY, def radius, def thickness) { - double distanceFromCircleCenter = Math.sqrt(Math.pow(circleX - posX, 2) + Math.pow(circleY - posY, 2)); +boolean isOnCircle(def posX, def posY, def circleX, def circleY, def radius, def thickness, def squashY) { + double distanceFromCircleCenter = Math.sqrt(Math.pow(circleX - posX, 2) + Math.pow((circleY - posY) / squashY, 2)); return ( distanceFromCircleCenter >= radius - thickness && distanceFromCircleCenter <= radius + thickness @@ -83,23 +83,34 @@ boolean isOnCircle(def posX, def posY, def circleX, def circleY, def radius, def def result = ''; int charCount = 0; +// Canvas dimensions int width = 31; -int height = 25; - -int eyePositionX = 8; -int eyePositionY = 6; -int eyeSize = 3; -int mouthSize = 11; +int height = 31; +double halfWidth = Math.floor(width * 0.5); +double halfHeight = Math.floor(height * 0.5); + +// Style constants +double strokeWidth = 0.6; + +// Smiley face configuration +int headSize = 13; +double headSquashY = 0.78; +int eyePositionX = 10; +int eyePositionY = 12; +int eyeSize = 1; +int mouthSize = 15; int mouthPositionX = width / 2; -int mouthPositionY = 9; +int mouthPositionY = 5; +int mouthOffsetY = 11; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { + boolean isHead = isOnCircle(x, y, halfWidth, halfHeight, headSize, strokeWidth, headSquashY); boolean isLeftEye = isInCircle(x, y, eyePositionX, eyePositionY, eyeSize); boolean isRightEye = isInCircle(x, y, width - eyePositionX - 1, eyePositionY, eyeSize); - boolean isMouth = isOnCircle(x, y, mouthPositionX, mouthPositionY, mouthSize, 1) && y > mouthPositionY + 3; + boolean isMouth = isOnCircle(x, y, mouthPositionX, mouthPositionY, mouthSize, strokeWidth, 1) && y > mouthPositionY + mouthOffsetY; - if (isLeftEye || isRightEye || isMouth) { + if (isLeftEye || isRightEye || isMouth || isHead) { result += "*"; } else { result += "."; diff --git a/x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx b/x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx index 31348dcd31076..8ee0d7fb09275 100644 --- a/x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx @@ -87,7 +87,7 @@ export function MainControls({ <>
- + diff --git a/x-pack/legacy/plugins/painless_lab/public/index.scss b/x-pack/legacy/plugins/painless_lab/public/index.scss index 5c86be1832ff7..69c1f8c217655 100644 --- a/x-pack/legacy/plugins/painless_lab/public/index.scss +++ b/x-pack/legacy/plugins/painless_lab/public/index.scss @@ -6,7 +6,7 @@ * behind the bottom bar. */ .painlessLabBottomBarPlaceholder { - height: $euiSize * 2; /* [1] */ + height: $euiSize * 3; /* [1] */ } .painlessLabRightPane { diff --git a/x-pack/legacy/plugins/painless_lab/public/register_painless.ts b/x-pack/legacy/plugins/painless_lab/public/register_painless.ts index 700e6d93dad2a..16c799ff32a2b 100644 --- a/x-pack/legacy/plugins/painless_lab/public/register_painless.ts +++ b/x-pack/legacy/plugins/painless_lab/public/register_painless.ts @@ -52,7 +52,18 @@ function getPainlessLanguage() { 'this', 'instanceof', ], - primitives: ['void', 'boolean', 'byte', 'short', 'char', 'int', 'long', 'float', 'double'], + primitives: [ + 'void', + 'boolean', + 'byte', + 'short', + 'char', + 'int', + 'long', + 'float', + 'double', + 'def', + ], constants: ['true', 'false'], operators: [ '=', @@ -177,6 +188,8 @@ function getPainlessLanguage() { } export function registerPainless() { - monaco.languages.register({ id: LANGUAGE_ID }); - monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, getPainlessLanguage()); + // TODO: Referring to `window.monaco` is a temporary fix for the imported `monaco` module not + // being the same one in use by the editor. + window.monaco.languages.register({ id: LANGUAGE_ID }); + window.monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, getPainlessLanguage()); } From 35de7e0e56098cbdf7f863243735fc662d5289fb Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 24 Feb 2020 10:44:55 +0100 Subject: [PATCH 06/23] [Painless Lab] Minor Fixes (#58135) * Code restructure, improve types, add plugin id, introduced hook Moved the code execution hook to a custom hook outside of main, also chaining off promise to avoid lower level handling of sequencing. * Re-instated formatting code To improve DX the execution error response from the painless API was massaged to a more reader friendly state, only giving non-repeating information. Currently it is hard to determine the line and character information from the painless endpoint. If the user wishes to see this raw information it will be available in the API response flyout. * Remove leading new line in default script * Remove registration of feature flag * Fix types * Restore previous auto-submit request behaviour * Remove use of null and remove old comment Stick with "undefined" as the designation for something not existing. --- .../plugins/painless_lab/common/constants.ts | 2 - x-pack/legacy/plugins/painless_lab/index.ts | 17 +- .../painless_lab/public/common/constants.tsx | 63 +++++++ .../painless_lab/public/common/types.ts | 42 +++-- .../painless_lab/public/components/editor.tsx | 1 - .../painless_lab/public/components/main.tsx | 154 ++++-------------- .../public/components/main_controls.tsx | 9 +- .../components/output_pane/context_tab.tsx | 29 ++-- .../components/output_pane/output_pane.tsx | 35 ++-- .../components/output_pane/output_tab.tsx | 5 +- .../components/output_pane/parameters_tab.tsx | 13 +- .../public/components/request_flyout.tsx | 2 +- .../painless_lab/public/hooks/index.ts | 7 + .../public/hooks/use_submit_code.ts | 62 +++++++ .../painless_lab/public/lib/helpers.ts | 23 ++- .../plugins/painless_lab/public/register.tsx | 9 +- .../painless_lab/public/register_painless.ts | 4 +- .../painless_lab/public/render_app.tsx | 3 +- 18 files changed, 252 insertions(+), 228 deletions(-) create mode 100644 x-pack/legacy/plugins/painless_lab/public/hooks/index.ts create mode 100644 x-pack/legacy/plugins/painless_lab/public/hooks/use_submit_code.ts diff --git a/x-pack/legacy/plugins/painless_lab/common/constants.ts b/x-pack/legacy/plugins/painless_lab/common/constants.ts index 771d4979143fd..7179daedb338c 100644 --- a/x-pack/legacy/plugins/painless_lab/common/constants.ts +++ b/x-pack/legacy/plugins/painless_lab/common/constants.ts @@ -7,5 +7,3 @@ export const PLUGIN_ID = 'painlessLab'; export const API_ROUTE_EXECUTE = '/api/painless_lab/execute'; - -export const ADVANCED_SETTINGS_FLAG_NAME = 'devTools:enablePainlessLab'; diff --git a/x-pack/legacy/plugins/painless_lab/index.ts b/x-pack/legacy/plugins/painless_lab/index.ts index 3be4a25508bad..5372bbf37cf2e 100644 --- a/x-pack/legacy/plugins/painless_lab/index.ts +++ b/x-pack/legacy/plugins/painless_lab/index.ts @@ -3,9 +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 { i18n } from '@kbn/i18n'; import { resolve } from 'path'; -import { PLUGIN_ID, ADVANCED_SETTINGS_FLAG_NAME } from './common/constants'; +import { PLUGIN_ID } from './common/constants'; import { registerLicenseChecker } from './server/register_license_checker'; import { registerExecuteRoute } from './server/register_execute_route'; @@ -27,20 +26,6 @@ export const painlessLab = (kibana: any) => devTools: [resolve(__dirname, 'public/register')], }, init: (server: Legacy.Server) => { - // Register feature flag - server.newPlatform.setup.core.uiSettings.register({ - [ADVANCED_SETTINGS_FLAG_NAME]: { - name: i18n.translate('xpack.painlessLab.devTools.painlessLabTitle', { - defaultMessage: 'Painless Lab', - }), - description: i18n.translate('xpack.painlessLab.devTools.painlessLabDescription', { - defaultMessage: 'Enable experimental Painless Lab.', - }), - value: false, - category: ['Dev Tools'], - }, - }); - registerLicenseChecker(server); registerExecuteRoute(server); }, diff --git a/x-pack/legacy/plugins/painless_lab/public/common/constants.tsx b/x-pack/legacy/plugins/painless_lab/public/common/constants.tsx index 4ff1f9fe340bf..b88ff1e226d27 100644 --- a/x-pack/legacy/plugins/painless_lab/public/common/constants.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/common/constants.tsx @@ -61,3 +61,66 @@ export const painlessContextOptions = [ ), }, ]; + +// Render a smiley face as an example. +export const exampleScript = `boolean isInCircle(def posX, def posY, def circleX, def circleY, def radius) { + double distanceFromCircleCenter = Math.sqrt(Math.pow(circleX - posX, 2) + Math.pow(circleY - posY, 2)); + return distanceFromCircleCenter <= radius; +} + +boolean isOnCircle(def posX, def posY, def circleX, def circleY, def radius, def thickness, def squashY) { + double distanceFromCircleCenter = Math.sqrt(Math.pow(circleX - posX, 2) + Math.pow((circleY - posY) / squashY, 2)); + return ( + distanceFromCircleCenter >= radius - thickness + && distanceFromCircleCenter <= radius + thickness + ); +} + +def result = ''; +int charCount = 0; + +// Canvas dimensions +int width = 31; +int height = 31; +double halfWidth = Math.floor(width * 0.5); +double halfHeight = Math.floor(height * 0.5); + +// Style constants +double strokeWidth = 0.6; + +// Smiley face configuration +int headSize = 13; +double headSquashY = 0.78; +int eyePositionX = 10; +int eyePositionY = 12; +int eyeSize = 1; +int mouthSize = 15; +int mouthPositionX = width / 2; +int mouthPositionY = 5; +int mouthOffsetY = 11; + +for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + boolean isHead = isOnCircle(x, y, halfWidth, halfHeight, headSize, strokeWidth, headSquashY); + boolean isLeftEye = isInCircle(x, y, eyePositionX, eyePositionY, eyeSize); + boolean isRightEye = isInCircle(x, y, width - eyePositionX - 1, eyePositionY, eyeSize); + boolean isMouth = isOnCircle(x, y, mouthPositionX, mouthPositionY, mouthSize, strokeWidth, 1) && y > mouthPositionY + mouthOffsetY; + + if (isLeftEye || isRightEye || isMouth || isHead) { + result += "*"; + } else { + result += "."; + } + + result += " "; + + // Make sure the smiley face doesn't deform as the container changes width. + charCount++; + if (charCount % width === 0) { + result += "\\\\n"; + } + } +} + +return result; +`; diff --git a/x-pack/legacy/plugins/painless_lab/public/common/types.ts b/x-pack/legacy/plugins/painless_lab/public/common/types.ts index 825b0a7848136..697247415c428 100644 --- a/x-pack/legacy/plugins/painless_lab/public/common/types.ts +++ b/x-pack/legacy/plugins/painless_lab/public/common/types.ts @@ -3,25 +3,40 @@ * or more contributor license agreements. Licensed under the Elastic License; * 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 Request { - script: { - source: string; - params?: Record; - }; - context?: string; - context_setup?: { - document: Record; - index: string; - }; + script: Script; + context?: Context; + context_setup?: ContextSetup; } export interface Response { - error?: ExecutionError; + error?: ExecutionError | Error; result?: string; } export type ExecutionErrorScriptStack = string[]; +export interface ExecutionErrorPosition { + start: number; + end: number; + offset: number; +} + export interface ExecutionError { script_stack?: ExecutionErrorScriptStack; caused_by?: { @@ -29,6 +44,8 @@ export interface ExecutionError { reason: string; }; message?: string; + position: ExecutionErrorPosition; + script: string; } export type JsonArray = JsonValue[]; @@ -37,3 +54,8 @@ export type JsonValue = null | boolean | number | string | JsonObject | JsonArra export interface JsonObject { [key: string]: JsonValue; } + +export type ContextChangeHandler = (change: { + context?: Partial; + contextSetup?: Partial; +}) => void; diff --git a/x-pack/legacy/plugins/painless_lab/public/components/editor.tsx b/x-pack/legacy/plugins/painless_lab/public/components/editor.tsx index f77f4ab888be6..8c2f07e539871 100644 --- a/x-pack/legacy/plugins/painless_lab/public/components/editor.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/editor.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ import React from 'react'; -import { EuiSpacer, EuiPageContent } from '@elastic/eui'; import { CodeEditor } from '../../../../../../src/plugins/kibana_react/public'; interface Props { diff --git a/x-pack/legacy/plugins/painless_lab/public/components/main.tsx b/x-pack/legacy/plugins/painless_lab/public/components/main.tsx index a1c6a9f0080f7..cc91f9d93d5bf 100644 --- a/x-pack/legacy/plugins/painless_lab/public/components/main.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/main.tsx @@ -4,136 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import { HttpSetup } from 'kibana/public'; import React, { useState, useEffect } from 'react'; -import { debounce } from 'lodash'; -import { - EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiTabbedContent, - EuiTitle, - EuiSpacer, - EuiPageContent, - EuiFlyout, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { buildRequestPayload, formatJson, getFromLocalStorage } from '../lib/helpers'; -import { Request, Response } from '../common/types'; +import { ContextChangeHandler } from '../common/types'; import { OutputPane } from './output_pane'; import { MainControls } from './main_controls'; import { Editor } from './editor'; import { RequestFlyout } from './request_flyout'; +import { useSubmitCode } from '../hooks'; +import { exampleScript } from '../common/constants'; -let _mostRecentRequestId = 0; - -const submit = async (code, context, contextSetup, executeCode, setResponse, setIsLoading) => { - // Prevent an older request that resolves after a more recent request from clobbering it. - // We store the resulting ID in this closure for comparison when the request resolves. - const requestId = ++_mostRecentRequestId; - setIsLoading(true); - - try { - localStorage.setItem('painlessLabCode', code); - localStorage.setItem('painlessLabContext', context); - localStorage.setItem('painlessLabContextSetup', JSON.stringify(contextSetup)); - const response = await executeCode(buildRequestPayload(code, context, contextSetup)); - - if (_mostRecentRequestId === requestId) { - if (response.error) { - setResponse({ - success: undefined, - error: response.error, - }); - } else { - setResponse({ - success: response, - error: undefined, - }); - } - setIsLoading(false); - } - } catch (error) { - if (_mostRecentRequestId === requestId) { - setResponse({ - success: undefined, - error: { error }, - }); - setIsLoading(false); - } - } -}; - -const debouncedSubmit = debounce(submit, 800); - -// Render a smiley face as an example. -const exampleScript = ` -boolean isInCircle(def posX, def posY, def circleX, def circleY, def radius) { - double distanceFromCircleCenter = Math.sqrt(Math.pow(circleX - posX, 2) + Math.pow(circleY - posY, 2)); - return distanceFromCircleCenter <= radius; -} - -boolean isOnCircle(def posX, def posY, def circleX, def circleY, def radius, def thickness, def squashY) { - double distanceFromCircleCenter = Math.sqrt(Math.pow(circleX - posX, 2) + Math.pow((circleY - posY) / squashY, 2)); - return ( - distanceFromCircleCenter >= radius - thickness - && distanceFromCircleCenter <= radius + thickness - ); -} - -def result = ''; -int charCount = 0; - -// Canvas dimensions -int width = 31; -int height = 31; -double halfWidth = Math.floor(width * 0.5); -double halfHeight = Math.floor(height * 0.5); - -// Style constants -double strokeWidth = 0.6; - -// Smiley face configuration -int headSize = 13; -double headSquashY = 0.78; -int eyePositionX = 10; -int eyePositionY = 12; -int eyeSize = 1; -int mouthSize = 15; -int mouthPositionX = width / 2; -int mouthPositionY = 5; -int mouthOffsetY = 11; - -for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - boolean isHead = isOnCircle(x, y, halfWidth, halfHeight, headSize, strokeWidth, headSquashY); - boolean isLeftEye = isInCircle(x, y, eyePositionX, eyePositionY, eyeSize); - boolean isRightEye = isInCircle(x, y, width - eyePositionX - 1, eyePositionY, eyeSize); - boolean isMouth = isOnCircle(x, y, mouthPositionX, mouthPositionY, mouthSize, strokeWidth, 1) && y > mouthPositionY + mouthOffsetY; - - if (isLeftEye || isRightEye || isMouth || isHead) { - result += "*"; - } else { - result += "."; - } - - result += " "; - - // Make sure the smiley face doesn't deform as the container changes width. - charCount++; - if (charCount % width === 0) { - result += "\\\\n"; - } - } +interface Props { + http: HttpSetup; } -return result; -`; - -export function Main({ executeCode }: { executeCode: (payload: Request) => Promise }) { +export function Main({ http }: Props) { const [code, setCode] = useState(getFromLocalStorage('painlessLabCode', exampleScript)); - const [response, setResponse] = useState({ error: undefined, success: undefined }); const [isRequestFlyoutOpen, setRequestFlyoutOpen] = useState(false); - const [isLoading, setIsLoading] = useState(false); const [context, setContext] = useState( getFromLocalStorage('painlessLabContext', 'painless_test_without_params') @@ -143,15 +33,29 @@ export function Main({ executeCode }: { executeCode: (payload: Request) => Promi getFromLocalStorage('painlessLabContextSetup', {}, true) ); + const { inProgress, response, submit } = useSubmitCode(http); + // Live-update the output as the user changes the input code. useEffect(() => { - debouncedSubmit(code, context, contextSetup, executeCode, setResponse, setIsLoading); - }, [code, context, contextSetup, executeCode]); + submit(code, context, contextSetup); + }, [submit, code, context, contextSetup]); const toggleRequestFlyout = () => { setRequestFlyoutOpen(!isRequestFlyoutOpen); }; + const contextChangeHandler: ContextChangeHandler = ({ + context: nextContext, + contextSetup: nextContextSetup, + }) => { + if (nextContext) { + setContext(nextContext); + } + if (nextContextSetup) { + setContextSetup(nextContextSetup); + } + }; + return ( <> @@ -171,17 +75,15 @@ export function Main({ executeCode }: { executeCode: (payload: Request) => Promi submit(code, context, contextSetup, executeCode, setResponse)} - isLoading={isLoading} + isLoading={inProgress} toggleRequestFlyout={toggleRequestFlyout} isRequestFlyoutOpen={isRequestFlyoutOpen} reset={() => setCode(exampleScript)} @@ -191,7 +93,7 @@ export function Main({ executeCode }: { executeCode: (payload: Request) => Promi setRequestFlyoutOpen(false)} requestBody={formatJson(buildRequestPayload(code, context, contextSetup))} - response={formatJson(response.success || response.error)} + response={response ? formatJson(response.result || response.error) : ''} /> )} diff --git a/x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx b/x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx index 8ee0d7fb09275..9e24fd9fb5a93 100644 --- a/x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx @@ -12,9 +12,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, - EuiButton, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; interface Props { @@ -24,12 +22,7 @@ interface Props { reset: () => void; } -export function MainControls({ - toggleRequestFlyout, - isRequestFlyoutOpen, - isLoading, - reset, -}: Props) { +export function MainControls({ toggleRequestFlyout, isRequestFlyoutOpen, reset }: Props) { const [isHelpOpen, setIsHelpOpen] = useState(false); const items = [ diff --git a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/context_tab.tsx b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/context_tab.tsx index 12c56577b8db6..06325c5c2661e 100644 --- a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/context_tab.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/context_tab.tsx @@ -21,16 +21,15 @@ 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: Record; - setContext: (context: string) => void; - setContextSetup: (contextSetup: Record) => void; - renderMainControls: () => React.ReactElement; + contextSetup: ContextSetup; + onContextChange: ContextChangeHandler; } -export function ContextTab({ context, contextSetup, setContext, setContextSetup }: Props) { +export function ContextTab({ context, contextSetup, onContextChange }: Props) { return ( <> @@ -67,7 +66,7 @@ export function ContextTab({ context, contextSetup, setContext, setContextSetup setContext(value)} + onChange={(value: any) => onContextChange({ context: value })} itemLayoutAlign="top" hasDividers fullWidth @@ -94,9 +93,11 @@ export function ContextTab({ context, contextSetup, setContext, setContextSetup - setContextSetup(Object.assign({}, contextSetup, { index: e.target.value })) - } + onChange={e => { + onContextChange({ + contextSetup: Object.assign({}, contextSetup, { index: e.target.value }), + }); + }} /> )} @@ -123,10 +124,12 @@ export function ContextTab({ context, contextSetup, setContext, setContextSetup - setContextSetup(Object.assign({}, contextSetup, { document: value })) - } + value={JSON.stringify(contextSetup.document, null, 2)} + onChange={(value: string) => { + onContextChange({ + contextSetup: Object.assign({}, contextSetup, { document: value }), + }); + }} options={{ fontSize: 12, minimap: { diff --git a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_pane.tsx b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_pane.tsx index 55aa8f5b220e8..db6e58124d74a 100644 --- a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_pane.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_pane.tsx @@ -15,28 +15,26 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { formatJson, formatResponse } from '../../lib/helpers'; -import { Response } from '../../common/types'; +import { Response, ContextSetup, Context, ContextChangeHandler } from '../../common/types'; import { OutputTab } from './output_tab'; import { ParametersTab } from './parameters_tab'; import { ContextTab } from './context_tab'; -export function OutputPane({ - response, - context, - contextSetup, - setContext, - setContextSetup, - isLoading, -}: { +interface Props { + context: Context; + contextSetup: ContextSetup; + isLoading: boolean; + onContextChange: ContextChangeHandler; response?: Response; -}) { +} + +export function OutputPane({ response, context, contextSetup, onContextChange, isLoading }: Props) { const outputTabLabel = ( {isLoading ? ( - ) : response.error ? ( + ) : response && response.error ? ( ) : ( @@ -59,7 +57,8 @@ export function OutputPane({ tabs={[ { id: 'output', - name: outputTabLabel, + // TODO: Currently this causes an Eui prop error because it is expecting string, but we give it React.ReactNode - should fix. + name: outputTabLabel as any, content: , }, { @@ -68,12 +67,7 @@ export function OutputPane({ defaultMessage: 'Parameters', }), content: ( - + ), }, { @@ -85,8 +79,7 @@ export function OutputPane({ ), }, diff --git a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_tab.tsx b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_tab.tsx index 4b69e7969c924..55bb9a34c78b6 100644 --- a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_tab.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_tab.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import { formatResponse } from '../../lib/helpers'; import { Response } from '../../common/types'; @@ -15,12 +14,12 @@ interface Props { response?: Response; } -export function OutputTab({ response = {} }: Props) { +export function OutputTab({ response }: Props) { return ( <> - {formatResponse(response.success || response.error)} + {formatResponse(response)} ); diff --git a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/parameters_tab.tsx b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/parameters_tab.tsx index abfb99842846e..b731ba0cbdaa7 100644 --- a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/parameters_tab.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/output_pane/parameters_tab.tsx @@ -16,17 +16,14 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; 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: Record; - setContext: (context: string) => void; - setContextSetup: (contextSetup: Record) => void; - renderMainControls: () => React.ReactElement; + contextSetup: ContextSetup; + onContextChange: ContextChangeHandler; } -export function ParametersTab({ context, contextSetup, setContext, setContextSetup }: Props) { +export function ParametersTab({ contextSetup, onContextChange }: Props) { return ( <> @@ -68,7 +65,7 @@ export function ParametersTab({ context, contextSetup, setContext, setContextSet languageId="javascript" height={600} value={contextSetup.params} - onChange={(value: string) => setContextSetup({ params: value })} + onChange={(value: string) => onContextChange({ contextSetup: { params: value } })} options={{ fontSize: 12, minimap: { diff --git a/x-pack/legacy/plugins/painless_lab/public/components/request_flyout.tsx b/x-pack/legacy/plugins/painless_lab/public/components/request_flyout.tsx index def4ae2876bb6..2d0262503b162 100644 --- a/x-pack/legacy/plugins/painless_lab/public/components/request_flyout.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/components/request_flyout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { EuiCodeBlock, EuiTabbedContent, diff --git a/x-pack/legacy/plugins/painless_lab/public/hooks/index.ts b/x-pack/legacy/plugins/painless_lab/public/hooks/index.ts new file mode 100644 index 0000000000000..159ff96d2278c --- /dev/null +++ b/x-pack/legacy/plugins/painless_lab/public/hooks/index.ts @@ -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 { useSubmitCode } from './use_submit_code'; diff --git a/x-pack/legacy/plugins/painless_lab/public/hooks/use_submit_code.ts b/x-pack/legacy/plugins/painless_lab/public/hooks/use_submit_code.ts new file mode 100644 index 0000000000000..87b2fb0a7b2bf --- /dev/null +++ b/x-pack/legacy/plugins/painless_lab/public/hooks/use_submit_code.ts @@ -0,0 +1,62 @@ +/* + * 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 { useRef, useCallback, useState } from 'react'; +import { HttpSetup } from 'kibana/public'; +import { debounce } from 'lodash'; +import { Response } from '../common/types'; +import { buildRequestPayload } from '../lib/helpers'; +import { executeCode } from '../lib/execute_code'; + +const DEBOUNCE_MS = 800; + +export const useSubmitCode = (http: HttpSetup) => { + const currentRequestIdRef = useRef(0); + const [response, setResponse] = useState(undefined); + const [inProgress, setInProgress] = useState(false); + + const submit = useCallback( + debounce( + async (code: string, context: string, contextSetup: Record) => { + setInProgress(true); + + // Prevent an older request that resolves after a more recent request from clobbering it. + // We store the resulting ID in this closure for comparison when the request resolves. + 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)); + + if (currentRequestIdRef.current === requestId) { + setResponse(result); + setInProgress(false); + } + // else ignore this response... + } catch (error) { + if (currentRequestIdRef.current === requestId) { + setResponse({ + error, + }); + setInProgress(false); + } + // else ignore this response... + } + }, + DEBOUNCE_MS, + { trailing: true } + ), + [http] + ); + + return { + response, + inProgress, + submit, + }; +}; diff --git a/x-pack/legacy/plugins/painless_lab/public/lib/helpers.ts b/x-pack/legacy/plugins/painless_lab/public/lib/helpers.ts index ddc633b2e99d7..d5c35476948d0 100644 --- a/x-pack/legacy/plugins/painless_lab/public/lib/helpers.ts +++ b/x-pack/legacy/plugins/painless_lab/public/lib/helpers.ts @@ -75,7 +75,6 @@ export function formatResponse(response?: Response): string { if (!response) { return ''; } - if (typeof response.result === 'string') { return response.result.replace(/\\n/g, '\n'); } else if (response.error) { @@ -84,15 +83,23 @@ export function formatResponse(response?: Response): string { return formatJson(response); } -export function formatExecutionError(json: ExecutionError): string { - if (json.script_stack && json.caused_by) { - return `Unhandled Exception ${json.caused_by.type} +export function formatExecutionError(executionErrorOrError: ExecutionError | Error): string { + if (executionErrorOrError instanceof Error) { + return executionErrorOrError.message; + } + + if ( + executionErrorOrError.script_stack && + executionErrorOrError.caused_by && + executionErrorOrError.position + ) { + return `Unhandled Exception ${executionErrorOrError.caused_by.type} -${json.caused_by.reason} +${executionErrorOrError.caused_by.reason} -Located at: -${formatJson(json.script_stack)} +Stack: +${formatJson(executionErrorOrError.script_stack)} `; } - return formatJson(json); + return formatJson(executionErrorOrError); } diff --git a/x-pack/legacy/plugins/painless_lab/public/register.tsx b/x-pack/legacy/plugins/painless_lab/public/register.tsx index 3f3561e1f52bb..1f5446bd07c27 100644 --- a/x-pack/legacy/plugins/painless_lab/public/register.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/register.tsx @@ -12,7 +12,6 @@ import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; import { npSetup, npStart } from 'ui/new_platform'; import { registerPainless } from './register_painless'; import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; -import { ADVANCED_SETTINGS_FLAG_NAME } from '../common/constants'; npSetup.plugins.home.featureCatalogue.register({ id: 'painlessLab', @@ -28,12 +27,8 @@ npSetup.plugins.home.featureCatalogue.register({ category: FeatureCatalogueCategory.ADMIN, }); -npSetup.core.uiSettings.get$(ADVANCED_SETTINGS_FLAG_NAME, false).subscribe(value => { - // eslint-disable-next-line - console.log('use this to figure out whether we should register', value); -}); - npSetup.plugins.devTools.register({ + id: 'painless_lab', order: 7, title: ( @@ -54,7 +49,7 @@ npSetup.plugins.devTools.register({ /> - ), + ) as any, enableRouting: false, disabled: false, tooltipContent: xpackInfo.get('features.painlessLab.message'), diff --git a/x-pack/legacy/plugins/painless_lab/public/register_painless.ts b/x-pack/legacy/plugins/painless_lab/public/register_painless.ts index 16c799ff32a2b..a3952c1941c6e 100644 --- a/x-pack/legacy/plugins/painless_lab/public/register_painless.ts +++ b/x-pack/legacy/plugins/painless_lab/public/register_painless.ts @@ -190,6 +190,6 @@ function getPainlessLanguage() { export function registerPainless() { // TODO: Referring to `window.monaco` is a temporary fix for the imported `monaco` module not // being the same one in use by the editor. - window.monaco.languages.register({ id: LANGUAGE_ID }); - window.monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, getPainlessLanguage()); + (window as any).monaco.languages.register({ id: LANGUAGE_ID }); + (window as any).monaco.languages.setMonarchTokensProvider(LANGUAGE_ID, getPainlessLanguage()); } diff --git a/x-pack/legacy/plugins/painless_lab/public/render_app.tsx b/x-pack/legacy/plugins/painless_lab/public/render_app.tsx index f6c0a5b6069b0..78102fd1002e5 100644 --- a/x-pack/legacy/plugins/painless_lab/public/render_app.tsx +++ b/x-pack/legacy/plugins/painless_lab/public/render_app.tsx @@ -9,7 +9,6 @@ import { CoreStart } from 'kibana/public'; import { render, unmountComponentAtNode } from 'react-dom'; import { Main } from './components/main'; import { createKibanaReactContext } from '../../../../../src/plugins/kibana_react/public'; -import { executeCode } from './lib/execute_code'; export function renderApp(element: any, { http, i18n, uiSettings }: CoreStart) { const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ @@ -18,7 +17,7 @@ export function renderApp(element: any, { http, i18n, uiSettings }: CoreStart) { render( -
executeCode(http, payload)} /> +
, element From d33a82b9cf758aa05f93fd238ea36238ed9eadb3 Mon Sep 17 00:00:00 2001 From: Alison Goryachev Date: Fri, 13 Mar 2020 16:01:33 -0400 Subject: [PATCH 07/23] [Painless Lab] NP migration (#59794) --- .github/CODEOWNERS | 1 + x-pack/.i18nrc.json | 2 +- x-pack/index.js | 2 - x-pack/legacy/plugins/painless_lab/index.ts | 32 ------ .../plugins/painless_lab/public/register.tsx | 73 -------------- .../painless_lab/server/lib/check_license.ts | 46 --------- .../server/lib/license_pre_routing_factory.ts | 24 ----- .../server/register_execute_route.ts | 31 ------ .../server/register_license_checker.ts | 21 ---- .../plugins/painless_lab/common/constants.ts | 15 +++ x-pack/plugins/painless_lab/kibana.json | 16 +++ .../public/application}/common/constants.tsx | 0 .../public/application}/common/types.ts | 0 .../public/application}/components/editor.tsx | 0 .../public/application}/components/main.tsx | 0 .../application}/components/main_controls.tsx | 0 .../components/output_pane/context_tab.tsx | 0 .../components/output_pane/index.ts | 0 .../components/output_pane/output_pane.tsx | 0 .../components/output_pane/output_tab.tsx | 0 .../components/output_pane/parameters_tab.tsx | 0 .../components/request_flyout.tsx | 0 .../public/application}/hooks/index.ts | 0 .../application}/hooks/use_submit_code.ts | 0 .../public/application}/index.scss | 0 .../public/application/index.tsx} | 21 +++- .../public/application}/lib/execute_code.ts | 4 +- .../public/application}/lib/helpers.ts | 0 .../public/application}/register_painless.ts | 0 x-pack/plugins/painless_lab/public/index.scss | 1 + x-pack/plugins/painless_lab/public/index.ts | 13 +++ x-pack/plugins/painless_lab/public/plugin.tsx | 98 +++++++++++++++++++ .../painless_lab/public/styles/_index.scss | 31 ++++++ x-pack/plugins/painless_lab/public/types.ts | 15 +++ x-pack/plugins/painless_lab/server/index.ts | 11 +++ .../painless_lab/server/lib/index.ts} | 4 +- .../painless_lab/server/lib/is_es_error.ts | 13 +++ x-pack/plugins/painless_lab/server/plugin.ts | 47 +++++++++ .../painless_lab/server/routes/api/execute.ts | 60 ++++++++++++ .../painless_lab/server/routes/api/index.ts | 7 ++ .../painless_lab/server/services/index.ts | 7 ++ .../painless_lab/server/services/license.ts | 82 ++++++++++++++++ x-pack/plugins/painless_lab/server/types.ts | 17 ++++ 43 files changed, 455 insertions(+), 239 deletions(-) delete mode 100644 x-pack/legacy/plugins/painless_lab/index.ts delete mode 100644 x-pack/legacy/plugins/painless_lab/public/register.tsx delete mode 100644 x-pack/legacy/plugins/painless_lab/server/lib/check_license.ts delete mode 100644 x-pack/legacy/plugins/painless_lab/server/lib/license_pre_routing_factory.ts delete mode 100644 x-pack/legacy/plugins/painless_lab/server/register_execute_route.ts delete mode 100644 x-pack/legacy/plugins/painless_lab/server/register_license_checker.ts create mode 100644 x-pack/plugins/painless_lab/common/constants.ts create mode 100644 x-pack/plugins/painless_lab/kibana.json rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/common/constants.tsx (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/common/types.ts (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/components/editor.tsx (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/components/main.tsx (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/components/main_controls.tsx (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/components/output_pane/context_tab.tsx (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/components/output_pane/index.ts (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/components/output_pane/output_pane.tsx (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/components/output_pane/output_tab.tsx (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/components/output_pane/parameters_tab.tsx (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/components/request_flyout.tsx (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/hooks/index.ts (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/hooks/use_submit_code.ts (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/index.scss (100%) rename x-pack/{legacy/plugins/painless_lab/public/render_app.tsx => plugins/painless_lab/public/application/index.tsx} (64%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/lib/execute_code.ts (76%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/lib/helpers.ts (100%) rename x-pack/{legacy/plugins/painless_lab/public => plugins/painless_lab/public/application}/register_painless.ts (100%) create mode 100644 x-pack/plugins/painless_lab/public/index.scss create mode 100644 x-pack/plugins/painless_lab/public/index.ts create mode 100644 x-pack/plugins/painless_lab/public/plugin.tsx create mode 100644 x-pack/plugins/painless_lab/public/styles/_index.scss create mode 100644 x-pack/plugins/painless_lab/public/types.ts create mode 100644 x-pack/plugins/painless_lab/server/index.ts rename x-pack/{legacy/plugins/painless_lab/common/constants.ts => plugins/painless_lab/server/lib/index.ts} (70%) create mode 100644 x-pack/plugins/painless_lab/server/lib/is_es_error.ts create mode 100644 x-pack/plugins/painless_lab/server/plugin.ts create mode 100644 x-pack/plugins/painless_lab/server/routes/api/execute.ts create mode 100644 x-pack/plugins/painless_lab/server/routes/api/index.ts create mode 100644 x-pack/plugins/painless_lab/server/services/index.ts create mode 100644 x-pack/plugins/painless_lab/server/services/license.ts create mode 100644 x-pack/plugins/painless_lab/server/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index de46bcfa69830..ab57fd491d748 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -178,6 +178,7 @@ /x-pack/plugins/remote_clusters/ @elastic/es-ui /x-pack/legacy/plugins/rollup/ @elastic/es-ui /x-pack/plugins/searchprofiler/ @elastic/es-ui +/x-pack/plugins/painless_lab/ @elastic/es-ui /x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui /x-pack/legacy/plugins/upgrade_assistant/ @elastic/es-ui /x-pack/plugins/upgrade_assistant/ @elastic/es-ui diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 6bddbc6577853..abe7952dd5ce4 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -30,7 +30,7 @@ "xpack.ml": ["plugins/ml", "legacy/plugins/ml"], "xpack.monitoring": "legacy/plugins/monitoring", "xpack.remoteClusters": "plugins/remote_clusters", - "xpack.painlessLab": "legacy/plugins/painless_lab", + "xpack.painlessLab": "plugins/painless_lab", "xpack.reporting": ["plugins/reporting", "legacy/plugins/reporting"], "xpack.rollupJobs": "legacy/plugins/rollup", "xpack.searchProfiler": "plugins/searchprofiler", diff --git a/x-pack/index.js b/x-pack/index.js index c06e2536b7328..f3f569e021070 100644 --- a/x-pack/index.js +++ b/x-pack/index.js @@ -12,7 +12,6 @@ import { security } from './legacy/plugins/security'; import { ml } from './legacy/plugins/ml'; import { tilemap } from './legacy/plugins/tilemap'; import { grokdebugger } from './legacy/plugins/grokdebugger'; -import { painlessLab } from './legacy/plugins/painless_lab'; import { dashboardMode } from './legacy/plugins/dashboard_mode'; import { logstash } from './legacy/plugins/logstash'; import { beats } from './legacy/plugins/beats_management'; @@ -52,7 +51,6 @@ module.exports = function(kibana) { ml(kibana), tilemap(kibana), grokdebugger(kibana), - painlessLab(kibana), dashboardMode(kibana), logstash(kibana), beats(kibana), diff --git a/x-pack/legacy/plugins/painless_lab/index.ts b/x-pack/legacy/plugins/painless_lab/index.ts deleted file mode 100644 index 5372bbf37cf2e..0000000000000 --- a/x-pack/legacy/plugins/painless_lab/index.ts +++ /dev/null @@ -1,32 +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 { resolve } from 'path'; -import { PLUGIN_ID } from './common/constants'; - -import { registerLicenseChecker } from './server/register_license_checker'; -import { registerExecuteRoute } from './server/register_execute_route'; -import { Legacy } from '../../../../kibana'; - -export const painlessLab = (kibana: any) => - new kibana.Plugin({ - id: PLUGIN_ID, - publicDir: resolve(__dirname, 'public'), - require: ['kibana', 'elasticsearch', 'xpack_main'], - configPrefix: 'xpack.painless_lab', - config(Joi: any) { - return Joi.object({ - enabled: Joi.boolean().default(true), - }).default(); - }, - uiExports: { - styleSheetPaths: resolve(__dirname, 'public/index.scss'), - devTools: [resolve(__dirname, 'public/register')], - }, - init: (server: Legacy.Server) => { - registerLicenseChecker(server); - registerExecuteRoute(server); - }, - }); diff --git a/x-pack/legacy/plugins/painless_lab/public/register.tsx b/x-pack/legacy/plugins/painless_lab/public/register.tsx deleted file mode 100644 index 1f5446bd07c27..0000000000000 --- a/x-pack/legacy/plugins/painless_lab/public/register.tsx +++ /dev/null @@ -1,73 +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 React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -// @ts-ignore -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { npSetup, npStart } from 'ui/new_platform'; -import { registerPainless } from './register_painless'; -import { FeatureCatalogueCategory } from '../../../../../src/plugins/home/public'; - -npSetup.plugins.home.featureCatalogue.register({ - id: 'painlessLab', - title: i18n.translate('xpack.painlessLab.registryProviderTitle', { - defaultMessage: 'Painless Lab (beta)', - }), - description: i18n.translate('xpack.painlessLab.registryProviderDescription', { - defaultMessage: 'Simulate and debug painless code', - }), - icon: '', - path: '/app/kibana#/dev_tools/painless_lab', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, -}); - -npSetup.plugins.devTools.register({ - id: 'painless_lab', - order: 7, - title: ( - - - {i18n.translate('xpack.painlessLab.displayName', { - defaultMessage: 'Painless Lab', - })} - - - - - - - ) as any, - enableRouting: false, - disabled: false, - tooltipContent: xpackInfo.get('features.painlessLab.message'), - async mount(context, { element }) { - registerPainless(); - - const licenseCheck = { - showPage: xpackInfo.get('features.painlessLab.enableLink'), - message: xpackInfo.get('features.painlessLab.message'), - }; - - if (!licenseCheck.showPage) { - npStart.core.notifications.toasts.addDanger(licenseCheck.message); - window.location.hash = '/dev_tools'; - return () => {}; - } - - const { renderApp } = await import('./render_app'); - return renderApp(element, npStart.core); - }, -}); diff --git a/x-pack/legacy/plugins/painless_lab/server/lib/check_license.ts b/x-pack/legacy/plugins/painless_lab/server/lib/check_license.ts deleted file mode 100644 index 5cbed532ca56a..0000000000000 --- a/x-pack/legacy/plugins/painless_lab/server/lib/check_license.ts +++ /dev/null @@ -1,46 +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 { i18n } from '@kbn/i18n'; - -export function checkLicense(xpackLicenseInfo: any) { - // If, for some reason, we cannot get the license information - // from Elasticsearch, assume worst case and disable the Watcher UI - if (!xpackLicenseInfo || !xpackLicenseInfo.isAvailable()) { - return { - enableLink: false, - enableAPIRoute: false, - message: i18n.translate('xpack.painlessLab.unavailableLicenseInformationMessage', { - defaultMessage: - 'You cannot use the Painless Lab because license information is not available at this time.', - }), - }; - } - - const isLicenseActive = xpackLicenseInfo.license.isActive(); - const licenseType = xpackLicenseInfo.license.getType(); - - // License is not valid - if (!isLicenseActive) { - return { - enableLink: false, - enableAPIRoute: false, - message: i18n.translate('xpack.painlessLab.licenseHasExpiredMessage', { - defaultMessage: - 'You cannot use the Painless Lab because your {licenseType} license has expired.', - values: { - licenseType, - }, - }), - }; - } - - // License is valid and active - return { - enableLink: true, - enableAPIRoute: true, - }; -} diff --git a/x-pack/legacy/plugins/painless_lab/server/lib/license_pre_routing_factory.ts b/x-pack/legacy/plugins/painless_lab/server/lib/license_pre_routing_factory.ts deleted file mode 100644 index 387a263114a6e..0000000000000 --- a/x-pack/legacy/plugins/painless_lab/server/lib/license_pre_routing_factory.ts +++ /dev/null @@ -1,24 +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 Boom from 'boom'; -import { PLUGIN_ID } from '../../common/constants'; -import { ServerFacade } from '../../../index_management'; - -export const licensePreRoutingFactory = (server: ServerFacade) => { - const xpackMainPlugin = server.plugins.xpack_main; - - // License checking and enable/disable logic - function licensePreRouting() { - const licenseCheckResults = xpackMainPlugin.info.feature(PLUGIN_ID).getLicenseCheckResults(); - if (!licenseCheckResults.enableAPIRoute) { - throw Boom.forbidden(licenseCheckResults.message); - } - - return null; - } - - return licensePreRouting; -}; diff --git a/x-pack/legacy/plugins/painless_lab/server/register_execute_route.ts b/x-pack/legacy/plugins/painless_lab/server/register_execute_route.ts deleted file mode 100644 index e4ffad9c21d60..0000000000000 --- a/x-pack/legacy/plugins/painless_lab/server/register_execute_route.ts +++ /dev/null @@ -1,31 +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 { ServerRoute } from 'hapi'; -import { licensePreRoutingFactory } from './lib/license_pre_routing_factory'; -import { Legacy } from '../../../../../kibana'; -import { API_ROUTE_EXECUTE } from '../common/constants'; - -export function registerExecuteRoute(server: any) { - const licensePreRouting = licensePreRoutingFactory(server); - - server.route({ - path: API_ROUTE_EXECUTE, - method: 'POST', - handler: (request: Legacy.Request) => { - const cluster = server.plugins.elasticsearch.getCluster('data'); - return cluster - .callWithRequest(request, 'scriptsPainlessExecute', { - body: request.payload, - }) - .catch((e: any) => { - return e.body; - }); - }, - config: { - pre: [licensePreRouting], - }, - } as ServerRoute); -} diff --git a/x-pack/legacy/plugins/painless_lab/server/register_license_checker.ts b/x-pack/legacy/plugins/painless_lab/server/register_license_checker.ts deleted file mode 100644 index 1ec5b33c4d9f9..0000000000000 --- a/x-pack/legacy/plugins/painless_lab/server/register_license_checker.ts +++ /dev/null @@ -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. - */ -// @ts-ignore -import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; -import { checkLicense } from './lib/check_license'; -import { PLUGIN_ID } from '../common/constants'; - -export function registerLicenseChecker(server: any) { - const xpackMainPlugin = server.plugins.xpack_main; - const plugin = server.plugins[PLUGIN_ID]; - - mirrorPluginStatus(xpackMainPlugin, plugin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense); - }); -} diff --git a/x-pack/plugins/painless_lab/common/constants.ts b/x-pack/plugins/painless_lab/common/constants.ts new file mode 100644 index 0000000000000..dfc7d8ae85a2c --- /dev/null +++ b/x-pack/plugins/painless_lab/common/constants.ts @@ -0,0 +1,15 @@ +/* + * 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 { LicenseType } from '../../licensing/common/types'; + +const basicLicense: LicenseType = 'basic'; + +export const PLUGIN = { + id: 'painlessLab', + minimumLicenseType: basicLicense, +}; + +export const API_BASE_PATH = '/api/painless_lab'; diff --git a/x-pack/plugins/painless_lab/kibana.json b/x-pack/plugins/painless_lab/kibana.json new file mode 100644 index 0000000000000..4b4ea24202846 --- /dev/null +++ b/x-pack/plugins/painless_lab/kibana.json @@ -0,0 +1,16 @@ +{ + "id": "painlessLab", + "version": "8.0.0", + "kibanaVersion": "kibana", + "requiredPlugins": [ + "devTools", + "licensing", + "home" + ], + "configPath": [ + "xpack", + "painless_lab" + ], + "server": true, + "ui": true +} diff --git a/x-pack/legacy/plugins/painless_lab/public/common/constants.tsx b/x-pack/plugins/painless_lab/public/application/common/constants.tsx similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/common/constants.tsx rename to x-pack/plugins/painless_lab/public/application/common/constants.tsx diff --git a/x-pack/legacy/plugins/painless_lab/public/common/types.ts b/x-pack/plugins/painless_lab/public/application/common/types.ts similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/common/types.ts rename to x-pack/plugins/painless_lab/public/application/common/types.ts diff --git a/x-pack/legacy/plugins/painless_lab/public/components/editor.tsx b/x-pack/plugins/painless_lab/public/application/components/editor.tsx similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/components/editor.tsx rename to x-pack/plugins/painless_lab/public/application/components/editor.tsx diff --git a/x-pack/legacy/plugins/painless_lab/public/components/main.tsx b/x-pack/plugins/painless_lab/public/application/components/main.tsx similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/components/main.tsx rename to x-pack/plugins/painless_lab/public/application/components/main.tsx diff --git a/x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx b/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/components/main_controls.tsx rename to x-pack/plugins/painless_lab/public/application/components/main_controls.tsx diff --git a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/context_tab.tsx b/x-pack/plugins/painless_lab/public/application/components/output_pane/context_tab.tsx similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/components/output_pane/context_tab.tsx rename to x-pack/plugins/painless_lab/public/application/components/output_pane/context_tab.tsx diff --git a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/index.ts b/x-pack/plugins/painless_lab/public/application/components/output_pane/index.ts similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/components/output_pane/index.ts rename to x-pack/plugins/painless_lab/public/application/components/output_pane/index.ts diff --git a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_pane.tsx b/x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_pane.tsx rename to x-pack/plugins/painless_lab/public/application/components/output_pane/output_pane.tsx diff --git a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_tab.tsx b/x-pack/plugins/painless_lab/public/application/components/output_pane/output_tab.tsx similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/components/output_pane/output_tab.tsx rename to x-pack/plugins/painless_lab/public/application/components/output_pane/output_tab.tsx diff --git a/x-pack/legacy/plugins/painless_lab/public/components/output_pane/parameters_tab.tsx b/x-pack/plugins/painless_lab/public/application/components/output_pane/parameters_tab.tsx similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/components/output_pane/parameters_tab.tsx rename to x-pack/plugins/painless_lab/public/application/components/output_pane/parameters_tab.tsx diff --git a/x-pack/legacy/plugins/painless_lab/public/components/request_flyout.tsx b/x-pack/plugins/painless_lab/public/application/components/request_flyout.tsx similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/components/request_flyout.tsx rename to x-pack/plugins/painless_lab/public/application/components/request_flyout.tsx diff --git a/x-pack/legacy/plugins/painless_lab/public/hooks/index.ts b/x-pack/plugins/painless_lab/public/application/hooks/index.ts similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/hooks/index.ts rename to x-pack/plugins/painless_lab/public/application/hooks/index.ts diff --git a/x-pack/legacy/plugins/painless_lab/public/hooks/use_submit_code.ts b/x-pack/plugins/painless_lab/public/application/hooks/use_submit_code.ts similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/hooks/use_submit_code.ts rename to x-pack/plugins/painless_lab/public/application/hooks/use_submit_code.ts diff --git a/x-pack/legacy/plugins/painless_lab/public/index.scss b/x-pack/plugins/painless_lab/public/application/index.scss similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/index.scss rename to x-pack/plugins/painless_lab/public/application/index.scss diff --git a/x-pack/legacy/plugins/painless_lab/public/render_app.tsx b/x-pack/plugins/painless_lab/public/application/index.tsx similarity index 64% rename from x-pack/legacy/plugins/painless_lab/public/render_app.tsx rename to x-pack/plugins/painless_lab/public/application/index.tsx index 78102fd1002e5..d980af2779a03 100644 --- a/x-pack/legacy/plugins/painless_lab/public/render_app.tsx +++ b/x-pack/plugins/painless_lab/public/application/index.tsx @@ -5,21 +5,34 @@ */ import React from 'react'; -import { CoreStart } from 'kibana/public'; import { render, unmountComponentAtNode } from 'react-dom'; +import { CoreSetup, CoreStart } from 'kibana/public'; import { Main } from './components/main'; import { createKibanaReactContext } from '../../../../../src/plugins/kibana_react/public'; -export function renderApp(element: any, { http, i18n, uiSettings }: CoreStart) { +interface AppDependencies { + http: CoreSetup['http']; + I18nContext: CoreStart['i18n']['Context']; + uiSettings: CoreSetup['uiSettings']; +} + +export function renderApp( + element: HTMLElement | null, + { http, I18nContext, uiSettings }: AppDependencies +) { + if (!element) { + return () => undefined; + } + const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ uiSettings, }); render( - +
- , + , element ); return () => unmountComponentAtNode(element); diff --git a/x-pack/legacy/plugins/painless_lab/public/lib/execute_code.ts b/x-pack/plugins/painless_lab/public/application/lib/execute_code.ts similarity index 76% rename from x-pack/legacy/plugins/painless_lab/public/lib/execute_code.ts rename to x-pack/plugins/painless_lab/public/application/lib/execute_code.ts index c8b9f2724d38b..ea7adb79cdacb 100644 --- a/x-pack/legacy/plugins/painless_lab/public/lib/execute_code.ts +++ b/x-pack/plugins/painless_lab/public/application/lib/execute_code.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { API_ROUTE_EXECUTE } from '../../common/constants'; +import { API_BASE_PATH } from '../../../common/constants'; export async function executeCode(http: any, payload: Record) { - return await http.post(API_ROUTE_EXECUTE, { + return await http.post(`${API_BASE_PATH}/execute`, { body: JSON.stringify(payload), }); } diff --git a/x-pack/legacy/plugins/painless_lab/public/lib/helpers.ts b/x-pack/plugins/painless_lab/public/application/lib/helpers.ts similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/lib/helpers.ts rename to x-pack/plugins/painless_lab/public/application/lib/helpers.ts diff --git a/x-pack/legacy/plugins/painless_lab/public/register_painless.ts b/x-pack/plugins/painless_lab/public/application/register_painless.ts similarity index 100% rename from x-pack/legacy/plugins/painless_lab/public/register_painless.ts rename to x-pack/plugins/painless_lab/public/application/register_painless.ts diff --git a/x-pack/plugins/painless_lab/public/index.scss b/x-pack/plugins/painless_lab/public/index.scss new file mode 100644 index 0000000000000..370ec54a85539 --- /dev/null +++ b/x-pack/plugins/painless_lab/public/index.scss @@ -0,0 +1 @@ +@import 'styles/index' diff --git a/x-pack/plugins/painless_lab/public/index.ts b/x-pack/plugins/painless_lab/public/index.ts new file mode 100644 index 0000000000000..f81218a4fcd96 --- /dev/null +++ b/x-pack/plugins/painless_lab/public/index.ts @@ -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. + */ + +import './styles/_index.scss'; +import { PluginInitializerContext } from 'src/core/public'; +import { PainlessLabUIPlugin } from './plugin'; + +export function plugin(ctx: PluginInitializerContext) { + return new PainlessLabUIPlugin(ctx); +} diff --git a/x-pack/plugins/painless_lab/public/plugin.tsx b/x-pack/plugins/painless_lab/public/plugin.tsx new file mode 100644 index 0000000000000..57166c5d312a7 --- /dev/null +++ b/x-pack/plugins/painless_lab/public/plugin.tsx @@ -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 from 'react'; +import { i18n } from '@kbn/i18n'; +import { Plugin, CoreStart, CoreSetup, PluginInitializerContext } from 'kibana/public'; +import { first } from 'rxjs/operators'; +import { EuiBetaBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; +import { LICENSE_CHECK_STATE } from '../../licensing/public'; + +import { PLUGIN } from '../common/constants'; +import { PluginDependencies } from './types'; +import { registerPainless } from './application/register_painless'; + +export class PainlessLabUIPlugin implements Plugin { + constructor(ctx: PluginInitializerContext) {} + + async setup( + { http, getStartServices, uiSettings }: CoreSetup, + { devTools, home, licensing }: PluginDependencies + ) { + home.featureCatalogue.register({ + id: PLUGIN.id, + title: i18n.translate('xpack.painlessLab.registryProviderTitle', { + defaultMessage: 'Painless Lab (beta)', + }), + description: i18n.translate('xpack.painlessLab.registryProviderDescription', { + defaultMessage: 'Simulate and debug painless code.', + }), + icon: '', + path: '/app/kibana#/dev_tools/painless_lab', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, + }); + + devTools.register({ + id: 'painless_lab', + order: 7, + title: ( + + + {i18n.translate('xpack.painlessLab.displayName', { + defaultMessage: 'Painless Lab', + })} + + + + + + + ) as any, + enableRouting: false, + disabled: false, + mount: async (ctx, { element }) => { + const [core] = await getStartServices(); + + const { + i18n: { Context: I18nContext }, + notifications, + } = core; + + registerPainless(); + + const license = await licensing.license$.pipe(first()).toPromise(); + const { state, message: invalidLicenseMessage } = license.check( + PLUGIN.id, + PLUGIN.minimumLicenseType + ); + const isValidLicense = state === LICENSE_CHECK_STATE.Valid; + + if (!isValidLicense) { + notifications.toasts.addDanger(invalidLicenseMessage as string); + window.location.hash = '/dev_tools'; + return () => {}; + } + + const { renderApp } = await import('./application'); + return renderApp(element, { I18nContext, http, uiSettings }); + }, + }); + } + + async start(core: CoreStart, plugins: any) {} + + async stop() {} +} diff --git a/x-pack/plugins/painless_lab/public/styles/_index.scss b/x-pack/plugins/painless_lab/public/styles/_index.scss new file mode 100644 index 0000000000000..26f58cf82266a --- /dev/null +++ b/x-pack/plugins/painless_lab/public/styles/_index.scss @@ -0,0 +1,31 @@ + +/** + * 1. This is a very brittle way of preventing the editor and other content from disappearing + * behind the bottom bar. + */ +.painlessLabBottomBarPlaceholder { + height: $euiSize * 3; /* [1] */ +} + +.painlessLabRightPane { + border-right: none; + border-top: none; + border-bottom: none; + border-radius: 0; + padding-top: 0; + height: 100%; +} + +.painlessLabRightPane__tabs { + display: flex; + flex-direction: column; + height: 100%; + + [role="tabpanel"] { + height: 100%; + } +} + +.painlessLab__betaLabelContainer { + line-height: 0; +} diff --git a/x-pack/plugins/painless_lab/public/types.ts b/x-pack/plugins/painless_lab/public/types.ts new file mode 100644 index 0000000000000..9153f4c28de8d --- /dev/null +++ b/x-pack/plugins/painless_lab/public/types.ts @@ -0,0 +1,15 @@ +/* + * 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 { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { DevToolsSetup } from '../../../../src/plugins/dev_tools/public'; +import { LicensingPluginSetup } from '../../licensing/public'; + +export interface PluginDependencies { + licensing: LicensingPluginSetup; + home: HomePublicPluginSetup; + devTools: DevToolsSetup; +} diff --git a/x-pack/plugins/painless_lab/server/index.ts b/x-pack/plugins/painless_lab/server/index.ts new file mode 100644 index 0000000000000..96ea9a163deca --- /dev/null +++ b/x-pack/plugins/painless_lab/server/index.ts @@ -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. + */ +import { PluginInitializerContext } from 'kibana/server'; +import { PainlessLabServerPlugin } from './plugin'; + +export const plugin = (ctx: PluginInitializerContext) => { + return new PainlessLabServerPlugin(ctx); +}; diff --git a/x-pack/legacy/plugins/painless_lab/common/constants.ts b/x-pack/plugins/painless_lab/server/lib/index.ts similarity index 70% rename from x-pack/legacy/plugins/painless_lab/common/constants.ts rename to x-pack/plugins/painless_lab/server/lib/index.ts index 7179daedb338c..a9a3c61472d8c 100644 --- a/x-pack/legacy/plugins/painless_lab/common/constants.ts +++ b/x-pack/plugins/painless_lab/server/lib/index.ts @@ -4,6 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const PLUGIN_ID = 'painlessLab'; - -export const API_ROUTE_EXECUTE = '/api/painless_lab/execute'; +export { isEsError } from './is_es_error'; diff --git a/x-pack/plugins/painless_lab/server/lib/is_es_error.ts b/x-pack/plugins/painless_lab/server/lib/is_es_error.ts new file mode 100644 index 0000000000000..4137293cf39c0 --- /dev/null +++ b/x-pack/plugins/painless_lab/server/lib/is_es_error.ts @@ -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. + */ + +import * as legacyElasticsearch from 'elasticsearch'; + +const esErrorsParent = legacyElasticsearch.errors._Abstract; + +export function isEsError(err: Error) { + return err instanceof esErrorsParent; +} diff --git a/x-pack/plugins/painless_lab/server/plugin.ts b/x-pack/plugins/painless_lab/server/plugin.ts new file mode 100644 index 0000000000000..74629a0b035ed --- /dev/null +++ b/x-pack/plugins/painless_lab/server/plugin.ts @@ -0,0 +1,47 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; + +import { PLUGIN } from '../common/constants'; +import { License } from './services'; +import { Dependencies } from './types'; +import { registerExecuteRoute } from './routes/api'; + +export class PainlessLabServerPlugin implements Plugin { + private readonly license: License; + private readonly logger: Logger; + + constructor({ logger }: PluginInitializerContext) { + this.logger = logger.get(); + this.license = new License(); + } + + async setup({ http }: CoreSetup, { licensing }: Dependencies) { + const router = http.createRouter(); + + this.license.setup( + { + pluginId: PLUGIN.id, + minimumLicenseType: PLUGIN.minimumLicenseType, + defaultErrorMessage: i18n.translate('xpack.painlessLab.licenseCheckErrorMessage', { + defaultMessage: 'License check failed', + }), + }, + { + licensing, + logger: this.logger, + } + ); + + registerExecuteRoute({ router, license: this.license }); + } + + start() {} + + stop() {} +} diff --git a/x-pack/plugins/painless_lab/server/routes/api/execute.ts b/x-pack/plugins/painless_lab/server/routes/api/execute.ts new file mode 100644 index 0000000000000..caf6ce5cb9932 --- /dev/null +++ b/x-pack/plugins/painless_lab/server/routes/api/execute.ts @@ -0,0 +1,60 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +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(), + }) + ), +}); + +export function registerExecuteRoute({ router, license }: RouteDependencies) { + router.post( + { + path: `${API_BASE_PATH}/execute`, + validate: { + body: bodySchema, + }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const body = req.body; + + try { + const callAsCurrentUser = ctx.core.elasticsearch.dataClient.callAsCurrentUser; + + const response = await callAsCurrentUser('scriptsPainlessExecute', { + body, + }); + + return res.ok({ + body: response, + }); + } catch (e) { + if (isEsError(e)) { + // Assume invalid painless script was submitted + // Return 200 with error object + return res.ok({ + body: e.body, + }); + } + return res.internalError({ body: e }); + } + }) + ); +} diff --git a/x-pack/plugins/painless_lab/server/routes/api/index.ts b/x-pack/plugins/painless_lab/server/routes/api/index.ts new file mode 100644 index 0000000000000..62f05971d59cc --- /dev/null +++ b/x-pack/plugins/painless_lab/server/routes/api/index.ts @@ -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 { registerExecuteRoute } from './execute'; diff --git a/x-pack/plugins/painless_lab/server/services/index.ts b/x-pack/plugins/painless_lab/server/services/index.ts new file mode 100644 index 0000000000000..b7a45e59549eb --- /dev/null +++ b/x-pack/plugins/painless_lab/server/services/index.ts @@ -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 { License } from './license'; diff --git a/x-pack/plugins/painless_lab/server/services/license.ts b/x-pack/plugins/painless_lab/server/services/license.ts new file mode 100644 index 0000000000000..1c9d77198f928 --- /dev/null +++ b/x-pack/plugins/painless_lab/server/services/license.ts @@ -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 { Logger } from 'src/core/server'; +import { + KibanaRequest, + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, +} from 'kibana/server'; + +import { LicensingPluginSetup } from '../../../licensing/server'; +import { LicenseType, LICENSE_CHECK_STATE } from '../../../licensing/common/types'; + +export interface LicenseStatus { + isValid: boolean; + message?: string; +} + +interface SetupSettings { + pluginId: string; + minimumLicenseType: LicenseType; + defaultErrorMessage: string; +} + +export class License { + private licenseStatus: LicenseStatus = { + isValid: false, + message: 'Invalid License', + }; + + setup( + { pluginId, minimumLicenseType, defaultErrorMessage }: SetupSettings, + { licensing, logger }: { licensing: LicensingPluginSetup; logger: Logger } + ) { + licensing.license$.subscribe(license => { + const { state, message } = license.check(pluginId, minimumLicenseType); + const hasRequiredLicense = state === LICENSE_CHECK_STATE.Valid; + + if (hasRequiredLicense) { + this.licenseStatus = { isValid: true }; + } else { + this.licenseStatus = { + isValid: false, + message: message || defaultErrorMessage, + }; + if (message) { + logger.info(message); + } + } + }); + } + + guardApiRoute(handler: RequestHandler) { + const license = this; + + return function licenseCheck( + ctx: RequestHandlerContext, + request: KibanaRequest, + response: KibanaResponseFactory + ) { + const licenseStatus = license.getStatus(); + + if (!licenseStatus.isValid) { + return response.customError({ + body: { + message: licenseStatus.message || '', + }, + statusCode: 403, + }); + } + + return handler(ctx, request, response); + }; + } + + getStatus() { + return this.licenseStatus; + } +} diff --git a/x-pack/plugins/painless_lab/server/types.ts b/x-pack/plugins/painless_lab/server/types.ts new file mode 100644 index 0000000000000..541a31dd175ec --- /dev/null +++ b/x-pack/plugins/painless_lab/server/types.ts @@ -0,0 +1,17 @@ +/* + * 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 { IRouter } from 'src/core/server'; +import { LicensingPluginSetup } from '../../licensing/server'; +import { License } from './services'; + +export interface RouteDependencies { + router: IRouter; + license: License; +} + +export interface Dependencies { + licensing: LicensingPluginSetup; +} From b7840ffdcf397c8119f005ce4bea1b0d6b8fc353 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Fri, 13 Mar 2020 21:32:47 -0700 Subject: [PATCH 08/23] Fix sample document editor. --- .../components/output_pane/context_tab.tsx | 198 +++++++++--------- .../components/output_pane/parameters_tab.tsx | 2 +- 2 files changed, 98 insertions(+), 102 deletions(-) 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 06325c5c2661e..619c966506ab7 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 @@ -29,121 +29,117 @@ interface Props { onContextChange: ContextChangeHandler; } -export function ContextTab({ context, contextSetup, onContextChange }: Props) { - return ( - <> - +export const ContextTab = ({ context, contextSetup, onContextChange }: Props) => ( + <> + + + + {' '} + + + + } + labelAppend={ + + + {i18n.translate('xpack.painlessLab.contextFieldDocLinkText', { + defaultMessage: 'Context docs', + })} + + + } + fullWidth + > + onContextChange({ context: value })} + itemLayoutAlign="top" + hasDividers + fullWidth + /> + + + {['filter', 'score'].indexOf(context) !== -1 && ( - {' '} + {' '} } - labelAppend={ - - - {i18n.translate('xpack.painlessLab.contextFieldDocLinkText', { - defaultMessage: 'Context docs', - })} - - - } fullWidth > - onContextChange({ context: value })} - itemLayoutAlign="top" - hasDividers + { + onContextChange({ + contextSetup: Object.assign({}, contextSetup, { index: e.target.value }), + }); + }} /> - - {['filter', 'score'].indexOf(context) !== -1 && ( - - - {' '} - - - - } - fullWidth - > - { - onContextChange({ - contextSetup: Object.assign({}, contextSetup, { index: e.target.value }), - }); + )} + {['filter', 'score'].indexOf(context) !== -1 && ( + + + {' '} + + + + } + fullWidth + > + + { + const newContextSetup = Object.assign({}, contextSetup, { document: value }); + onContextChange({ contextSetup: newContextSetup }); + }} + options={{ + fontSize: 12, + minimap: { + enabled: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + automaticLayout: true, }} /> - - )} - {['filter', 'score'].indexOf(context) !== -1 && ( - - - {' '} - - - - } - fullWidth - > - - { - onContextChange({ - contextSetup: Object.assign({}, contextSetup, { document: value }), - }); - }} - options={{ - fontSize: 12, - minimap: { - enabled: false, - }, - scrollBeyondLastLine: false, - wordWrap: 'on', - wrappingIndent: 'indent', - automaticLayout: true, - }} - /> - - - )} - - ); -} + + + )} + +); 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 b731ba0cbdaa7..3542c99b2584f 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 @@ -62,7 +62,7 @@ export function ParametersTab({ contextSetup, onContextChange }: Props) { > onContextChange({ contextSetup: { params: value } })} From 3de6244b8640b3ece17758673f319b7be9c44ce0 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Mon, 16 Mar 2020 19:03:37 -0700 Subject: [PATCH 09/23] [Painless Lab] Fix float -> integer coercion bug (#60201) * Clarify data and persistence flow. Fix floating point precision bug. * Send a string to API and ES client instead of an object. --- .../public/application/common/constants.tsx | 3 +- .../public/application/common/types.ts | 34 ++------ .../public/application/components/editor.tsx | 6 +- .../public/application/components/main.tsx | 87 +++++++++++-------- .../components/output_pane/context_tab.tsx | 38 ++++---- .../components/output_pane/output_pane.tsx | 33 +++++-- .../components/output_pane/parameters_tab.tsx | 19 ++-- .../application/hooks/use_submit_code.ts | 16 ++-- .../public/application/lib/execute_code.ts | 13 --- .../public/application/lib/helpers.ts | 80 +++++++++-------- .../painless_lab/server/routes/api/execute.ts | 16 +--- 11 files changed, 172 insertions(+), 173 deletions(-) delete mode 100644 x-pack/plugins/painless_lab/public/application/lib/execute_code.ts 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, }); From f89b489835364f6c1f31f7766fb168a3d32ff5cd Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Mon, 16 Mar 2020 21:34:14 -0700 Subject: [PATCH 10/23] Rename helpers lib to format. Add tests for formatRequestPayload. --- .../public/application/common/types.ts | 10 +- .../public/application/components/main.tsx | 4 +- .../components/output_pane/output_tab.tsx | 2 +- .../application/hooks/use_submit_code.ts | 4 +- .../lib/__snapshots__/format.test.ts.snap | 205 ++++++++++++++++++ .../public/application/lib/format.test.ts | 86 ++++++++ .../application/lib/{helpers.ts => format.ts} | 38 ++-- 7 files changed, 323 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/painless_lab/public/application/lib/__snapshots__/format.test.ts.snap create mode 100644 x-pack/plugins/painless_lab/public/application/lib/format.test.ts rename x-pack/plugins/painless_lab/public/application/lib/{helpers.ts => format.ts} (77%) 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 cf3d58d6b26d0..97f83f7088a1e 100644 --- a/x-pack/plugins/painless_lab/public/application/common/types.ts +++ b/x-pack/plugins/painless_lab/public/application/common/types.ts @@ -8,11 +8,11 @@ export type Context = string; export interface RequestPayloadConfig { - code: string; - context: string; - parameters: string; - index: string; - document: string; + code?: string; + context?: string; + parameters?: string; + index?: string; + document?: string; } export enum PayloadFormat { diff --git a/x-pack/plugins/painless_lab/public/application/components/main.tsx b/x-pack/plugins/painless_lab/public/application/components/main.tsx index 9b4263773f5f8..b88d96b897ea2 100644 --- a/x-pack/plugins/painless_lab/public/application/components/main.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/main.tsx @@ -8,7 +8,7 @@ import { HttpSetup } from 'kibana/public'; import React, { useState, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { buildRequestPayload, formatJson } from '../lib/helpers'; +import { formatRequestPayload, formatJson } from '../lib/format'; import { painlessContextOptions, exampleScript } from '../common/constants'; import { PayloadFormat } from '../common/types'; import { useSubmitCode } from '../hooks'; @@ -108,7 +108,7 @@ export function Main({ http }: Props) { {isRequestFlyoutOpen && ( setRequestFlyoutOpen(false)} - requestBody={buildRequestPayload( + requestBody={formatRequestPayload( { code, context, document, index, parameters }, PayloadFormat.PRETTY )} diff --git a/x-pack/plugins/painless_lab/public/application/components/output_pane/output_tab.tsx b/x-pack/plugins/painless_lab/public/application/components/output_pane/output_tab.tsx index 55bb9a34c78b6..fafd0f1f7cde4 100644 --- a/x-pack/plugins/painless_lab/public/application/components/output_pane/output_tab.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/output_pane/output_tab.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; -import { formatResponse } from '../../lib/helpers'; +import { formatResponse } from '../../lib/format'; import { Response } from '../../common/types'; interface Props { 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 d4aa6c2af9f16..a4be0886f08bc 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 @@ -10,7 +10,7 @@ import { debounce } from 'lodash'; import { API_BASE_PATH } from '../../../common/constants'; import { Response, RequestPayloadConfig, PayloadFormat } from '../common/types'; -import { buildRequestPayload } from '../lib/helpers'; +import { formatRequestPayload } from '../lib/format'; const DEBOUNCE_MS = 800; @@ -32,7 +32,7 @@ export const useSubmitCode = (http: HttpSetup) => { 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)), + body: JSON.stringify(formatRequestPayload(config, PayloadFormat.UGLY)), }); if (currentRequestIdRef.current === requestId) { diff --git a/x-pack/plugins/painless_lab/public/application/lib/__snapshots__/format.test.ts.snap b/x-pack/plugins/painless_lab/public/application/lib/__snapshots__/format.test.ts.snap new file mode 100644 index 0000000000000..4df90d1b3abe1 --- /dev/null +++ b/x-pack/plugins/painless_lab/public/application/lib/__snapshots__/format.test.ts.snap @@ -0,0 +1,205 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`formatRequestPayload pretty formats a complex multi-line script 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"\\"\\"// Here's a comment and a variable, then a loop. + double halfWidth = Math.floor(width * 0.5); + for (int y = 0; y < height; y++) { + return \\"results here\\\\\\\\n\\"; + } + + return result;\\"\\"\\" + } +}" +`; + +exports[`formatRequestPayload pretty formats a single-line script 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"\\"\\"return \\"ok\\";\\"\\"\\" + } +}" +`; + +exports[`formatRequestPayload pretty formats code and parameters 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"\\"\\"return \\"ok\\";\\"\\"\\", + \\"params\\": { + \\"a\\": { + \\"b\\": \\"c\\", + \\"d\\": \\"e\\" + } + } + } +}" +`; + +exports[`formatRequestPayload pretty formats code, context, index, and document 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"\\"\\"return \\"ok\\";\\"\\"\\" + }, + \\"context\\": \\"filter\\", + \\"context_setup\\": { + \\"index\\": \\"index\\", + \\"document\\": { + \\"a\\": { + \\"b\\": \\"c\\", + \\"d\\": \\"e\\" + } + } + } +}" +`; + +exports[`formatRequestPayload pretty formats code, parameters, and context 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"\\"\\"return \\"ok\\";\\"\\"\\", + \\"params\\": { + \\"a\\": { + \\"b\\": \\"c\\", + \\"d\\": \\"e\\" + } + } + }, + \\"context\\": \\"filter\\", + \\"context_setup\\": { + \\"index\\": \\"\\", + \\"document\\": + } +}" +`; + +exports[`formatRequestPayload pretty formats code, parameters, context, index, and document 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"\\"\\"return \\"ok\\";\\"\\"\\", + \\"params\\": { + \\"a\\": { + \\"b\\": \\"c\\", + \\"d\\": \\"e\\" + } + } + }, + \\"context\\": \\"filter\\", + \\"context_setup\\": { + \\"index\\": \\"index\\", + \\"document\\": { + \\"a\\": { + \\"b\\": \\"c\\", + \\"d\\": \\"e\\" + } + } + } +}" +`; + +exports[`formatRequestPayload pretty formats no script 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"\\"\\"\\"\\"\\" + } +}" +`; + +exports[`formatRequestPayload ugly formats a complex multi-line script 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"// Here's a comment and a variable, then a loop.\\\\ndouble halfWidth = Math.floor(width * 0.5);\\\\nfor (int y = 0; y < height; y++) {\\\\n return \\\\\\"results here\\\\\\\\\\\\\\\\n\\\\\\";\\\\n}\\\\n\\\\nreturn result;\\" + } +}" +`; + +exports[`formatRequestPayload ugly formats a single-line script 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"return \\\\\\"ok\\\\\\";\\" + } +}" +`; + +exports[`formatRequestPayload ugly formats code and parameters 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"return \\\\\\"ok\\\\\\";\\", + \\"params\\": { + \\"a\\": { + \\"b\\": \\"c\\", + \\"d\\": \\"e\\" + } +} + } +}" +`; + +exports[`formatRequestPayload ugly formats code, context, index, and document 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"return \\\\\\"ok\\\\\\";\\" + }, + \\"context\\": \\"filter\\", + \\"context_setup\\": { + \\"index\\": \\"index\\", + \\"document\\": { + \\"a\\": { + \\"b\\": \\"c\\", + \\"d\\": \\"e\\" + } +} + } +}" +`; + +exports[`formatRequestPayload ugly formats code, parameters, and context 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"return \\\\\\"ok\\\\\\";\\", + \\"params\\": { + \\"a\\": { + \\"b\\": \\"c\\", + \\"d\\": \\"e\\" + } +} + }, + \\"context\\": \\"filter\\", + \\"context_setup\\": { + \\"index\\": \\"undefined\\", + \\"document\\": undefined + } +}" +`; + +exports[`formatRequestPayload ugly formats code, parameters, context, index, and document 1`] = ` +"{ + \\"script\\": { + \\"source\\": \\"return \\\\\\"ok\\\\\\";\\", + \\"params\\": { + \\"a\\": { + \\"b\\": \\"c\\", + \\"d\\": \\"e\\" + } +} + }, + \\"context\\": \\"filter\\", + \\"context_setup\\": { + \\"index\\": \\"index\\", + \\"document\\": { + \\"a\\": { + \\"b\\": \\"c\\", + \\"d\\": \\"e\\" + } +} + } +}" +`; + +exports[`formatRequestPayload ugly formats no script 1`] = ` +"{ + \\"script\\": { + \\"source\\": undefined + } +}" +`; diff --git a/x-pack/plugins/painless_lab/public/application/lib/format.test.ts b/x-pack/plugins/painless_lab/public/application/lib/format.test.ts new file mode 100644 index 0000000000000..1f46d6e665bcc --- /dev/null +++ b/x-pack/plugins/painless_lab/public/application/lib/format.test.ts @@ -0,0 +1,86 @@ +/* + * 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 { PayloadFormat } from '../common/types'; +import { formatRequestPayload } from './format'; + +describe('formatRequestPayload', () => { + Object.values(PayloadFormat).forEach(format => { + describe(`${format} formats`, () => { + test('no script', () => { + expect(formatRequestPayload({}, format)).toMatchSnapshot(); + }); + + test('a single-line script', () => { + const code = 'return "ok";'; + expect(formatRequestPayload({ code }, format)).toMatchSnapshot(); + }); + + test('a complex multi-line script', () => { + const code = `// Here's a comment and a variable, then a loop. +double halfWidth = Math.floor(width * 0.5); +for (int y = 0; y < height; y++) { + return "results here\\\\n"; +} + +return result;`; + expect(formatRequestPayload({ code }, format)).toMatchSnapshot(); + }); + + test('code and parameters', () => { + const code = 'return "ok";'; + const parameters = `{ + "a": { + "b": "c", + "d": "e" + } +}`; + expect(formatRequestPayload({ code, parameters }, format)).toMatchSnapshot(); + }); + + test('code, parameters, and context', () => { + const code = 'return "ok";'; + const parameters = `{ + "a": { + "b": "c", + "d": "e" + } +}`; + const context = 'filter'; + expect(formatRequestPayload({ code, parameters, context }, format)).toMatchSnapshot(); + }); + + test('code, context, index, and document', () => { + const code = 'return "ok";'; + const context = 'filter'; + const index = 'index'; + const document = `{ + "a": { + "b": "c", + "d": "e" + } +}`; + expect(formatRequestPayload({ code, context, index, document }, format)).toMatchSnapshot(); + }); + + test('code, parameters, context, index, and document', () => { + const code = 'return "ok";'; + const parameters = `{ + "a": { + "b": "c", + "d": "e" + } +}`; + const context = 'filter'; + const index = 'index'; + const document = parameters; + expect( + formatRequestPayload({ code, parameters, context, index, document }, format) + ).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/x-pack/plugins/painless_lab/public/application/lib/helpers.ts b/x-pack/plugins/painless_lab/public/application/lib/format.ts similarity index 77% rename from x-pack/plugins/painless_lab/public/application/lib/helpers.ts rename to x-pack/plugins/painless_lab/public/application/lib/format.ts index 2152ee03a8af0..0b941577c0ddc 100644 --- a/x-pack/plugins/painless_lab/public/application/lib/helpers.ts +++ b/x-pack/plugins/painless_lab/public/application/lib/format.ts @@ -6,14 +6,6 @@ import { RequestPayloadConfig, Response, ExecutionError, PayloadFormat } from '../common/types'; -export function parseJSON(text: string) { - try { - return JSON.parse(text); - } catch (e) { - return {}; - } -} - function prettifyPayload(payload = '', indentationLevel = 0) { const indentation = new Array(indentationLevel + 1).join(' '); return payload.replace(/\n/g, `\n${indentation}`); @@ -23,18 +15,32 @@ function prettifyPayload(payload = '', indentationLevel = 0) { * 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 buildRequestPayload( +export function formatRequestPayload( { 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); + + let formattedCode; + let formattedParameters; + let formattedContext; + let formattedIndex; + let formattedDocument; + + if (format === PayloadFormat.UGLY) { + formattedCode = JSON.stringify(code); + formattedParameters = parameters; + formattedContext = context; + formattedIndex = index; + formattedDocument = document; + } else { + // Triple quote the code because it's multiline. + formattedCode = `"""${prettifyPayload(code, 4)}"""`; + formattedParameters = prettifyPayload(parameters, 4); + formattedContext = prettifyPayload(context, 6); + formattedIndex = prettifyPayload(index); + formattedDocument = prettifyPayload(document, 4); + } const requestPayload = `{ "script": { From f5bec8ea666c873ffb1bbfc79b88bf3801ab92fc Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 18 Mar 2020 16:24:53 +0100 Subject: [PATCH 11/23] Add query parameter to score context (#60414) * Fix typo and i18n * Make state init lazy Otherwise we are needlessly reading and JSON.parse'ing on every state update * Support the query parameter in requests to Painless * Fix borked i18n --- .../public/application/common/constants.tsx | 16 ++- .../public/application/common/types.ts | 1 + .../public/application/components/main.tsx | 24 +++-- .../application/components/main_controls.tsx | 102 +++++++++--------- .../components/output_pane/context_tab.tsx | 54 ++++++++++ .../components/output_pane/output_pane.tsx | 6 ++ .../public/application/lib/format.ts | 22 ++-- x-pack/plugins/painless_lab/public/index.scss | 2 +- .../painless_lab/public/styles/_index.scss | 19 +++- 9 files changed, 169 insertions(+), 77 deletions(-) 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 8ee10a7392557..8cf3c6b03a2e6 100644 --- a/x-pack/plugins/painless_lab/public/application/common/constants.tsx +++ b/x-pack/plugins/painless_lab/public/application/common/constants.tsx @@ -29,7 +29,11 @@ export const painlessContextOptions = [ <> {defaultLabel} -

The script result will be converted to a string

+

+ {i18n.translate('xpack.painlessLab.context.scoreLabel', { + defaultMessage: 'The script result will be converted to a string', + })} +

), @@ -41,7 +45,11 @@ export const painlessContextOptions = [ <> {filterLabel} -

Use the context of a filter’s script query

+

+ {i18n.translate('xpack.painlessLab.context.scoreLabel', { + defaultMessage: "Use the context of a filter's script query", + })} +

), @@ -54,7 +62,9 @@ export const painlessContextOptions = [ {scoreLabel}

- Use the context of a cript_score function in function_score query + {i18n.translate('xpack.painlessLab.context.scoreLabel', { + defaultMessage: 'Use the context of a script_score function in function_score query', + })}

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 97f83f7088a1e..d2fda58eff13b 100644 --- a/x-pack/plugins/painless_lab/public/application/common/types.ts +++ b/x-pack/plugins/painless_lab/public/application/common/types.ts @@ -13,6 +13,7 @@ export interface RequestPayloadConfig { parameters?: string; index?: string; document?: string; + query?: string; } export enum PayloadFormat { diff --git a/x-pack/plugins/painless_lab/public/application/components/main.tsx b/x-pack/plugins/painless_lab/public/application/components/main.tsx index b88d96b897ea2..88685874d19e3 100644 --- a/x-pack/plugins/painless_lab/public/application/components/main.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/main.tsx @@ -24,20 +24,21 @@ interface Props { const PAINLESS_LAB_KEY = 'painlessLabState'; export function Main({ http }: Props) { - const [state, setState] = useState({ + const [state, setState] = useState(() => ({ code: exampleScript, context: painlessContextOptions[0].value, parameters: '', index: '', document: '', + query: '', ...JSON.parse(localStorage.getItem(PAINLESS_LAB_KEY) || '{}'), - }); + })); const [isRequestFlyoutOpen, setRequestFlyoutOpen] = useState(false); const { inProgress, response, submit } = useSubmitCode(http); // Live-update the output and persist state as the user changes it. - const { code, context, parameters, index, document } = state; + const { code, context, parameters, index, document, query } = state; useEffect(() => { submit(state); localStorage.setItem(PAINLESS_LAB_KEY, JSON.stringify(state)); @@ -63,13 +64,17 @@ export function Main({ http }: Props) { setState({ ...state, document: newDocument }); }; + const onQueryChange = (newQuery: string) => { + setState({ ...state, query: newQuery }); + }; + const toggleRequestFlyout = () => { setRequestFlyoutOpen(!isRequestFlyoutOpen); }; return ( - <> - +
+

@@ -90,10 +95,12 @@ export function Main({ http }: Props) { parameters={parameters} index={index} document={document} + query={query} onContextChange={onContextChange} onParametersChange={onParametersChange} onIndexChange={onIndexChange} onDocumentChange={onDocumentChange} + onQueryChange={onQueryChange} /> @@ -108,13 +115,10 @@ export function Main({ http }: Props) { {isRequestFlyoutOpen && ( setRequestFlyoutOpen(false)} - requestBody={formatRequestPayload( - { code, context, document, index, parameters }, - PayloadFormat.PRETTY - )} + requestBody={formatRequestPayload(state, PayloadFormat.PRETTY)} response={response ? formatJson(response.result || response.error) : ''} /> )} - +

); } diff --git a/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx b/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx index 9e24fd9fb5a93..cbdf92d01b7de 100644 --- a/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx @@ -77,58 +77,54 @@ export function MainControls({ toggleRequestFlyout, isRequestFlyoutOpen, reset } ]; return ( - <> -
- - - - - - - setIsHelpOpen(!isHelpOpen)} - > - {i18n.translate('xpack.painlessLab.helpButtonLabel', { - defaultMessage: 'Help', - })} - - } - isOpen={isHelpOpen} - closePopover={() => setIsHelpOpen(false)} - panelPaddingSize="none" - withTitle - anchorPosition="upRight" - > - - - - - - - - {isRequestFlyoutOpen - ? i18n.translate('xpack.painlessLab.hideRequestButtonLabel', { - defaultMessage: 'Hide API request', - }) - : i18n.translate('xpack.painlessLab.showRequestButtonLabel', { - defaultMessage: 'Show API request', - })} - - - - - + + + + + + setIsHelpOpen(!isHelpOpen)} + > + {i18n.translate('xpack.painlessLab.helpButtonLabel', { + defaultMessage: 'Help', + })} + + } + isOpen={isHelpOpen} + closePopover={() => setIsHelpOpen(false)} + panelPaddingSize="none" + withTitle + anchorPosition="upRight" + > + + + + + + + + {isRequestFlyoutOpen + ? i18n.translate('xpack.painlessLab.hideRequestButtonLabel', { + defaultMessage: 'Hide API request', + }) + : i18n.translate('xpack.painlessLab.showRequestButtonLabel', { + defaultMessage: 'Show API request', + })} + + + + ); } 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 4a0c18733075f..b1ac68e1f5719 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 @@ -26,18 +26,22 @@ interface Props { context: any; index: string; document: string; + query: string; onContextChange: (context: string) => void; onIndexChange: (index: string) => void; onDocumentChange: (document: string) => void; + onQueryChange: (query: string) => void; } export const ContextTab = ({ context, index, document, + query, onContextChange, onIndexChange, onDocumentChange, + onQueryChange, }: Props) => ( <> @@ -100,6 +104,56 @@ export const ContextTab = ({ onIndexChange(e.target.value)} /> )} + {/* Query DSL Code Editor */} + {'score'.indexOf(context) !== -1 && ( + + + {' '} + + + + } + labelAppend={ + + + {i18n.translate('xpack.painlessLab.queryFieldDocLinkText', { + defaultMessage: 'Query DSL docs', + })} + + + } + fullWidth + > + + + + + )} {['filter', 'score'].indexOf(context) !== -1 && ( void; onParametersChange: (change: string) => void; onIndexChange: (change: string) => void; onDocumentChange: (change: string) => void; + onQueryChange: (change: string) => void; } export function OutputPane({ @@ -40,10 +42,12 @@ export function OutputPane({ parameters, index, document, + query, onContextChange, onParametersChange, onIndexChange, onDocumentChange, + onQueryChange, }: Props) { const outputTabLabel = ( @@ -96,9 +100,11 @@ export function OutputPane({ context={context} index={index} document={document} + query={query} onContextChange={onContextChange} onIndexChange={onIndexChange} onDocumentChange={onDocumentChange} + onQueryChange={onQueryChange} /> ), }, diff --git a/x-pack/plugins/painless_lab/public/application/lib/format.ts b/x-pack/plugins/painless_lab/public/application/lib/format.ts index 0b941577c0ddc..5f98266ee9774 100644 --- a/x-pack/plugins/painless_lab/public/application/lib/format.ts +++ b/x-pack/plugins/painless_lab/public/application/lib/format.ts @@ -16,16 +16,17 @@ function prettifyPayload(payload = '', indentationLevel = 0) { * e.g. 1.0, is preserved instead of being coerced to an integer, e.g. 1. */ export function formatRequestPayload( - { code, context, parameters, index, document }: RequestPayloadConfig, + { code, context, parameters, index, document, query }: RequestPayloadConfig, format: PayloadFormat = PayloadFormat.UGLY ): string { const isAdvancedContext = context === 'filter' || context === 'score'; - let formattedCode; - let formattedParameters; - let formattedContext; - let formattedIndex; - let formattedDocument; + let formattedCode: string | undefined; + let formattedParameters: string | undefined; + let formattedContext: string | undefined; + let formattedIndex: string | undefined; + let formattedDocument: string | undefined; + let formattedQuery: string | undefined; if (format === PayloadFormat.UGLY) { formattedCode = JSON.stringify(code); @@ -33,6 +34,7 @@ export function formatRequestPayload( formattedContext = context; formattedIndex = index; formattedDocument = document; + formattedQuery = query; } else { // Triple quote the code because it's multiline. formattedCode = `"""${prettifyPayload(code, 4)}"""`; @@ -40,6 +42,7 @@ export function formatRequestPayload( formattedContext = prettifyPayload(context, 6); formattedIndex = prettifyPayload(index); formattedDocument = prettifyPayload(document, 4); + formattedQuery = prettifyPayload(query, 4); } const requestPayload = `{ @@ -56,7 +59,12 @@ export function formatRequestPayload( "context": "${formattedContext}", "context_setup": { "index": "${formattedIndex}", - "document": ${formattedDocument} + "document": ${formattedDocument}${ + query && context === 'score' + ? `, + "query": ${formattedQuery}` + : '' + } }` : `` } diff --git a/x-pack/plugins/painless_lab/public/index.scss b/x-pack/plugins/painless_lab/public/index.scss index 370ec54a85539..29a5761255278 100644 --- a/x-pack/plugins/painless_lab/public/index.scss +++ b/x-pack/plugins/painless_lab/public/index.scss @@ -1 +1 @@ -@import 'styles/index' +@import 'styles/index'; diff --git a/x-pack/plugins/painless_lab/public/styles/_index.scss b/x-pack/plugins/painless_lab/public/styles/_index.scss index 26f58cf82266a..733c5a4b56aef 100644 --- a/x-pack/plugins/painless_lab/public/styles/_index.scss +++ b/x-pack/plugins/painless_lab/public/styles/_index.scss @@ -1,10 +1,13 @@ +@import '@elastic/eui/src/components/header/variables'; /** - * 1. This is a very brittle way of preventing the editor and other content from disappearing - * behind the bottom bar. + * This is a very brittle way of preventing the editor and other content from disappearing + * behind the bottom bar. */ +$bottomBarHeight: calc(#{$euiSize} * 3); + .painlessLabBottomBarPlaceholder { - height: $euiSize * 3; /* [1] */ + height: $bottomBarHeight } .painlessLabRightPane { @@ -23,9 +26,19 @@ [role="tabpanel"] { height: 100%; + overflow-y: auto; } } .painlessLab__betaLabelContainer { line-height: 0; } + +.painlessLabMainContainer { + height: calc(100vh - calc(#{$euiHeaderChildSize} * 2) - #{$bottomBarHeight}); +} + +.painlessLabPanelsContainer { + // The panels container should adopt the height of the main container + height: 100%; +} From 1027b8ad37a46ef1b9b5651e1a049d29a35c2e28 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 18 Mar 2020 18:38:12 +0100 Subject: [PATCH 12/23] Fix i18n --- .../painless_lab/public/application/common/constants.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8cf3c6b03a2e6..c33eb329e4c84 100644 --- a/x-pack/plugins/painless_lab/public/application/common/constants.tsx +++ b/x-pack/plugins/painless_lab/public/application/common/constants.tsx @@ -46,7 +46,7 @@ export const painlessContextOptions = [ {filterLabel}

- {i18n.translate('xpack.painlessLab.context.scoreLabel', { + {i18n.translate('xpack.painlessLab.context.filterLabel', { defaultMessage: "Use the context of a filter's script query", })}

From bcdcbe1de12301c3852aca6546554b5565b4ff6a Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 18 Mar 2020 18:48:03 +0100 Subject: [PATCH 13/23] Another i18n issue --- .../painless_lab/public/application/common/constants.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c33eb329e4c84..d8430dbfc7d9d 100644 --- a/x-pack/plugins/painless_lab/public/application/common/constants.tsx +++ b/x-pack/plugins/painless_lab/public/application/common/constants.tsx @@ -30,7 +30,7 @@ export const painlessContextOptions = [ {defaultLabel}

- {i18n.translate('xpack.painlessLab.context.scoreLabel', { + {i18n.translate('xpack.painlessLab.context.defaultLabel', { defaultMessage: 'The script result will be converted to a string', })}

From c2063209c2d128379e5d17ed4fd76c13aac88e7b Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 19 Mar 2020 10:02:57 +0100 Subject: [PATCH 14/23] [Painless] Minor state update model refactor (#60532) * Fix typo and i18n * Make state init lazy Otherwise we are needlessly reading and JSON.parse'ing on every state update * Support the query parameter in requests to Painless * WiP on state refactor * Some cleanup after manual testing * Fix types and i18n --- .../public/application/common/types.ts | 9 - .../public/application/components/main.tsx | 69 +---- .../components/output_pane/context_tab.tsx | 292 +++++++++--------- .../components/output_pane/output_pane.tsx | 46 +-- .../components/output_pane/parameters_tab.tsx | 16 +- .../public/application/constants.ts | 7 + .../public/application/context.tsx | 44 +++ .../application/hooks/use_submit_code.ts | 5 +- .../painless_lab/public/application/index.tsx | 8 +- .../public/application/lib/format.ts | 5 +- .../painless_lab/public/application/store.ts | 24 ++ 11 files changed, 254 insertions(+), 271 deletions(-) create mode 100644 x-pack/plugins/painless_lab/public/application/constants.ts create mode 100644 x-pack/plugins/painless_lab/public/application/context.tsx create mode 100644 x-pack/plugins/painless_lab/public/application/store.ts 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 d2fda58eff13b..e0c7a8c7a6ff3 100644 --- a/x-pack/plugins/painless_lab/public/application/common/types.ts +++ b/x-pack/plugins/painless_lab/public/application/common/types.ts @@ -7,15 +7,6 @@ // This should be an enumerated list export type Context = string; -export interface RequestPayloadConfig { - code?: string; - context?: string; - parameters?: string; - index?: string; - document?: string; - query?: string; -} - export enum PayloadFormat { UGLY = 'ugly', PRETTY = 'pretty', diff --git a/x-pack/plugins/painless_lab/public/application/components/main.tsx b/x-pack/plugins/painless_lab/public/application/components/main.tsx index 88685874d19e3..4df3a484bddce 100644 --- a/x-pack/plugins/painless_lab/public/application/components/main.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/main.tsx @@ -5,69 +5,34 @@ */ import { HttpSetup } from 'kibana/public'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, FunctionComponent } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { formatRequestPayload, formatJson } from '../lib/format'; -import { painlessContextOptions, exampleScript } from '../common/constants'; +import { exampleScript } from '../common/constants'; import { PayloadFormat } from '../common/types'; import { useSubmitCode } from '../hooks'; import { OutputPane } from './output_pane'; import { MainControls } from './main_controls'; import { Editor } from './editor'; import { RequestFlyout } from './request_flyout'; +import { useAppContext } from '../context'; interface Props { http: HttpSetup; } -const PAINLESS_LAB_KEY = 'painlessLabState'; - -export function Main({ http }: Props) { - const [state, setState] = useState(() => ({ - code: exampleScript, - context: painlessContextOptions[0].value, - parameters: '', - index: '', - document: '', - query: '', - ...JSON.parse(localStorage.getItem(PAINLESS_LAB_KEY) || '{}'), - })); +export const Main: FunctionComponent = ({ http }) => { + const { state, updateState } = useAppContext(); const [isRequestFlyoutOpen, setRequestFlyoutOpen] = useState(false); const { inProgress, response, submit } = useSubmitCode(http); // Live-update the output and persist state as the user changes it. - const { code, context, parameters, index, document, query } = state; useEffect(() => { submit(state); - localStorage.setItem(PAINLESS_LAB_KEY, JSON.stringify(state)); }, [state, submit]); - const onCodeChange = (newCode: string) => { - setState({ ...state, code: newCode }); - }; - - const onContextChange = (newContext: string) => { - setState({ ...state, context: newContext }); - }; - - const onParametersChange = (newParameters: string) => { - setState({ ...state, parameters: newParameters }); - }; - - const onIndexChange = (newIndex: string) => { - setState({ ...state, index: newIndex }); - }; - - const onDocumentChange = (newDocument: string) => { - setState({ ...state, document: newDocument }); - }; - - const onQueryChange = (newQuery: string) => { - setState({ ...state, query: newQuery }); - }; - const toggleRequestFlyout = () => { setRequestFlyoutOpen(!isRequestFlyoutOpen); }; @@ -84,24 +49,14 @@ export function Main({ http }: Props) { - + updateState(() => ({ code: nextCode }))} + /> - +
@@ -109,7 +64,7 @@ export function Main({ http }: Props) { isLoading={inProgress} toggleRequestFlyout={toggleRequestFlyout} isRequestFlyoutOpen={isRequestFlyoutOpen} - reset={() => onCodeChange(exampleScript)} + reset={() => updateState(() => ({ code: exampleScript }))} /> {isRequestFlyoutOpen && ( @@ -121,4 +76,4 @@ export function Main({ http }: Props) { )}
); -} +}; 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 b1ac68e1f5719..f66d5948bef93 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { EuiFieldText, EuiFormRow, @@ -21,101 +21,27 @@ import { i18n } from '@kbn/i18n'; import { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; import { painlessContextOptions } from '../../common/constants'; +import { useAppContext } from '../../context'; -interface Props { - context: any; - index: string; - document: string; - query: string; - onContextChange: (context: string) => void; - onIndexChange: (index: string) => void; - onDocumentChange: (document: string) => void; - onQueryChange: (query: string) => void; -} +export const ContextTab: FunctionComponent = () => { + const { state, updateState } = useAppContext(); + const { context, document, index, query } = state; -export const ContextTab = ({ - context, - index, - document, - query, - onContextChange, - onIndexChange, - onDocumentChange, - onQueryChange, -}: Props) => ( - <> - - - - {' '} - - - - } - labelAppend={ - - - {i18n.translate('xpack.painlessLab.contextFieldDocLinkText', { - defaultMessage: 'Context docs', - })} - - - } - fullWidth - > - - - - {['filter', 'score'].indexOf(context) !== -1 && ( - - - {' '} - - - - } - fullWidth - > - onIndexChange(e.target.value)} /> - - )} - {/* Query DSL Code Editor */} - {'score'.indexOf(context) !== -1 && ( + return ( + <> + - {' '} + {' '} @@ -123,75 +49,143 @@ export const ContextTab = ({ labelAppend={ - {i18n.translate('xpack.painlessLab.queryFieldDocLinkText', { - defaultMessage: 'Query DSL docs', + {i18n.translate('xpack.painlessLab.contextFieldDocLinkText', { + defaultMessage: 'Context docs', })} } fullWidth > - - - + updateState(() => ({ context: nextContext }))} + itemLayoutAlign="top" + hasDividers + fullWidth + /> - )} - {['filter', 'score'].indexOf(context) !== -1 && ( - - - {' '} - - - - } - fullWidth - > - - + + {' '} + + + + } + fullWidth + > + { + const nextIndex = e.target.value; + updateState(() => ({ index: nextIndex })); }} /> - - - )} - -); + + )} + {/* Query DSL Code Editor */} + {'score'.indexOf(context) !== -1 && ( + + + {' '} + + + + } + labelAppend={ + + + {i18n.translate('xpack.painlessLab.queryFieldDocLinkText', { + defaultMessage: 'Query DSL docs', + })} + + + } + fullWidth + > + + updateState(() => ({ query: nextQuery }))} + options={{ + fontSize: 12, + minimap: { + enabled: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + automaticLayout: true, + }} + /> + + + )} + {['filter', 'score'].indexOf(context) !== -1 && ( + + + {' '} + + + + } + fullWidth + > + + updateState(() => ({ document: nextDocument }))} + options={{ + fontSize: 12, + minimap: { + enabled: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + automaticLayout: true, + }} + /> + + + )} + + ); +}; 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 3a9f604f9a23f..1e4bf5b5c88e0 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { EuiIcon, EuiFlexGroup, @@ -23,32 +23,9 @@ import { ContextTab } from './context_tab'; interface Props { isLoading: boolean; response?: Response; - context: string; - parameters: string; - index: string; - document: string; - query: string; - onContextChange: (change: string) => void; - onParametersChange: (change: string) => void; - onIndexChange: (change: string) => void; - onDocumentChange: (change: string) => void; - onQueryChange: (change: string) => void; } -export function OutputPane({ - isLoading, - response, - context, - parameters, - index, - document, - query, - onContextChange, - onParametersChange, - onIndexChange, - onDocumentChange, - onQueryChange, -}: Props) { +export const OutputPane: FunctionComponent = ({ isLoading, response }) => { const outputTabLabel = ( @@ -86,30 +63,17 @@ export function OutputPane({ name: i18n.translate('xpack.painlessLab.parametersTabLabel', { defaultMessage: 'Parameters', }), - content: ( - - ), + content: , }, { id: 'context', name: i18n.translate('xpack.painlessLab.contextTabLabel', { defaultMessage: 'Context', }), - content: ( - - ), + 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 4ed27bf47dc68..2e21e8250b998 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 @@ -3,7 +3,7 @@ * 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 React, { FunctionComponent } from 'react'; import { EuiFormRow, EuiPanel, @@ -18,12 +18,10 @@ import { monaco } from '@kbn/ui-shared-deps/monaco'; import { i18n } from '@kbn/i18n'; import { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; -interface Props { - parameters: string; - onParametersChange: (change: string) => void; -} +import { useAppContext } from '../../context'; -export function ParametersTab({ parameters, onParametersChange }: Props) { +export const ParametersTab: FunctionComponent = () => { + const { state, updateState } = useAppContext(); return ( <> @@ -64,8 +62,8 @@ export function ParametersTab({ parameters, onParametersChange }: Props) { updateState(() => ({ parameters: nextParams }))} options={{ fontSize: 12, minimap: { @@ -88,4 +86,4 @@ export function ParametersTab({ parameters, onParametersChange }: Props) { ); -} +}; diff --git a/x-pack/plugins/painless_lab/public/application/constants.ts b/x-pack/plugins/painless_lab/public/application/constants.ts new file mode 100644 index 0000000000000..df7c7f961ed7c --- /dev/null +++ b/x-pack/plugins/painless_lab/public/application/constants.ts @@ -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 const PAINLESS_LAB_KEY = 'painlessLabState'; diff --git a/x-pack/plugins/painless_lab/public/application/context.tsx b/x-pack/plugins/painless_lab/public/application/context.tsx new file mode 100644 index 0000000000000..f2cbadefd6a6b --- /dev/null +++ b/x-pack/plugins/painless_lab/public/application/context.tsx @@ -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, { createContext, ReactNode, useState, useContext } from 'react'; + +import { initialState, Store } from './store'; +import { PAINLESS_LAB_KEY } from './constants'; + +interface ContextValue { + state: Store; + updateState: (nextState: (s: Store) => Partial) => void; +} + +const AppContext = createContext(undefined as any); + +export const AppContextProvider = ({ children }: { children: ReactNode }) => { + const [state, setState] = useState(() => ({ + ...initialState, + ...JSON.parse(localStorage.getItem(PAINLESS_LAB_KEY) || '{}'), + })); + + const updateState = (getNextState: (s: Store) => Partial): void => { + const update = getNextState(state); + const nextState = { + ...state, + ...update, + }; + localStorage.setItem(PAINLESS_LAB_KEY, JSON.stringify(nextState)); + setState(() => nextState); + }; + + return {children}; +}; + +export const useAppContext = () => { + const ctx = useContext(AppContext); + if (!ctx) { + throw new Error('AppContext can only be used inside of AppContextProvider!'); + } + return ctx; +}; 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 a4be0886f08bc..ead5c2be34d99 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 @@ -9,8 +9,9 @@ import { HttpSetup } from 'kibana/public'; import { debounce } from 'lodash'; import { API_BASE_PATH } from '../../../common/constants'; -import { Response, RequestPayloadConfig, PayloadFormat } from '../common/types'; +import { Response, PayloadFormat } from '../common/types'; import { formatRequestPayload } from '../lib/format'; +import { Store } from '../store'; const DEBOUNCE_MS = 800; @@ -21,7 +22,7 @@ export const useSubmitCode = (http: HttpSetup) => { const submit = useCallback( debounce( - async (config: RequestPayloadConfig) => { + async (config: Store) => { setInProgress(true); // Prevent an older request that resolves after a more recent request from clobbering it. diff --git a/x-pack/plugins/painless_lab/public/application/index.tsx b/x-pack/plugins/painless_lab/public/application/index.tsx index d980af2779a03..48124e61a2eb1 100644 --- a/x-pack/plugins/painless_lab/public/application/index.tsx +++ b/x-pack/plugins/painless_lab/public/application/index.tsx @@ -7,9 +7,11 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { CoreSetup, CoreStart } from 'kibana/public'; -import { Main } from './components/main'; import { createKibanaReactContext } from '../../../../../src/plugins/kibana_react/public'; +import { AppContextProvider } from './context'; +import { Main } from './components/main'; + interface AppDependencies { http: CoreSetup['http']; I18nContext: CoreStart['i18n']['Context']; @@ -30,7 +32,9 @@ export function renderApp( render( -
+ +
+ , element diff --git a/x-pack/plugins/painless_lab/public/application/lib/format.ts b/x-pack/plugins/painless_lab/public/application/lib/format.ts index 5f98266ee9774..cf719a68380f0 100644 --- a/x-pack/plugins/painless_lab/public/application/lib/format.ts +++ b/x-pack/plugins/painless_lab/public/application/lib/format.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestPayloadConfig, Response, ExecutionError, PayloadFormat } from '../common/types'; +import { Response, ExecutionError, PayloadFormat } from '../common/types'; +import { Store } from '../store'; function prettifyPayload(payload = '', indentationLevel = 0) { const indentation = new Array(indentationLevel + 1).join(' '); @@ -16,7 +17,7 @@ function prettifyPayload(payload = '', indentationLevel = 0) { * e.g. 1.0, is preserved instead of being coerced to an integer, e.g. 1. */ export function formatRequestPayload( - { code, context, parameters, index, document, query }: RequestPayloadConfig, + { code, context, parameters, index, document, query }: Partial, format: PayloadFormat = PayloadFormat.UGLY ): string { const isAdvancedContext = context === 'filter' || context === 'score'; diff --git a/x-pack/plugins/painless_lab/public/application/store.ts b/x-pack/plugins/painless_lab/public/application/store.ts new file mode 100644 index 0000000000000..b385ac60bbbd1 --- /dev/null +++ b/x-pack/plugins/painless_lab/public/application/store.ts @@ -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 { exampleScript, painlessContextOptions } from './common/constants'; + +export interface Store { + context: string; + code: string; + parameters: string; + index: string; + document: string; + query: string; +} + +export const initialState = { + context: painlessContextOptions[0].value, + code: exampleScript, + parameters: '', + index: '', + document: '', + query: '', +}; From 470da3e2e96f18ea9b8137f9071871adc5e719d9 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 19 Mar 2020 10:05:43 +0100 Subject: [PATCH 15/23] Fix i18n in context_tab --- .../public/application/components/output_pane/context_tab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f66d5948bef93..c9804b0e863d4 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 @@ -108,7 +108,7 @@ export const ContextTab: FunctionComponent = () => { })} > - {' '} + {' '} From 4506714951b55e8642c86ca459c2b53f7d5b0a28 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 19 Mar 2020 12:41:57 +0100 Subject: [PATCH 16/23] i18n --- .../public/application/components/output_pane/context_tab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c9804b0e863d4..75afd91910e1a 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 @@ -102,7 +102,7 @@ export const ContextTab: FunctionComponent = () => { Date: Fri, 20 Mar 2020 10:17:06 +0100 Subject: [PATCH 17/23] [Painless] Language Service (#60612) * Added language service * Use the correct monaco instance and add wordwise operations * Remove plugin context initializer for now --- packages/kbn-ui-shared-deps/monaco.ts | 2 + .../public/application/register_painless.ts | 195 ------------------ x-pack/plugins/painless_lab/public/index.ts | 5 +- .../plugins/painless_lab/public/lib/index.ts | 7 + .../public/lib/monaco_painless_lang.ts | 174 ++++++++++++++++ x-pack/plugins/painless_lab/public/plugin.tsx | 12 +- .../painless_lab/public/services/index.ts | 7 + .../public/services/language_service.ts | 45 ++++ 8 files changed, 244 insertions(+), 203 deletions(-) delete mode 100644 x-pack/plugins/painless_lab/public/application/register_painless.ts create mode 100644 x-pack/plugins/painless_lab/public/lib/index.ts create mode 100644 x-pack/plugins/painless_lab/public/lib/monaco_painless_lang.ts create mode 100644 x-pack/plugins/painless_lab/public/services/index.ts create mode 100644 x-pack/plugins/painless_lab/public/services/language_service.ts diff --git a/packages/kbn-ui-shared-deps/monaco.ts b/packages/kbn-ui-shared-deps/monaco.ts index 570aca86c484c..42801c69a3e2c 100644 --- a/packages/kbn-ui-shared-deps/monaco.ts +++ b/packages/kbn-ui-shared-deps/monaco.ts @@ -25,6 +25,8 @@ import 'monaco-editor/esm/vs/base/worker/defaultWorkerFactory'; import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands.js'; import 'monaco-editor/esm/vs/editor/browser/widget/codeEditorWidget.js'; +import 'monaco-editor/esm/vs/editor/contrib/wordOperations/wordOperations.js'; // Needed for word-wise char navigation + import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController.js'; // Needed for suggestions import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature diff --git a/x-pack/plugins/painless_lab/public/application/register_painless.ts b/x-pack/plugins/painless_lab/public/application/register_painless.ts deleted file mode 100644 index a3952c1941c6e..0000000000000 --- a/x-pack/plugins/painless_lab/public/application/register_painless.ts +++ /dev/null @@ -1,195 +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 * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; -export const LANGUAGE_ID = 'painless'; - -/** - * Extends the default type for a Monarch language so we can use - * attribute references (like @keywords to reference the keywords list) - * in the defined tokenizer - */ -interface Language extends monaco.languages.IMonarchLanguage { - default: string; - brackets: any; - keywords: string[]; - symbols: RegExp; - escapes: RegExp; - digits: RegExp; - primitives: string[]; - octaldigits: RegExp; - binarydigits: RegExp; - constants: string[]; - operators: string[]; -} - -function getPainlessLanguage() { - return { - default: '', - // painless does not use < >, so we define our own - brackets: [ - ['{', '}', 'delimiter.curly'], - ['[', ']', 'delimiter.square'], - ['(', ')', 'delimiter.parenthesis'], - ], - keywords: [ - 'if', - 'in', - 'else', - 'while', - 'do', - 'for', - 'continue', - 'break', - 'return', - 'new', - 'try', - 'catch', - 'throw', - 'this', - 'instanceof', - ], - primitives: [ - 'void', - 'boolean', - 'byte', - 'short', - 'char', - 'int', - 'long', - 'float', - 'double', - 'def', - ], - constants: ['true', 'false'], - operators: [ - '=', - '>', - '<', - '!', - '~', - '?', - '?:', - '?.', - ':', - '==', - '===', - '<=', - '>=', - '!=', - '!==', - '&&', - '||', - '++', - '--', - '+', - '-', - '*', - '/', - '&', - '|', - '^', - '%', - '<<', - '>>', - '>>>', - '+=', - '-=', - '*=', - '/=', - '&=', - '|=', - '^=', - '%=', - '<<=', - '>>=', - '>>>=', - '->', - '::', - '=~', - '==~', - ], - symbols: /[=>, so we define our own + brackets: [ + ['{', '}', 'delimiter.curly'], + ['[', ']', 'delimiter.square'], + ['(', ')', 'delimiter.parenthesis'], + ], + keywords: [ + 'if', + 'in', + 'else', + 'while', + 'do', + 'for', + 'continue', + 'break', + 'return', + 'new', + 'try', + 'catch', + 'throw', + 'this', + 'instanceof', + ], + primitives: ['void', 'boolean', 'byte', 'short', 'char', 'int', 'long', 'float', 'double', 'def'], + constants: ['true', 'false'], + operators: [ + '=', + '>', + '<', + '!', + '~', + '?', + '?:', + '?.', + ':', + '==', + '===', + '<=', + '>=', + '!=', + '!==', + '&&', + '||', + '++', + '--', + '+', + '-', + '*', + '/', + '&', + '|', + '^', + '%', + '<<', + '>>', + '>>>', + '+=', + '-=', + '*=', + '/=', + '&=', + '|=', + '^=', + '%=', + '<<=', + '>>=', + '>>>=', + '->', + '::', + '=~', + '==~', + ], + symbols: /[=> { - constructor(ctx: PluginInitializerContext) {} + languageService = new LanguageService(); async setup( { http, getStartServices, uiSettings }: CoreSetup, @@ -71,7 +71,7 @@ export class PainlessLabUIPlugin implements Plugin { + const blob = new Blob([workerSrc], { type: 'application/javascript' }); + return new Worker(window.URL.createObjectURL(blob)); + }, + }; + } + } + + public stop() { + if (CAN_CREATE_WORKER) { + (window as any).MonacoEnvironment = this.originalMonacoEnvironment; + } + } +} From ec762ab5c801b9b2b5ea0082cc8e4a0b410892a9 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Fri, 20 Mar 2020 10:31:03 +0100 Subject: [PATCH 18/23] [Painless] Replace hard-coded links (#60603) * Replace hard-coded links Also remove all props from Main component * Pass the new links object to the request flyout too * Link directly to painless execute API's contexts --- .../public/application/components/main.tsx | 13 ++++------ .../application/components/main_controls.tsx | 11 +++++--- .../components/output_pane/context_tab.tsx | 12 +++------ .../components/output_pane/parameters_tab.tsx | 7 ++--- .../application/components/request_flyout.tsx | 23 +++++++++------- .../public/application/context.tsx | 26 +++++++++++++++++-- .../painless_lab/public/application/index.tsx | 9 ++++--- x-pack/plugins/painless_lab/public/links.ts | 20 ++++++++++++++ x-pack/plugins/painless_lab/public/plugin.tsx | 5 +++- 9 files changed, 85 insertions(+), 41 deletions(-) create mode 100644 x-pack/plugins/painless_lab/public/links.ts diff --git a/x-pack/plugins/painless_lab/public/application/components/main.tsx b/x-pack/plugins/painless_lab/public/application/components/main.tsx index 4df3a484bddce..faeeb9b9f86c9 100644 --- a/x-pack/plugins/painless_lab/public/application/components/main.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/main.tsx @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HttpSetup } from 'kibana/public'; import React, { useState, useEffect, FunctionComponent } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -18,15 +17,11 @@ import { Editor } from './editor'; import { RequestFlyout } from './request_flyout'; import { useAppContext } from '../context'; -interface Props { - http: HttpSetup; -} - -export const Main: FunctionComponent = ({ http }) => { - const { state, updateState } = useAppContext(); +export const Main: FunctionComponent = () => { + const { state, updateState, services, links } = useAppContext(); const [isRequestFlyoutOpen, setRequestFlyoutOpen] = useState(false); - const { inProgress, response, submit } = useSubmitCode(http); + const { inProgress, response, submit } = useSubmitCode(services.http); // Live-update the output and persist state as the user changes it. useEffect(() => { @@ -61,6 +56,7 @@ export const Main: FunctionComponent = ({ http }) => { = ({ http }) => { {isRequestFlyoutOpen && ( setRequestFlyoutOpen(false)} requestBody={formatRequestPayload(state, PayloadFormat.PRETTY)} response={response ? formatJson(response.result || response.error) : ''} diff --git a/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx b/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx index cbdf92d01b7de..44bbe89665756 100644 --- a/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx @@ -15,21 +15,24 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { Links } from '../../links'; + interface Props { toggleRequestFlyout: () => void; isRequestFlyoutOpen: boolean; isLoading: boolean; reset: () => void; + links: Links; } -export function MainControls({ toggleRequestFlyout, isRequestFlyoutOpen, reset }: Props) { +export function MainControls({ toggleRequestFlyout, isRequestFlyoutOpen, reset, links }: Props) { const [isHelpOpen, setIsHelpOpen] = useState(false); const items = [ setIsHelpOpen(false)} > @@ -41,7 +44,7 @@ export function MainControls({ toggleRequestFlyout, isRequestFlyoutOpen, reset } setIsHelpOpen(false)} > @@ -53,7 +56,7 @@ export function MainControls({ toggleRequestFlyout, isRequestFlyoutOpen, reset } setIsHelpOpen(false)} > 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 75afd91910e1a..77732ea1ca6ce 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 @@ -24,7 +24,7 @@ import { painlessContextOptions } from '../../common/constants'; import { useAppContext } from '../../context'; export const ContextTab: FunctionComponent = () => { - const { state, updateState } = useAppContext(); + const { state, updateState, links } = useAppContext(); const { context, document, index, query } = state; return ( @@ -48,10 +48,7 @@ export const ContextTab: FunctionComponent = () => { } labelAppend={ - + {i18n.translate('xpack.painlessLab.contextFieldDocLinkText', { defaultMessage: 'Context docs', })} @@ -115,10 +112,7 @@ export const ContextTab: FunctionComponent = () => { } labelAppend={ - + {i18n.translate('xpack.painlessLab.queryFieldDocLinkText', { defaultMessage: 'Query DSL docs', })} 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 2e21e8250b998..cc34b7a61735e 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 @@ -21,7 +21,7 @@ import { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public import { useAppContext } from '../../context'; export const ParametersTab: FunctionComponent = () => { - const { state, updateState } = useAppContext(); + const { state, updateState, links } = useAppContext(); return ( <> @@ -44,10 +44,7 @@ export const ParametersTab: FunctionComponent = () => { fullWidth labelAppend={ - + {i18n.translate('xpack.painlessLab.parametersFieldDocLinkText', { defaultMessage: 'Parameters docs', })} diff --git a/x-pack/plugins/painless_lab/public/application/components/request_flyout.tsx b/x-pack/plugins/painless_lab/public/application/components/request_flyout.tsx index 2d0262503b162..34918a48cfc4e 100644 --- a/x-pack/plugins/painless_lab/public/application/components/request_flyout.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/request_flyout.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { EuiCodeBlock, EuiTabbedContent, @@ -17,16 +17,21 @@ import { EuiFlexItem, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { Links } from '../../links'; -export function RequestFlyout({ - onClose, - requestBody, - response, -}: { +interface Props { onClose: any; requestBody: string; + links: Links; response?: string; -}) { +} + +export const RequestFlyout: FunctionComponent = ({ + onClose, + requestBody, + response, + links, +}) => { return ( @@ -48,7 +53,7 @@ export function RequestFlyout({ @@ -90,4 +95,4 @@ export function RequestFlyout({ ); -} +}; diff --git a/x-pack/plugins/painless_lab/public/application/context.tsx b/x-pack/plugins/painless_lab/public/application/context.tsx index f2cbadefd6a6b..e61b331c833a3 100644 --- a/x-pack/plugins/painless_lab/public/application/context.tsx +++ b/x-pack/plugins/painless_lab/public/application/context.tsx @@ -5,6 +5,9 @@ */ import React, { createContext, ReactNode, useState, useContext } from 'react'; +import { HttpSetup } from 'src/core/public'; + +import { Links } from '../links'; import { initialState, Store } from './store'; import { PAINLESS_LAB_KEY } from './constants'; @@ -12,11 +15,26 @@ import { PAINLESS_LAB_KEY } from './constants'; interface ContextValue { state: Store; updateState: (nextState: (s: Store) => Partial) => void; + services: { + http: HttpSetup; + }; + links: Links; } const AppContext = createContext(undefined as any); -export const AppContextProvider = ({ children }: { children: ReactNode }) => { +interface AppContextProviderArgs { + children: ReactNode; + value: { + http: HttpSetup; + links: Links; + }; +} + +export const AppContextProvider = ({ + children, + value: { http, links }, +}: AppContextProviderArgs) => { const [state, setState] = useState(() => ({ ...initialState, ...JSON.parse(localStorage.getItem(PAINLESS_LAB_KEY) || '{}'), @@ -32,7 +50,11 @@ export const AppContextProvider = ({ children }: { children: ReactNode }) => { setState(() => nextState); }; - return {children}; + return ( + + {children} + + ); }; export const useAppContext = () => { diff --git a/x-pack/plugins/painless_lab/public/application/index.tsx b/x-pack/plugins/painless_lab/public/application/index.tsx index 48124e61a2eb1..8057988d1488e 100644 --- a/x-pack/plugins/painless_lab/public/application/index.tsx +++ b/x-pack/plugins/painless_lab/public/application/index.tsx @@ -9,6 +9,8 @@ import { render, unmountComponentAtNode } from 'react-dom'; import { CoreSetup, CoreStart } from 'kibana/public'; import { createKibanaReactContext } from '../../../../../src/plugins/kibana_react/public'; +import { Links } from '../links'; + import { AppContextProvider } from './context'; import { Main } from './components/main'; @@ -16,11 +18,12 @@ interface AppDependencies { http: CoreSetup['http']; I18nContext: CoreStart['i18n']['Context']; uiSettings: CoreSetup['uiSettings']; + links: Links; } export function renderApp( element: HTMLElement | null, - { http, I18nContext, uiSettings }: AppDependencies + { http, I18nContext, uiSettings, links }: AppDependencies ) { if (!element) { return () => undefined; @@ -32,8 +35,8 @@ export function renderApp( render( - -
+ +
, diff --git a/x-pack/plugins/painless_lab/public/links.ts b/x-pack/plugins/painless_lab/public/links.ts new file mode 100644 index 0000000000000..8f610140c3f34 --- /dev/null +++ b/x-pack/plugins/painless_lab/public/links.ts @@ -0,0 +1,20 @@ +/* + * 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 { DocLinksStart } from 'src/core/public'; + +export type Links = ReturnType; + +export const getLinks = ({ DOC_LINK_VERSION, ELASTIC_WEBSITE_URL }: DocLinksStart) => + Object.freeze({ + painlessExecuteAPI: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html`, + painlessExecuteAPIContexts: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-execute-api.html#_contexts`, + painlessAPIReference: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-api-reference.html`, + painlessWalkthrough: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-walkthrough.html`, + painlessLangSpec: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/painless/${DOC_LINK_VERSION}/painless-lang-spec.html`, + esQueryDSL: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/query-dsl.html`, + modulesScriptingPreferParams: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/modules-scripting-using.html#prefer-params`, + }); diff --git a/x-pack/plugins/painless_lab/public/plugin.tsx b/x-pack/plugins/painless_lab/public/plugin.tsx index f24e0f9c472d7..7f5d753b0041b 100644 --- a/x-pack/plugins/painless_lab/public/plugin.tsx +++ b/x-pack/plugins/painless_lab/public/plugin.tsx @@ -14,7 +14,9 @@ import { FeatureCatalogueCategory } from '../../../../src/plugins/home/public'; import { LICENSE_CHECK_STATE } from '../../licensing/public'; import { PLUGIN } from '../common/constants'; + import { PluginDependencies } from './types'; +import { getLinks } from './links'; import { LanguageService } from './services'; export class PainlessLabUIPlugin implements Plugin { @@ -69,6 +71,7 @@ export class PainlessLabUIPlugin implements Plugin Date: Fri, 20 Mar 2020 16:15:03 -0700 Subject: [PATCH 19/23] Remove responsive stacking from tabs with icons in them. --- .../public/application/components/output_pane/output_pane.tsx | 2 +- x-pack/plugins/painless_lab/public/plugin.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 1e4bf5b5c88e0..156363a1c89a8 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 @@ -27,7 +27,7 @@ interface Props { export const OutputPane: FunctionComponent = ({ isLoading, response }) => { const outputTabLabel = ( - + {isLoading ? ( diff --git a/x-pack/plugins/painless_lab/public/plugin.tsx b/x-pack/plugins/painless_lab/public/plugin.tsx index 7f5d753b0041b..97aec877d0bc0 100644 --- a/x-pack/plugins/painless_lab/public/plugin.tsx +++ b/x-pack/plugins/painless_lab/public/plugin.tsx @@ -44,7 +44,7 @@ export class PainlessLabUIPlugin implements Plugin + {i18n.translate('xpack.painlessLab.displayName', { defaultMessage: 'Painless Lab', From 093f5244b9ff9fb370b6ea43e6e5819647244a60 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Mon, 23 Mar 2020 10:30:09 -0700 Subject: [PATCH 20/23] Resize Painless Lab bottom bar to accommodate nav drawer width (#60833) --- src/plugins/dev_tools/public/plugin.ts | 2 ++ .../public/application/components/main.tsx | 27 ++++++++++++--- .../application/components/main_controls.tsx | 16 +++++++-- .../public/application/context.tsx | 4 +-- .../public/application/index.scss | 33 ------------------- .../painless_lab/public/application/index.tsx | 6 ++-- x-pack/plugins/painless_lab/public/plugin.tsx | 13 +++++++- .../painless_lab/public/styles/_index.scss | 13 ++++++++ 8 files changed, 69 insertions(+), 45 deletions(-) delete mode 100644 x-pack/plugins/painless_lab/public/application/index.scss diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index 9ebfeb5387b26..df61271baf879 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -132,4 +132,6 @@ export class DevToolsPlugin implements Plugin { getSortedDevTools: this.getSortedDevTools.bind(this), }; } + + public stop() {} } diff --git a/x-pack/plugins/painless_lab/public/application/components/main.tsx b/x-pack/plugins/painless_lab/public/application/components/main.tsx index faeeb9b9f86c9..d692eab27ff42 100644 --- a/x-pack/plugins/painless_lab/public/application/components/main.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/main.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useState, useEffect, FunctionComponent } from 'react'; +import React, { useState, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { formatRequestPayload, formatJson } from '../lib/format'; @@ -17,11 +17,19 @@ import { Editor } from './editor'; import { RequestFlyout } from './request_flyout'; import { useAppContext } from '../context'; -export const Main: FunctionComponent = () => { - const { state, updateState, services, links } = useAppContext(); +export const Main = () => { + const { + state, + updateState, + services: { + http, + chrome: { getIsNavDrawerLocked$ }, + }, + links, + } = useAppContext(); const [isRequestFlyoutOpen, setRequestFlyoutOpen] = useState(false); - const { inProgress, response, submit } = useSubmitCode(services.http); + const { inProgress, response, submit } = useSubmitCode(http); // Live-update the output and persist state as the user changes it. useEffect(() => { @@ -32,6 +40,16 @@ export const Main: FunctionComponent = () => { setRequestFlyoutOpen(!isRequestFlyoutOpen); }; + const [isNavDrawerLocked, setIsNavDrawerLocked] = useState(false); + + useEffect(() => { + const subscription = getIsNavDrawerLocked$().subscribe((newIsNavDrawerLocked: boolean) => { + setIsNavDrawerLocked(newIsNavDrawerLocked); + }); + + return () => subscription.unsubscribe(); + }); + return (
@@ -61,6 +79,7 @@ export const Main: FunctionComponent = () => { toggleRequestFlyout={toggleRequestFlyout} isRequestFlyoutOpen={isRequestFlyoutOpen} reset={() => updateState(() => ({ code: exampleScript }))} + isNavDrawerLocked={isNavDrawerLocked} /> {isRequestFlyoutOpen && ( diff --git a/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx b/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx index 44bbe89665756..6307c21e26dc4 100644 --- a/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/main_controls.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import React, { useState } from 'react'; +import classNames from 'classnames'; import { EuiPopover, EuiBottomBar, @@ -23,9 +24,16 @@ interface Props { isLoading: boolean; reset: () => void; links: Links; + isNavDrawerLocked: boolean; } -export function MainControls({ toggleRequestFlyout, isRequestFlyoutOpen, reset, links }: Props) { +export function MainControls({ + toggleRequestFlyout, + isRequestFlyoutOpen, + reset, + links, + isNavDrawerLocked, +}: Props) { const [isHelpOpen, setIsHelpOpen] = useState(false); const items = [ @@ -79,8 +87,12 @@ export function MainControls({ toggleRequestFlyout, isRequestFlyoutOpen, reset, , ]; + const classes = classNames('painlessLab__bottomBar', { + 'painlessLab__bottomBar-isNavDrawerLocked': isNavDrawerLocked, + }); + return ( - + diff --git a/x-pack/plugins/painless_lab/public/application/context.tsx b/x-pack/plugins/painless_lab/public/application/context.tsx index e61b331c833a3..808c55f63b0de 100644 --- a/x-pack/plugins/painless_lab/public/application/context.tsx +++ b/x-pack/plugins/painless_lab/public/application/context.tsx @@ -33,7 +33,7 @@ interface AppContextProviderArgs { export const AppContextProvider = ({ children, - value: { http, links }, + value: { http, links, chrome }, }: AppContextProviderArgs) => { const [state, setState] = useState(() => ({ ...initialState, @@ -51,7 +51,7 @@ export const AppContextProvider = ({ }; return ( - + {children} ); diff --git a/x-pack/plugins/painless_lab/public/application/index.scss b/x-pack/plugins/painless_lab/public/application/index.scss deleted file mode 100644 index 69c1f8c217655..0000000000000 --- a/x-pack/plugins/painless_lab/public/application/index.scss +++ /dev/null @@ -1,33 +0,0 @@ -// Import the EUI global scope so we can use EUI constants -@import 'src/legacy/ui/public/styles/_styling_constants'; - -/** - * 1. This is a very brittle way of preventing the editor and other content from disappearing - * behind the bottom bar. - */ -.painlessLabBottomBarPlaceholder { - height: $euiSize * 3; /* [1] */ -} - -.painlessLabRightPane { - border-right: none; - border-top: none; - border-bottom: none; - border-radius: 0; - padding-top: 0; - height: 100%; -} - -.painlessLabRightPane__tabs { - display: flex; - flex-direction: column; - height: 100%; - - [role="tabpanel"] { - height: 100%; - } -} - -.painlessLab__betaLabelContainer { - line-height: 0; -} diff --git a/x-pack/plugins/painless_lab/public/application/index.tsx b/x-pack/plugins/painless_lab/public/application/index.tsx index 8057988d1488e..f0a0280d12457 100644 --- a/x-pack/plugins/painless_lab/public/application/index.tsx +++ b/x-pack/plugins/painless_lab/public/application/index.tsx @@ -19,23 +19,23 @@ interface AppDependencies { I18nContext: CoreStart['i18n']['Context']; uiSettings: CoreSetup['uiSettings']; links: Links; + chrome: CoreSetup['chrome']; } export function renderApp( element: HTMLElement | null, - { http, I18nContext, uiSettings, links }: AppDependencies + { http, I18nContext, uiSettings, links, chrome }: AppDependencies ) { if (!element) { return () => undefined; } - const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ uiSettings, }); render( - +
diff --git a/x-pack/plugins/painless_lab/public/plugin.tsx b/x-pack/plugins/painless_lab/public/plugin.tsx index 97aec877d0bc0..b9ca7031cf670 100644 --- a/x-pack/plugins/painless_lab/public/plugin.tsx +++ b/x-pack/plugins/painless_lab/public/plugin.tsx @@ -72,6 +72,7 @@ export class PainlessLabUIPlugin implements Plugin { + tearDownApp(); + }; }, }); } diff --git a/x-pack/plugins/painless_lab/public/styles/_index.scss b/x-pack/plugins/painless_lab/public/styles/_index.scss index 733c5a4b56aef..ed9f7d5a8d248 100644 --- a/x-pack/plugins/painless_lab/public/styles/_index.scss +++ b/x-pack/plugins/painless_lab/public/styles/_index.scss @@ -1,4 +1,5 @@ @import '@elastic/eui/src/components/header/variables'; +@import '@elastic/eui/src/components/nav_drawer/variables'; /** * This is a very brittle way of preventing the editor and other content from disappearing @@ -42,3 +43,15 @@ $bottomBarHeight: calc(#{$euiSize} * 3); // The panels container should adopt the height of the main container height: 100%; } + +/** + * 1. Hack EUI so the bottom bar doesn't obscure the nav drawer flyout. + */ +.painlessLab__bottomBar { + z-index: 0; /* 1 */ + left: $euiNavDrawerWidthCollapsed; +} + +.painlessLab__bottomBar-isNavDrawerLocked { + left: $euiNavDrawerWidthExpanded; +} From d0d303676125d5f7200acf1604ee0ccc4b3ed68e Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Mon, 23 Mar 2020 13:24:34 -0700 Subject: [PATCH 21/23] Validate Painless Lab index field (#60841) * Make JSON format of parameters field more prominent. Set default parameters to provide an example to users. * Set default document to provide an example to users. * Simplify context's updateState interface. * Refactor store and context file organization. - Remove common directory, move constants and types files to root. - Move initialState into context file, where it's being used. * Add validation for index input. * Create context directory. --- .../public/application/components/main.tsx | 29 +++--- .../components/output_pane/context_tab.tsx | 39 +++++--- .../components/output_pane/output_pane.tsx | 2 +- .../components/output_pane/output_tab.tsx | 2 +- .../components/output_pane/parameters_tab.tsx | 15 +-- .../application/{common => }/constants.tsx | 0 .../public/application/context.tsx | 66 ------------- .../public/application/context/context.tsx | 95 +++++++++++++++++++ .../{constants.ts => context/index.tsx} | 2 +- .../{store.ts => context/initial_payload.ts} | 24 +++-- .../application/hooks/use_submit_code.ts | 5 +- .../painless_lab/public/application/index.tsx | 6 +- .../public/application/lib/format.test.ts | 2 +- .../public/application/lib/format.ts | 5 +- .../public/application/{common => }/types.ts | 23 ++++- .../painless_lab/server/routes/api/execute.ts | 2 +- 16 files changed, 190 insertions(+), 127 deletions(-) rename x-pack/plugins/painless_lab/public/application/{common => }/constants.tsx (100%) delete mode 100644 x-pack/plugins/painless_lab/public/application/context.tsx create mode 100644 x-pack/plugins/painless_lab/public/application/context/context.tsx rename x-pack/plugins/painless_lab/public/application/{constants.ts => context/index.tsx} (79%) rename x-pack/plugins/painless_lab/public/application/{store.ts => context/initial_payload.ts} (52%) rename x-pack/plugins/painless_lab/public/application/{common => }/types.ts (69%) diff --git a/x-pack/plugins/painless_lab/public/application/components/main.tsx b/x-pack/plugins/painless_lab/public/application/components/main.tsx index d692eab27ff42..10907536e9cc2 100644 --- a/x-pack/plugins/painless_lab/public/application/components/main.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/main.tsx @@ -8,19 +8,19 @@ import React, { useState, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { formatRequestPayload, formatJson } from '../lib/format'; -import { exampleScript } from '../common/constants'; -import { PayloadFormat } from '../common/types'; +import { exampleScript } from '../constants'; +import { PayloadFormat } from '../types'; import { useSubmitCode } from '../hooks'; +import { useAppContext } from '../context'; import { OutputPane } from './output_pane'; import { MainControls } from './main_controls'; import { Editor } from './editor'; import { RequestFlyout } from './request_flyout'; -import { useAppContext } from '../context'; -export const Main = () => { +export const Main: React.FunctionComponent = () => { const { - state, - updateState, + store: { payload, validation }, + updatePayload, services: { http, chrome: { getIsNavDrawerLocked$ }, @@ -31,10 +31,12 @@ export const Main = () => { const [isRequestFlyoutOpen, setRequestFlyoutOpen] = useState(false); const { inProgress, response, submit } = useSubmitCode(http); - // Live-update the output and persist state as the user changes it. + // Live-update the output and persist payload state as the user changes it. useEffect(() => { - submit(state); - }, [state, submit]); + if (validation.isValid) { + submit(payload); + } + }, [payload, submit, validation.isValid]); const toggleRequestFlyout = () => { setRequestFlyoutOpen(!isRequestFlyoutOpen); @@ -62,10 +64,7 @@ export const Main = () => { - updateState(() => ({ code: nextCode }))} - /> + updatePayload({ code: nextCode })} /> @@ -78,15 +77,15 @@ export const Main = () => { isLoading={inProgress} toggleRequestFlyout={toggleRequestFlyout} isRequestFlyoutOpen={isRequestFlyoutOpen} - reset={() => updateState(() => ({ code: exampleScript }))} isNavDrawerLocked={isNavDrawerLocked} + reset={() => updatePayload({ code: exampleScript })} /> {isRequestFlyoutOpen && ( setRequestFlyoutOpen(false)} - requestBody={formatRequestPayload(state, PayloadFormat.PRETTY)} + requestBody={formatRequestPayload(payload, 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 77732ea1ca6ce..47efd524f092a 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 @@ -20,12 +20,16 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public'; -import { painlessContextOptions } from '../../common/constants'; +import { painlessContextOptions } from '../../constants'; import { useAppContext } from '../../context'; export const ContextTab: FunctionComponent = () => { - const { state, updateState, links } = useAppContext(); - const { context, document, index, query } = state; + const { + store: { payload, validation }, + updatePayload, + links, + } = useAppContext(); + const { context, document, index, query } = payload; return ( <> @@ -60,7 +64,7 @@ export const ContextTab: FunctionComponent = () => { updateState(() => ({ context: nextContext }))} + onChange={nextContext => updatePayload({ context: nextContext })} itemLayoutAlign="top" hasDividers fullWidth @@ -72,25 +76,38 @@ export const ContextTab: FunctionComponent = () => { label={ - {' '} + {' '} } fullWidth + isInvalid={!validation.fields.index} + error={ + validation.fields.index + ? [] + : [ + i18n.translate('xpack.painlessLab.indexFieldMissingErrorMessage', { + defaultMessage: 'Enter an index name', + }), + ] + } > { const nextIndex = e.target.value; - updateState(() => ({ index: nextIndex })); + updatePayload({ index: nextIndex }); }} + isInvalid={!validation.fields.index} /> )} @@ -126,7 +143,7 @@ export const ContextTab: FunctionComponent = () => { languageId="json" height={150} value={query} - onChange={nextQuery => updateState(() => ({ query: nextQuery }))} + onChange={nextQuery => updatePayload({ query: nextQuery })} options={{ fontSize: 12, minimap: { @@ -152,7 +169,7 @@ export const ContextTab: FunctionComponent = () => { {' '} @@ -165,7 +182,7 @@ export const ContextTab: FunctionComponent = () => { languageId="json" height={400} value={document} - onChange={nextDocument => updateState(() => ({ document: nextDocument }))} + onChange={nextDocument => updatePayload({ document: nextDocument })} 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 156363a1c89a8..e6a97bb02f738 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,7 +15,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Response } from '../../common/types'; +import { Response } from '../../types'; import { OutputTab } from './output_tab'; import { ParametersTab } from './parameters_tab'; import { ContextTab } from './context_tab'; diff --git a/x-pack/plugins/painless_lab/public/application/components/output_pane/output_tab.tsx b/x-pack/plugins/painless_lab/public/application/components/output_pane/output_tab.tsx index fafd0f1f7cde4..8969e5421640a 100644 --- a/x-pack/plugins/painless_lab/public/application/components/output_pane/output_tab.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/output_pane/output_tab.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiCodeBlock, EuiSpacer } from '@elastic/eui'; import { formatResponse } from '../../lib/format'; -import { Response } from '../../common/types'; +import { Response } from '../../types'; interface Props { response?: Response; 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 cc34b7a61735e..7c8bce0f7b21b 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 @@ -21,7 +21,11 @@ import { CodeEditor } from '../../../../../../../src/plugins/kibana_react/public import { useAppContext } from '../../context'; export const ParametersTab: FunctionComponent = () => { - const { state, updateState, links } = useAppContext(); + const { + store: { payload }, + updatePayload, + links, + } = useAppContext(); return ( <> @@ -35,7 +39,7 @@ export const ParametersTab: FunctionComponent = () => { {' '} @@ -51,16 +55,13 @@ export const ParametersTab: FunctionComponent = () => { } - helpText={i18n.translate('xpack.painlessLab.helpIconAriaLabel', { - defaultMessage: 'Use JSON format', - })} > updateState(() => ({ parameters: nextParams }))} + value={payload.parameters} + onChange={nextParams => updatePayload({ parameters: nextParams })} options={{ fontSize: 12, minimap: { diff --git a/x-pack/plugins/painless_lab/public/application/common/constants.tsx b/x-pack/plugins/painless_lab/public/application/constants.tsx similarity index 100% rename from x-pack/plugins/painless_lab/public/application/common/constants.tsx rename to x-pack/plugins/painless_lab/public/application/constants.tsx diff --git a/x-pack/plugins/painless_lab/public/application/context.tsx b/x-pack/plugins/painless_lab/public/application/context.tsx deleted file mode 100644 index 808c55f63b0de..0000000000000 --- a/x-pack/plugins/painless_lab/public/application/context.tsx +++ /dev/null @@ -1,66 +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 React, { createContext, ReactNode, useState, useContext } from 'react'; -import { HttpSetup } from 'src/core/public'; - -import { Links } from '../links'; - -import { initialState, Store } from './store'; -import { PAINLESS_LAB_KEY } from './constants'; - -interface ContextValue { - state: Store; - updateState: (nextState: (s: Store) => Partial) => void; - services: { - http: HttpSetup; - }; - links: Links; -} - -const AppContext = createContext(undefined as any); - -interface AppContextProviderArgs { - children: ReactNode; - value: { - http: HttpSetup; - links: Links; - }; -} - -export const AppContextProvider = ({ - children, - value: { http, links, chrome }, -}: AppContextProviderArgs) => { - const [state, setState] = useState(() => ({ - ...initialState, - ...JSON.parse(localStorage.getItem(PAINLESS_LAB_KEY) || '{}'), - })); - - const updateState = (getNextState: (s: Store) => Partial): void => { - const update = getNextState(state); - const nextState = { - ...state, - ...update, - }; - localStorage.setItem(PAINLESS_LAB_KEY, JSON.stringify(nextState)); - setState(() => nextState); - }; - - return ( - - {children} - - ); -}; - -export const useAppContext = () => { - const ctx = useContext(AppContext); - if (!ctx) { - throw new Error('AppContext can only be used inside of AppContextProvider!'); - } - return ctx; -}; diff --git a/x-pack/plugins/painless_lab/public/application/context/context.tsx b/x-pack/plugins/painless_lab/public/application/context/context.tsx new file mode 100644 index 0000000000000..0fb5842dfea58 --- /dev/null +++ b/x-pack/plugins/painless_lab/public/application/context/context.tsx @@ -0,0 +1,95 @@ +/* + * 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, { createContext, ReactNode, useState, useContext } from 'react'; +import { HttpSetup, ChromeStart } from 'src/core/public'; + +import { Links } from '../../links'; +import { Store, Payload, Validation } from '../types'; +import { initialPayload } from './initial_payload'; + +interface AppContextProviderArgs { + children: ReactNode; + value: { + http: HttpSetup; + links: Links; + chrome: ChromeStart; + }; +} + +interface ContextValue { + store: Store; + updatePayload: (changes: Partial) => void; + services: { + http: HttpSetup; + chrome: ChromeStart; + }; + links: Links; +} + +const AppContext = createContext(undefined as any); + +const validatePayload = (payload: Payload): Validation => { + const { index } = payload; + + // For now just validate that the user has entered an index. + const indexExists = Boolean(index || index.trim()); + + return { + isValid: indexExists, + fields: { + index: indexExists, + }, + }; +}; + +export const AppContextProvider = ({ + children, + value: { http, links, chrome }, +}: AppContextProviderArgs) => { + const PAINLESS_LAB_KEY = 'painlessLabState'; + + const [store, setStore] = useState(() => { + // Using a callback here ensures these values are only calculated on the first render. + const defaultPayload = { + ...initialPayload, + ...JSON.parse(localStorage.getItem(PAINLESS_LAB_KEY) || '{}'), + }; + + return { + payload: defaultPayload, + validation: validatePayload(defaultPayload), + }; + }); + + const updatePayload = (changes: Partial): void => { + const nextPayload = { + ...store.payload, + ...changes, + }; + // Persist state locally so we can load it up when the user reopens the app. + localStorage.setItem(PAINLESS_LAB_KEY, JSON.stringify(nextPayload)); + + setStore({ + payload: nextPayload, + validation: validatePayload(nextPayload), + }); + }; + + return ( + + {children} + + ); +}; + +export const useAppContext = () => { + const ctx = useContext(AppContext); + if (!ctx) { + throw new Error('AppContext can only be used inside of AppContextProvider!'); + } + return ctx; +}; diff --git a/x-pack/plugins/painless_lab/public/application/constants.ts b/x-pack/plugins/painless_lab/public/application/context/index.tsx similarity index 79% rename from x-pack/plugins/painless_lab/public/application/constants.ts rename to x-pack/plugins/painless_lab/public/application/context/index.tsx index df7c7f961ed7c..7a685137b7a4f 100644 --- a/x-pack/plugins/painless_lab/public/application/constants.ts +++ b/x-pack/plugins/painless_lab/public/application/context/index.tsx @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export const PAINLESS_LAB_KEY = 'painlessLabState'; +export { AppContextProvider, useAppContext } from './context'; diff --git a/x-pack/plugins/painless_lab/public/application/store.ts b/x-pack/plugins/painless_lab/public/application/context/initial_payload.ts similarity index 52% rename from x-pack/plugins/painless_lab/public/application/store.ts rename to x-pack/plugins/painless_lab/public/application/context/initial_payload.ts index b385ac60bbbd1..4d9d8ad8b3ae7 100644 --- a/x-pack/plugins/painless_lab/public/application/store.ts +++ b/x-pack/plugins/painless_lab/public/application/context/initial_payload.ts @@ -3,22 +3,20 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { exampleScript, painlessContextOptions } from './common/constants'; -export interface Store { - context: string; - code: string; - parameters: string; - index: string; - document: string; - query: string; -} +import { exampleScript, painlessContextOptions } from '../constants'; -export const initialState = { +export const initialPayload = { context: painlessContextOptions[0].value, code: exampleScript, - parameters: '', - index: '', - document: '', + parameters: `{ + "string_parameter": "string value", + "number_parameter": 1.5, + "boolean_parameter": true +}`, + index: 'my-index', + document: `{ + "my_field": "field_value" +}`, query: '', }; 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 ead5c2be34d99..36cd4f280ac4c 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 @@ -9,9 +9,8 @@ import { HttpSetup } from 'kibana/public'; import { debounce } from 'lodash'; import { API_BASE_PATH } from '../../../common/constants'; -import { Response, PayloadFormat } from '../common/types'; +import { Response, PayloadFormat, Payload } from '../types'; import { formatRequestPayload } from '../lib/format'; -import { Store } from '../store'; const DEBOUNCE_MS = 800; @@ -22,7 +21,7 @@ export const useSubmitCode = (http: HttpSetup) => { const submit = useCallback( debounce( - async (config: Store) => { + async (config: Payload) => { setInProgress(true); // Prevent an older request that resolves after a more recent request from clobbering it. diff --git a/x-pack/plugins/painless_lab/public/application/index.tsx b/x-pack/plugins/painless_lab/public/application/index.tsx index f0a0280d12457..ebcb84bbce83c 100644 --- a/x-pack/plugins/painless_lab/public/application/index.tsx +++ b/x-pack/plugins/painless_lab/public/application/index.tsx @@ -7,19 +7,19 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { CoreSetup, CoreStart } from 'kibana/public'; +import { HttpSetup, ChromeStart } from 'src/core/public'; import { createKibanaReactContext } from '../../../../../src/plugins/kibana_react/public'; import { Links } from '../links'; - import { AppContextProvider } from './context'; import { Main } from './components/main'; interface AppDependencies { - http: CoreSetup['http']; + http: HttpSetup; I18nContext: CoreStart['i18n']['Context']; uiSettings: CoreSetup['uiSettings']; links: Links; - chrome: CoreSetup['chrome']; + chrome: ChromeStart; } export function renderApp( diff --git a/x-pack/plugins/painless_lab/public/application/lib/format.test.ts b/x-pack/plugins/painless_lab/public/application/lib/format.test.ts index 1f46d6e665bcc..5f0022ebbc089 100644 --- a/x-pack/plugins/painless_lab/public/application/lib/format.test.ts +++ b/x-pack/plugins/painless_lab/public/application/lib/format.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { PayloadFormat } from '../common/types'; +import { PayloadFormat } from '../types'; import { formatRequestPayload } from './format'; describe('formatRequestPayload', () => { diff --git a/x-pack/plugins/painless_lab/public/application/lib/format.ts b/x-pack/plugins/painless_lab/public/application/lib/format.ts index cf719a68380f0..15ecdf682d247 100644 --- a/x-pack/plugins/painless_lab/public/application/lib/format.ts +++ b/x-pack/plugins/painless_lab/public/application/lib/format.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Response, ExecutionError, PayloadFormat } from '../common/types'; -import { Store } from '../store'; +import { Response, ExecutionError, PayloadFormat, Payload } from '../types'; function prettifyPayload(payload = '', indentationLevel = 0) { const indentation = new Array(indentationLevel + 1).join(' '); @@ -17,7 +16,7 @@ function prettifyPayload(payload = '', indentationLevel = 0) { * e.g. 1.0, is preserved instead of being coerced to an integer, e.g. 1. */ export function formatRequestPayload( - { code, context, parameters, index, document, query }: Partial, + { code, context, parameters, index, document, query }: Partial, format: PayloadFormat = PayloadFormat.UGLY ): string { const isAdvancedContext = context === 'filter' || context === 'score'; diff --git a/x-pack/plugins/painless_lab/public/application/common/types.ts b/x-pack/plugins/painless_lab/public/application/types.ts similarity index 69% rename from x-pack/plugins/painless_lab/public/application/common/types.ts rename to x-pack/plugins/painless_lab/public/application/types.ts index e0c7a8c7a6ff3..d800558ef7ecc 100644 --- a/x-pack/plugins/painless_lab/public/application/common/types.ts +++ b/x-pack/plugins/painless_lab/public/application/types.ts @@ -4,7 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ -// This should be an enumerated list +export interface Store { + payload: Payload; + validation: Validation; +} + +export interface Payload { + context: string; + code: string; + parameters: string; + index: string; + document: string; + query: string; +} + +export interface Validation { + isValid: boolean; + fields: { + index: boolean; + }; +} + +// TODO: This should be an enumerated list export type Context = string; export enum PayloadFormat { 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 559d02aa08386..55adb5e0410cc 100644 --- a/x-pack/plugins/painless_lab/server/routes/api/execute.ts +++ b/x-pack/plugins/painless_lab/server/routes/api/execute.ts @@ -5,8 +5,8 @@ */ import { schema } from '@kbn/config-schema'; -import { RouteDependencies } from '../../types'; import { API_BASE_PATH } from '../../../common/constants'; +import { RouteDependencies } from '../../types'; import { isEsError } from '../../lib'; const bodySchema = schema.string(); From e77eb947ac669b22652e51158f6ec010cb4b3f2d Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Mon, 23 Mar 2020 13:30:13 -0700 Subject: [PATCH 22/23] Fix bottom bar z-index. --- x-pack/plugins/painless_lab/public/styles/_index.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/painless_lab/public/styles/_index.scss b/x-pack/plugins/painless_lab/public/styles/_index.scss index ed9f7d5a8d248..f68dbe302511a 100644 --- a/x-pack/plugins/painless_lab/public/styles/_index.scss +++ b/x-pack/plugins/painless_lab/public/styles/_index.scss @@ -45,10 +45,11 @@ $bottomBarHeight: calc(#{$euiSize} * 3); } /** - * 1. Hack EUI so the bottom bar doesn't obscure the nav drawer flyout. + * 1. Hack EUI so the bottom bar doesn't obscure the nav drawer flyout, but is also not obscured + * by the main content area. */ .painlessLab__bottomBar { - z-index: 0; /* 1 */ + z-index: 5; /* 1 */ left: $euiNavDrawerWidthCollapsed; } From 111f2f845bac8eff01d288df85127abbde12f9b5 Mon Sep 17 00:00:00 2001 From: CJ Cenizal Date: Mon, 23 Mar 2020 13:39:54 -0700 Subject: [PATCH 23/23] Position flyout help link so it's bottom-aligned with the title and farther from the close button. --- .../public/application/components/request_flyout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/painless_lab/public/application/components/request_flyout.tsx b/x-pack/plugins/painless_lab/public/application/components/request_flyout.tsx index 34918a48cfc4e..123df91f4346a 100644 --- a/x-pack/plugins/painless_lab/public/application/components/request_flyout.tsx +++ b/x-pack/plugins/painless_lab/public/application/components/request_flyout.tsx @@ -35,7 +35,7 @@ export const RequestFlyout: FunctionComponent = ({ return ( - + {/* We need an extra div to get out of flex grow */}