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();