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: '', +};