diff --git a/changelogs/fragments/8565.yml b/changelogs/fragments/8565.yml new file mode 100644 index 000000000000..c555535dcf51 --- /dev/null +++ b/changelogs/fragments/8565.yml @@ -0,0 +1,2 @@ +feat: +- Adds editor footer to single line editor on focus ([#8565](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/8565)) \ No newline at end of file diff --git a/src/plugins/data/common/data_frames/types.ts b/src/plugins/data/common/data_frames/types.ts index 9fe6deb75985..31d07f9a5070 100644 --- a/src/plugins/data/common/data_frames/types.ts +++ b/src/plugins/data/common/data_frames/types.ts @@ -121,7 +121,7 @@ export type IDataFrameResponse = SearchResponse & (IDataFrameDefaultResponse | IDataFramePollingResponse | IDataFrameErrorResponse); export interface IDataFrameError extends SearchResponse { - error: Error; + error: Error | string; } export interface PollQueryResultsParams { diff --git a/src/plugins/data/common/utils/helpers.ts b/src/plugins/data/common/utils/helpers.ts index 2d1ab2ab1417..1f10f9a320d8 100644 --- a/src/plugins/data/common/utils/helpers.ts +++ b/src/plugins/data/common/utils/helpers.ts @@ -28,7 +28,6 @@ * under the License. */ -import { i18n } from '@osd/i18n'; import { PollQueryResultsHandler, FetchStatusResponse } from '../data_frames'; export interface QueryStatusOptions { @@ -53,11 +52,7 @@ export const handleQueryResults = async ( } while (queryStatus !== 'SUCCESS' && queryStatus !== 'FAILED'); if (queryStatus === 'FAILED') { - throw new Error( - i18n.translate('data.search.request.failed', { - defaultMessage: 'An error occurred while executing the search query', - }) - ); + throw new Error(queryResultsRes?.body.error); } return queryResultsRes; diff --git a/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap b/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap index cec14cdb7fa2..03b83fd37c34 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap +++ b/src/plugins/data/public/query/query_string/language_service/lib/__snapshots__/query_result.test.tsx.snap @@ -5,6 +5,8 @@ exports[`Query Result show error status with error message 2`] = ` anchorPosition="downRight" button={ Error @@ -43,6 +46,7 @@ exports[`Query Result show error status with error message 2`] = ` Reasons: + error reason Details: + error details

diff --git a/src/plugins/data/public/query/query_string/language_service/lib/_recent_query.scss b/src/plugins/data/public/query/query_string/language_service/lib/_recent_query.scss index 4b7a30ce85af..b932fe1adc33 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/_recent_query.scss +++ b/src/plugins/data/public/query/query_string/language_service/lib/_recent_query.scss @@ -7,3 +7,8 @@ background-color: $euiColorLightestShade; } } + +.editor_footerItem { + // Needed so the footer items never have paddings + padding: 0 !important; +} diff --git a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx index 074ba4b5d0d5..2e8ab769e2e4 100644 --- a/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx +++ b/src/plugins/data/public/query/query_string/language_service/lib/query_result.tsx @@ -22,10 +22,9 @@ export interface QueryStatus { status: ResultStatus; body?: { error?: { - reason?: string; - details: string; + statusCode?: number; + message?: string; }; - statusCode?: number; }; elapsedMs?: number; startTime?: number; @@ -77,6 +76,22 @@ export function QueryResult(props: { queryStatus: QueryStatus }) {
); } + const time = Math.floor(elapsedTime / 1000); + return ( + {}} + isLoading + data-test-subj="queryResultLoading" + className="editor__footerItem" + > + {i18n.translate('data.query.languageService.queryResults.loadTime', { + defaultMessage: 'Loading {time} s', + values: { time }, + })} + + ); } if (props.queryStatus.status === ResultStatus.READY) { @@ -101,7 +116,13 @@ export function QueryResult(props: { queryStatus: QueryStatus }) { } return ( - {}}> + {}} + > {message} @@ -122,8 +143,10 @@ export function QueryResult(props: { queryStatus: QueryStatus }) { size="xs" onClick={onButtonClick} data-test-subj="queryResultErrorBtn" + className="editor__footerItem" + color="danger" > - + {i18n.translate('data.query.languageService.queryResults.error', { defaultMessage: `Error`, })} @@ -137,23 +160,15 @@ export function QueryResult(props: { queryStatus: QueryStatus }) { data-test-subj="queryResultError" > ERRORS -
- - - {i18n.translate('data.query.languageService.queryResults.reasons', { - defaultMessage: `Reasons:`, - })} - - {props.queryStatus.body.error.reason} - +

- {i18n.translate('data.query.languageService.queryResults.details', { - defaultMessage: `Details:`, + {i18n.translate('data.query.languageService.queryResults.message', { + defaultMessage: `Message:`, })} - - {props.queryStatus.body.error.details} + {' '} + {props.queryStatus.body.error.message}

diff --git a/src/plugins/data/public/ui/query_editor/_query_editor.scss b/src/plugins/data/public/ui/query_editor/_query_editor.scss index cf2321cf3d4e..d6577d33fdd3 100644 --- a/src/plugins/data/public/ui/query_editor/_query_editor.scss +++ b/src/plugins/data/public/ui/query_editor/_query_editor.scss @@ -218,3 +218,40 @@ display: block; } } + +.queryEditor__footer { + display: flex; + gap: 4px; + background: $euiColorLightestShade; + padding: 2px 4px; + margin-top: 5px; + margin-left: -5px; + margin-right: -5px; + z-index: 1; + position: relative; + align-items: center; +} + +.queryEditor__footerSpacer { + flex-grow: 1; +} + +.queryEditor__footerItem { + // Needed so the footer items never have paddings + padding: 0 !important; +} + +// TODO: Temporary workaround to disable padding for single line editor footer +.euiFormControlLayout--group.euiFormControlLayout--compressed .osdQuerEditor__singleLine .euiText { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.euiFormControlLayout--group .osdQuerEditor__singleLine .euiText { + background-color: unset !important; + line-height: 21px !important; +} + +.euiFormControlLayout--group .osdQuerEditor__singleLine .euiButtonEmpty { + border-right: 0; +} diff --git a/src/plugins/data/public/ui/query_editor/editors/shared.tsx b/src/plugins/data/public/ui/query_editor/editors/shared.tsx index ab7ad44bbf7d..fb5df10c65c9 100644 --- a/src/plugins/data/public/ui/query_editor/editors/shared.tsx +++ b/src/plugins/data/public/ui/query_editor/editors/shared.tsx @@ -3,9 +3,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiCompressedFieldText } from '@elastic/eui'; +import { EuiCompressedFieldText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { monaco } from '@osd/monaco'; -import React from 'react'; +import React, { Fragment, useCallback, useRef, useState } from 'react'; import { CodeEditor } from '../../../../../opensearch_dashboards_react/public'; interface SingleLineInputProps extends React.JSX.IntrinsicAttributes { @@ -15,6 +15,7 @@ interface SingleLineInputProps extends React.JSX.IntrinsicAttributes { editorDidMount: (editor: any) => void; provideCompletionItems: monaco.languages.CompletionItemProvider['provideCompletionItems']; prepend?: React.ComponentProps['prepend']; + footerItems?: any; } type CollapsedComponent = React.ComponentType; @@ -61,56 +62,109 @@ export const SingleLineInput: React.FC = ({ editorDidMount, provideCompletionItems, prepend, -}) => ( -
- {prepend} -
- { + const [editorIsFocused, setEditorIsFocused] = useState(false); + const blurTimeoutRef = useRef(); + + const handleEditorDidMount = useCallback( + (editor: monaco.editor.IStandaloneCodeEditor) => { + editorDidMount(editor); + + const focusDisposable = editor.onDidFocusEditorText(() => { + if (blurTimeoutRef.current) { + clearTimeout(blurTimeoutRef.current); + } + setEditorIsFocused(true); + }); + + const blurDisposable = editor.onDidBlurEditorText(() => { + blurTimeoutRef.current = setTimeout(() => { + setEditorIsFocused(false); + }, 500); + }); + + return () => { + focusDisposable.dispose(); + blurDisposable.dispose(); + if (blurTimeoutRef.current) { + clearTimeout(blurTimeoutRef.current); + } + }; + }, + [editorDidMount] + ); + + return ( +
+ {prepend} +
+ + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + cursorStyle: 'line', + wordBasedSuggestions: false, + }} + suggestionProvider={{ + provideCompletionItems, + triggerCharacters: [' '], + }} + languageConfiguration={{ + autoClosingPairs: [ + { + open: '(', + close: ')', + }, + { + open: '"', + close: '"', + }, + ], + }} + triggerSuggestOnFocus={true} + /> + {editorIsFocused && ( +
+ {footerItems && ( + + {footerItems.start?.map((item) => ( +
{item}
+ ))} +
+ {footerItems.end?.map((item) => ( +
{item}
+ ))} + + )} +
+ )} +
-
-); + ); +}; diff --git a/src/plugins/data/public/ui/query_editor/query_editor.tsx b/src/plugins/data/public/ui/query_editor/query_editor.tsx index b4299cb6c829..e67a696de838 100644 --- a/src/plugins/data/public/ui/query_editor/query_editor.tsx +++ b/src/plugins/data/public/ui/query_editor/query_editor.tsx @@ -376,10 +376,15 @@ export default class QueryEditorUI extends Component { }, footerItems: { start: [ - + {`${this.state.lineCount} ${this.state.lineCount === 1 ? 'line' : 'lines'}`} , - + {this.props.query.dataset?.timeFieldName || ''} , , @@ -390,6 +395,7 @@ export default class QueryEditorUI extends Component { iconType="clock" size="xs" onClick={this.toggleRecentQueries} + className="queryEditor__footerItem" > {'Recent queries'} @@ -429,6 +435,34 @@ export default class QueryEditorUI extends Component { }, provideCompletionItems: this.provideCompletionItems, prepend: this.props.prepend, + footerItems: { + start: [ + + {`${this.state.lineCount ?? 1} ${ + this.state.lineCount === 1 || !this.state.lineCount ? 'line' : 'lines' + }`} + , + + {this.props.query.dataset?.timeFieldName || ''} + , + , + ], + end: [ + + + {'Recent queries'} + + , + ], + }, }; const languageEditorFunc = this.languageManager.getLanguage(this.props.query.language)!.editor; @@ -478,13 +512,13 @@ export default class QueryEditorUI extends Component { {!this.state.isCollapsed && ( <>
{languageEditor.Body()}
- )} + {this.renderQueryEditorExtensions()}
diff --git a/src/plugins/discover/public/application/view_components/utils/use_search.ts b/src/plugins/discover/public/application/view_components/utils/use_search.ts index 3796faea1142..a789280f7db5 100644 --- a/src/plugins/discover/public/application/view_components/utils/use_search.ts +++ b/src/plugins/discover/public/application/view_components/utils/use_search.ts @@ -150,6 +150,7 @@ export const useSearch = (services: DiscoverViewServices) => { if (!dataset) { data$.next({ status: shouldSearchOnPageLoad() ? ResultStatus.LOADING : ResultStatus.UNINITIALIZED, + queryStatus: { startTime }, }); return; } @@ -181,7 +182,7 @@ export const useSearch = (services: DiscoverViewServices) => { try { // Only show loading indicator if we are fetching when the rows are empty if (fetchStateRef.current.rows?.length === 0) { - data$.next({ status: ResultStatus.LOADING }); + data$.next({ status: ResultStatus.LOADING, queryStatus: { startTime } }); } // Initialize inspect adapter for search source @@ -272,15 +273,19 @@ export const useSearch = (services: DiscoverViewServices) => { } let errorBody; try { - errorBody = JSON.parse(error.message); + errorBody = JSON.parse(error.body); } catch (e) { - errorBody = error.message; + if (error.body) { + errorBody = error.body; + } else { + errorBody = error; + } } data$.next({ status: ResultStatus.ERROR, queryStatus: { - body: errorBody, + body: { error: errorBody }, elapsedMs, }, }); @@ -296,6 +301,7 @@ export const useSearch = (services: DiscoverViewServices) => { services, sort, savedSearch?.searchSource, + startTime, data$, shouldSearchOnPageLoad, inspectorAdapters.requests, diff --git a/src/plugins/query_enhancements/server/routes/index.ts b/src/plugins/query_enhancements/server/routes/index.ts index cb2dbc644189..dd17231adee2 100644 --- a/src/plugins/query_enhancements/server/routes/index.ts +++ b/src/plugins/query_enhancements/server/routes/index.ts @@ -85,8 +85,14 @@ export function defineSearchStrategyRouteProvider(logger: Logger, router: IRoute const queryRes: IDataFrameResponse = await searchStrategy.search(context, req as any, {}); return res.ok({ body: { ...queryRes } }); } catch (err) { + let error; + try { + error = JSON.parse(err.message); + } catch (e) { + error = err; + } return res.custom({ - statusCode: err.name, + statusCode: error.status, body: err.message, }); } diff --git a/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts b/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts index a48d47ce7826..9f3e7fd57e6f 100644 --- a/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts +++ b/src/plugins/query_enhancements/server/search/sql_async_search_strategy.ts @@ -82,7 +82,7 @@ export const sqlAsyncSearchStrategyProvider = ( type: DATA_FRAME_TYPES.POLLING, status: 'failed', body: { - error: new Error(`JOB: ${pollQueryResultsParams.queryId} failed`), + error: `JOB: ${inProgressQueryId} failed: ${queryStatusResponse.data.error}`, }, } as IDataFrameResponse; }