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;
}