diff --git a/.buildkite/ftr_platform_stateful_configs.yml b/.buildkite/ftr_platform_stateful_configs.yml index fc46fa24f257f..02d6355c212bd 100644 --- a/.buildkite/ftr_platform_stateful_configs.yml +++ b/.buildkite/ftr_platform_stateful_configs.yml @@ -44,8 +44,7 @@ enabled: - test/api_integration/config.js - test/examples/config.js - test/functional/apps/bundles/config.ts - - test/functional/apps/console/monaco/config.ts - - test/functional/apps/console/ace/config.ts + - test/functional/apps/console/config.ts - test/functional/apps/context/config.ts - test/functional/apps/dashboard_elements/controls/common/config.ts - test/functional/apps/dashboard_elements/controls/options_list/config.ts diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.ts b/.buildkite/scripts/steps/storybooks/build_and_upload.ts index 96208bc02b310..148742c2ae046 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.ts +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.ts @@ -22,7 +22,6 @@ const STORYBOOKS = [ 'coloring', 'chart_icons', 'content_management_examples', - 'controls', 'custom_integrations', 'dashboard_enhanced', 'dashboard', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 457458ec5b1c6..76dccf659fb54 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1759,7 +1759,7 @@ x-pack/plugins/security_solution/server/lib/security_integrations @elastic/secur /x-pack/plugins/monitoring/**/*.scss @elastic/observability-design # Ent. Search design -/x-pack/plugins/enterprise_search/**/*.scss @elastic/ent-search-design +/x-pack/plugins/enterprise_search/**/*.scss @elastic/search-design # Security design /x-pack/plugins/endpoint/**/*.scss @elastic/security-design diff --git a/examples/controls_example/public/app/control_group_renderer_examples/edit_example.tsx b/examples/controls_example/public/app/control_group_renderer_examples/edit_example.tsx index d98b3cb3a041b..cf5a76956c36d 100644 --- a/examples/controls_example/public/app/control_group_renderer_examples/edit_example.tsx +++ b/examples/controls_example/public/app/control_group_renderer_examples/edit_example.tsx @@ -9,6 +9,7 @@ import { pickBy } from 'lodash'; import React, { useEffect, useState } from 'react'; + import { EuiButton, EuiButtonEmpty, @@ -16,20 +17,24 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, + EuiSkeletonRectangle, EuiSpacer, EuiText, EuiTitle, - EuiSkeletonRectangle, } from '@elastic/eui'; -import { ViewMode } from '@kbn/embeddable-plugin/public'; -import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL } from '@kbn/controls-plugin/common'; -import { ControlGroupRuntimeState, ControlStateTransform } from '@kbn/controls-plugin/public'; +import { + OPTIONS_LIST_CONTROL, + RANGE_SLIDER_CONTROL, + type ControlGroupRuntimeState, +} from '@kbn/controls-plugin/common'; import { ACTION_DELETE_CONTROL, ACTION_EDIT_CONTROL, ControlGroupRenderer, + ControlGroupRendererApi, + type ControlStateTransform, } from '@kbn/controls-plugin/public'; -import { ControlGroupRendererApi } from '@kbn/controls-plugin/public'; +import { ViewMode } from '@kbn/embeddable-plugin/public'; const INPUT_KEY = 'kbnControls:saveExample:input'; diff --git a/examples/controls_example/public/app/control_group_renderer_examples/search_example.tsx b/examples/controls_example/public/app/control_group_renderer_examples/search_example.tsx index ed9a0403d449b..2325840c21927 100644 --- a/examples/controls_example/public/app/control_group_renderer_examples/search_example.tsx +++ b/examples/controls_example/public/app/control_group_renderer_examples/search_example.tsx @@ -8,12 +8,9 @@ */ import React, { useEffect, useState } from 'react'; -import { v4 as uuidv4 } from 'uuid'; import { lastValueFrom } from 'rxjs'; -import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; -import type { Filter, Query, TimeRange } from '@kbn/es-query'; +import { v4 as uuidv4 } from 'uuid'; + import { EuiCallOut, EuiLoadingSpinner, @@ -23,6 +20,11 @@ import { EuiTitle, } from '@elastic/eui'; import { ControlGroupRenderer, ControlGroupRendererApi } from '@kbn/controls-plugin/public'; +import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import type { DataView } from '@kbn/data-views-plugin/public'; +import type { Filter, Query, TimeRange } from '@kbn/es-query'; +import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; + import { PLUGIN_ID } from '../../constants'; interface Props { diff --git a/examples/controls_example/public/app/react_control_example/runtime_control_group_state.ts b/examples/controls_example/public/app/react_control_example/runtime_control_group_state.ts index a2f811a54253c..c5975a65842ba 100644 --- a/examples/controls_example/public/app/react_control_example/runtime_control_group_state.ts +++ b/examples/controls_example/public/app/react_control_example/runtime_control_group_state.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { ControlGroupRuntimeState } from '@kbn/controls-plugin/public'; +import { ControlGroupRuntimeState } from '@kbn/controls-plugin/common'; const RUNTIME_STATE_SESSION_STORAGE_KEY = 'kibana.examples.controls.reactControlExample.controlGroupRuntimeState'; diff --git a/examples/controls_example/public/app/react_control_example/serialized_control_group_state.ts b/examples/controls_example/public/app/react_control_example/serialized_control_group_state.ts index e9e5b070db2bc..3d6487cdc7cba 100644 --- a/examples/controls_example/public/app/react_control_example/serialized_control_group_state.ts +++ b/examples/controls_example/public/app/react_control_example/serialized_control_group_state.ts @@ -7,8 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { SerializedPanelState } from '@kbn/presentation-containers'; -import { ControlGroupSerializedState } from '@kbn/controls-plugin/public'; +import type { SerializedPanelState } from '@kbn/presentation-containers'; +import type { ControlGroupSerializedState } from '@kbn/controls-plugin/common'; import { OPTIONS_LIST_CONTROL, RANGE_SLIDER_CONTROL, diff --git a/examples/discover_customization_examples/public/plugin.tsx b/examples/discover_customization_examples/public/plugin.tsx index c80224422bed8..7c35287b843ba 100644 --- a/examples/discover_customization_examples/public/plugin.tsx +++ b/examples/discover_customization_examples/public/plugin.tsx @@ -28,7 +28,7 @@ import ReactDOM from 'react-dom'; import useObservable from 'react-use/lib/useObservable'; import { ControlGroupRendererApi, ControlGroupRenderer } from '@kbn/controls-plugin/public'; import { css } from '@emotion/react'; -import type { ControlsPanels } from '@kbn/controls-plugin/common'; +import type { ControlPanelsState } from '@kbn/controls-plugin/common'; import { Route, Router, Routes } from '@kbn/shared-ux-router'; import { I18nProvider } from '@kbn/i18n-react'; import { KibanaThemeProvider } from '@kbn/react-kibana-context-theme'; @@ -357,7 +357,7 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin { } const stateSubscription = stateStorage - .change$('controlPanels') + .change$('controlPanels') .subscribe((panels) => controlGroupAPI.updateInput({ initialChildControlState: panels ?? undefined }) ); @@ -410,7 +410,7 @@ export class DiscoverCustomizationExamplesPlugin implements Plugin { { - const panels = stateStorage.get('controlPanels'); + const panels = stateStorage.get('controlPanels'); if (!panels) { builder.addOptionsListControl(initialState, { diff --git a/packages/kbn-esql-ast/src/pretty_print/__tests__/fixtures.ts b/packages/kbn-esql-ast/src/pretty_print/__tests__/fixtures.ts index 43325fba10809..5e2113bbfb623 100644 --- a/packages/kbn-esql-ast/src/pretty_print/__tests__/fixtures.ts +++ b/packages/kbn-esql-ast/src/pretty_print/__tests__/fixtures.ts @@ -10,7 +10,7 @@ export const query1 = ` from kibana_sample_data_logs | EVAL timestamp=DATE_TRUNC(3 hour, @timestamp), status = CASE( to_integer(response.keyword) >= 200 and to_integer(response.keyword) < 400, "HTTP 2xx and 3xx", to_integer(response.keyword) >= 400 and to_integer(response.keyword) < 500, "HTTP 4xx", "HTTP 5xx") -| stats results = count(*) by \`Over time\` = BUCKET(timestamp, 50, ?t_start, ?t_end), status +| stats results = count(*) by \`Over time\` = BUCKET(timestamp, 50, ?_tstart, ?_tend), status `; export const query2 = ` diff --git a/packages/kbn-esql-ast/src/walker/walker.test.ts b/packages/kbn-esql-ast/src/walker/walker.test.ts index b51dcde38c3a5..eeae2ea142348 100644 --- a/packages/kbn-esql-ast/src/walker/walker.test.ts +++ b/packages/kbn-esql-ast/src/walker/walker.test.ts @@ -793,7 +793,7 @@ describe('Walker.params()', () => { test('can collect all params from grouping functions', () => { const query = - 'ROW x=1, time=2024-07-10 | stats z = avg(x) by bucket(time, 20, ?t_start,?t_end)'; + 'ROW x=1, time=2024-07-10 | stats z = avg(x) by bucket(time, 20, ?_tstart,?_tend)'; const { ast } = getAstAndSyntaxErrors(query); const params = Walker.params(ast); @@ -802,13 +802,13 @@ describe('Walker.params()', () => { type: 'literal', literalType: 'param', paramType: 'named', - value: 't_start', + value: '_tstart', }, { type: 'literal', literalType: 'param', paramType: 'named', - value: 't_end', + value: '_tend', }, ]); }); diff --git a/packages/kbn-esql-utils/src/utils/get_initial_esql_query.test.ts b/packages/kbn-esql-utils/src/utils/get_initial_esql_query.test.ts index a431a3c4ccf46..033849e8e3f6a 100644 --- a/packages/kbn-esql-utils/src/utils/get_initial_esql_query.test.ts +++ b/packages/kbn-esql-utils/src/utils/get_initial_esql_query.test.ts @@ -98,7 +98,7 @@ describe('getInitialESQLQuery', () => { ] as DataView['fields']; const dataView = getDataView('logs*', fields, '@custom_timestamp'); expect(getInitialESQLQuery(dataView)).toBe( - 'FROM logs* | WHERE @custom_timestamp >= ?t_start AND @custom_timestamp <= ?t_end | LIMIT 10' + 'FROM logs* | WHERE @custom_timestamp >= ?_tstart AND @custom_timestamp <= ?_tend | LIMIT 10' ); }); }); diff --git a/packages/kbn-esql-utils/src/utils/get_initial_esql_query.ts b/packages/kbn-esql-utils/src/utils/get_initial_esql_query.ts index b59358a857eda..d1feec759f36a 100644 --- a/packages/kbn-esql-utils/src/utils/get_initial_esql_query.ts +++ b/packages/kbn-esql-utils/src/utils/get_initial_esql_query.ts @@ -20,7 +20,7 @@ export function getInitialESQLQuery(dataView: DataView): string { const timeFieldName = dataView?.timeFieldName; const filterByTimeParams = !hasAtTimestampField && timeFieldName - ? ` | WHERE ${timeFieldName} >= ?t_start AND ${timeFieldName} <= ?t_end` + ? ` | WHERE ${timeFieldName} >= ?_tstart AND ${timeFieldName} <= ?_tend` : ''; return `FROM ${dataView.getIndexPattern()}${filterByTimeParams} | LIMIT 10`; } diff --git a/packages/kbn-esql-utils/src/utils/query_parsing_helpers.test.ts b/packages/kbn-esql-utils/src/utils/query_parsing_helpers.test.ts index 47096f3421d76..5479d6982e46a 100644 --- a/packages/kbn-esql-utils/src/utils/query_parsing_helpers.test.ts +++ b/packages/kbn-esql-utils/src/utils/query_parsing_helpers.test.ts @@ -154,12 +154,12 @@ describe('esql query helpers', () => { }); it('should return the time field if there is at least one time param', () => { - expect(getTimeFieldFromESQLQuery('from a | eval b = 1 | where time >= ?t_start')).toBe( + expect(getTimeFieldFromESQLQuery('from a | eval b = 1 | where time >= ?_tstart')).toBe( 'time' ); }); - it('should return undefined if there is one named param but is not ?t_start or ?t_end', () => { + it('should return undefined if there is one named param but is not ?_tstart or ?_tend', () => { expect( getTimeFieldFromESQLQuery('from a | eval b = 1 | where time >= ?late') ).toBeUndefined(); @@ -167,14 +167,14 @@ describe('esql query helpers', () => { it('should return undefined if there is one named param but is used without a time field', () => { expect( - getTimeFieldFromESQLQuery('from a | eval b = DATE_TRUNC(1 day, ?t_start)') + getTimeFieldFromESQLQuery('from a | eval b = DATE_TRUNC(1 day, ?_tstart)') ).toBeUndefined(); }); it('should return the time field if there is at least one time param in the bucket function', () => { expect( getTimeFieldFromESQLQuery( - 'from a | stats meow = avg(bytes) by bucket(event.timefield, 200, ?t_start, ?t_end)' + 'from a | stats meow = avg(bytes) by bucket(event.timefield, 200, ?_tstart, ?_tend)' ) ).toBe('event.timefield'); }); diff --git a/packages/kbn-esql-utils/src/utils/query_parsing_helpers.ts b/packages/kbn-esql-utils/src/utils/query_parsing_helpers.ts index 53ce6e06bb536..d61d8ea0329cc 100644 --- a/packages/kbn-esql-utils/src/utils/query_parsing_helpers.ts +++ b/packages/kbn-esql-utils/src/utils/query_parsing_helpers.ts @@ -77,7 +77,7 @@ export function removeDropCommandsFromESQLQuery(esql?: string): string { } /** - * When the ?t_start and ?t_end params are used, we want to retrieve the timefield from the query. + * When the ?_tstart and ?_tend params are used, we want to retrieve the timefield from the query. * @param esql:string * @returns string */ @@ -91,7 +91,7 @@ export const getTimeFieldFromESQLQuery = (esql: string) => { const params = Walker.params(ast); const timeNamedParam = params.find( - (param) => param.value === 't_start' || param.value === 't_end' + (param) => param.value === '_tstart' || param.value === '_tend' ); if (!timeNamedParam || !functions.length) { return undefined; diff --git a/packages/kbn-esql-utils/src/utils/run_query.test.ts b/packages/kbn-esql-utils/src/utils/run_query.test.ts index 18089f422483f..8618d078f5e06 100644 --- a/packages/kbn-esql-utils/src/utils/run_query.test.ts +++ b/packages/kbn-esql-utils/src/utils/run_query.test.ts @@ -19,26 +19,26 @@ describe('getStartEndParams', () => { it('should return an array with the start param if exists at the query', () => { const time = { from: 'Jul 5, 2024 @ 08:03:56.849', to: 'Jul 5, 2024 @ 10:03:56.849' }; - const query = 'FROM foo | where time > ?t_start'; + const query = 'FROM foo | where time > ?_tstart'; const params = getStartEndParams(query, time); expect(params).toHaveLength(1); - expect(params[0]).toHaveProperty('t_start'); + expect(params[0]).toHaveProperty('_tstart'); }); it('should return an array with the end param if exists at the query', () => { const time = { from: 'Jul 5, 2024 @ 08:03:56.849', to: 'Jul 5, 2024 @ 10:03:56.849' }; - const query = 'FROM foo | where time < ?t_end'; + const query = 'FROM foo | where time < ?_tend'; const params = getStartEndParams(query, time); expect(params).toHaveLength(1); - expect(params[0]).toHaveProperty('t_end'); + expect(params[0]).toHaveProperty('_tend'); }); it('should return an array with the end and start params if exist at the query', () => { const time = { from: 'Jul 5, 2024 @ 08:03:56.849', to: 'Jul 5, 2024 @ 10:03:56.849' }; - const query = 'FROM foo | where time < ?t_end amd time > ?t_start'; + const query = 'FROM foo | where time < ?_tend amd time > ?_tstart'; const params = getStartEndParams(query, time); expect(params).toHaveLength(2); - expect(params[0]).toHaveProperty('t_start'); - expect(params[1]).toHaveProperty('t_end'); + expect(params[0]).toHaveProperty('_tstart'); + expect(params[1]).toHaveProperty('_tend'); }); }); diff --git a/packages/kbn-esql-utils/src/utils/run_query.ts b/packages/kbn-esql-utils/src/utils/run_query.ts index 0d016a7f22f92..b9b09336a7c20 100644 --- a/packages/kbn-esql-utils/src/utils/run_query.ts +++ b/packages/kbn-esql-utils/src/utils/run_query.ts @@ -16,11 +16,11 @@ import { esFieldTypeToKibanaFieldType } from '@kbn/field-types'; import type { ESQLColumn, ESQLSearchResponse, ESQLSearchParams } from '@kbn/es-types'; import { lastValueFrom } from 'rxjs'; -export const hasStartEndParams = (query: string) => /\?t_start|\?t_end/i.test(query); +export const hasStartEndParams = (query: string) => /\?_tstart|\?_tend/i.test(query); export const getStartEndParams = (query: string, time?: TimeRange) => { - const startNamedParams = /\?t_start/i.test(query); - const endNamedParams = /\?t_end/i.test(query); + const startNamedParams = /\?_tstart/i.test(query); + const endNamedParams = /\?_tend/i.test(query); if (time && (startNamedParams || endNamedParams)) { const timeParams = { start: startNamedParams ? dateMath.parse(time.from)?.toISOString() : undefined, @@ -28,10 +28,10 @@ export const getStartEndParams = (query: string, time?: TimeRange) => { }; const namedParams = []; if (timeParams?.start) { - namedParams.push({ t_start: timeParams.start }); + namedParams.push({ _tstart: timeParams.start }); } if (timeParams?.end) { - namedParams.push({ t_end: timeParams.end }); + namedParams.push({ _tend: timeParams.end }); } return namedParams; } diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts index 452a3b2754644..6b2f5fea0cc8d 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/autocomplete.command.stats.test.ts @@ -292,14 +292,14 @@ describe('autocomplete.suggest', () => { }); test('on space within bucket()', async () => { const { assertSuggestions } = await setup(); - await assertSuggestions('from a | stats avg(b) by BUCKET(/, 50, ?t_start, ?t_end)', [ + await assertSuggestions('from a | stats avg(b) by BUCKET(/, 50, ?_tstart, ?_tend)', [ // Note there's no space or comma in the suggested field names ...getFieldNamesByType(['date', ...ESQL_COMMON_NUMERIC_TYPES]), ...getFunctionSignaturesByReturnType('eval', ['date', ...ESQL_COMMON_NUMERIC_TYPES], { scalar: true, }), ]); - await assertSuggestions('from a | stats avg(b) by BUCKET( / , 50, ?t_start, ?t_end)', [ + await assertSuggestions('from a | stats avg(b) by BUCKET( / , 50, ?_tstart, ?_tend)', [ // Note there's no space or comma in the suggested field names ...getFieldNamesByType(['date', ...ESQL_COMMON_NUMERIC_TYPES]), ...getFunctionSignaturesByReturnType('eval', ['date', ...ESQL_COMMON_NUMERIC_TYPES], { @@ -308,7 +308,7 @@ describe('autocomplete.suggest', () => { ]); await assertSuggestions( - 'from a | stats avg(b) by BUCKET(dateField, /50, ?t_start, ?t_end)', + 'from a | stats avg(b) by BUCKET(dateField, /50, ?_tstart, ?_tend)', [ ...getLiteralsByType('time_literal'), ...getFunctionSignaturesByReturnType('eval', ['integer', 'date_period'], { diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts index b33d705711f05..9d75a6265e5b0 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/factories.ts @@ -30,7 +30,7 @@ const allFunctions = aggregationFunctionDefinitions .concat(scalarFunctionDefinitions) .concat(groupingFunctionDefinitions); -export const TIME_SYSTEM_PARAMS = ['?t_start', '?t_end']; +export const TIME_SYSTEM_PARAMS = ['?_tstart', '?_tend']; export const getAddDateHistogramSnippet = (histogramBarTarget = 50) => { return `BUCKET($0, ${histogramBarTarget}, ${TIME_SYSTEM_PARAMS.join(', ')})`; @@ -442,13 +442,13 @@ export function getCompatibleLiterals( } export const TIME_SYSTEM_DESCRIPTIONS = { - '?t_start': i18n.translate( + '?_tstart': i18n.translate( 'kbn-esql-validation-autocomplete.esql.autocomplete.timeSystemParamStart', { defaultMessage: 'The start time from the date picker', } ), - '?t_end': i18n.translate( + '?_tend': i18n.translate( 'kbn-esql-validation-autocomplete.esql.autocomplete.timeSystemParamEnd', { defaultMessage: 'The end time from the date picker', diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts index f8f68769f0113..bbb2867981425 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/__tests__/validation.params.test.ts @@ -24,16 +24,16 @@ test('should allow param inside agg function argument', async () => { test('allow params in WHERE command expressions', async () => { const { validate } = await setup(); - const res1 = await validate('FROM index | WHERE textField >= ?t_start'); + const res1 = await validate('FROM index | WHERE textField >= ?_tstart'); const res2 = await validate(` FROM index - | WHERE textField >= ?t_start + | WHERE textField >= ?_tstart | WHERE textField <= ?0 | WHERE textField == ? `); const res3 = await validate(` FROM index - | WHERE textField >= ?t_start + | WHERE textField >= ?_tstart AND textField <= ?0 AND textField == ? `); diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts index 0f79a5fff0506..611a7a0e13df5 100644 --- a/packages/kbn-management/settings/setting_ids/index.ts +++ b/packages/kbn-management/settings/setting_ids/index.ts @@ -144,8 +144,6 @@ export const OBSERVABILITY_LOGS_EXPLORER_ALLOWED_DATA_VIEWS_ID = 'observability:logsExplorer:allowedDataViews'; export const OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE = 'observability:entityCentricExperience'; export const OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID = 'observability:logSources'; -export const OBSERVABILITY_AI_ASSISTANT_LOGS_INDEX_PATTERN_ID = - 'observability:aiAssistantLogsIndexPattern'; export const OBSERVABILITY_AI_ASSISTANT_SIMULATED_FUNCTION_CALLING = 'observability:aiAssistantSimulatedFunctionCalling'; export const OBSERVABILITY_AI_ASSISTANT_SEARCH_CONNECTOR_INDEX_PATTERN = diff --git a/packages/kbn-monaco/index.ts b/packages/kbn-monaco/index.ts index 795664b60e7b7..ba8b0edb68e1a 100644 --- a/packages/kbn-monaco/index.ts +++ b/packages/kbn-monaco/index.ts @@ -39,6 +39,7 @@ export { CONSOLE_THEME_ID, getParsedRequestsProvider, ConsoleParsedRequestsProvider, + createOutputParser, } from './src/console'; export type { ParsedRequest } from './src/console'; diff --git a/packages/kbn-monaco/src/console/index.ts b/packages/kbn-monaco/src/console/index.ts index 6b26c6262f568..cf36505b27759 100644 --- a/packages/kbn-monaco/src/console/index.ts +++ b/packages/kbn-monaco/src/console/index.ts @@ -43,3 +43,5 @@ export const ConsoleOutputLang: LangModuleType = { export type { ParsedRequest } from './types'; export { getParsedRequestsProvider } from './language'; export { ConsoleParsedRequestsProvider } from './console_parsed_requests_provider'; + +export { createOutputParser } from './output_parser'; diff --git a/packages/kbn-monaco/src/console/output_parser.js b/packages/kbn-monaco/src/console/output_parser.js new file mode 100644 index 0000000000000..8601cf764055e --- /dev/null +++ b/packages/kbn-monaco/src/console/output_parser.js @@ -0,0 +1,401 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +/* eslint-disable prettier/prettier,prefer-const,no-throw-literal,camelcase,@typescript-eslint/no-shadow,one-var,object-shorthand,eqeqeq */ + +export const createOutputParser = () => { + let at, // The index of the current character + ch, // The current character + escapee = { + '"': '"', + '\\': '\\', + '/': '/', + b: '\b', + f: '\f', + n: '\n', + r: '\r', + t: '\t', + }, + text, + errors, + addError = function (text) { + errors.push({ text: text, offset: at }); + }, + responses, + responseStartOffset, + responseEndOffset, + getLastResponse = function() { + return responses.length > 0 ? responses.pop() : {}; + }, + addResponseStart = function() { + responseStartOffset = at - 1; + responses.push({ startOffset: responseStartOffset }); + }, + addResponseData = function(data) { + const lastResponse = getLastResponse(); + const dataArray = lastResponse.data || []; + dataArray.push(data); + lastResponse.data = dataArray; + responses.push(lastResponse); + responseEndOffset = at - 1; + }, + addResponseEnd = function() { + const lastResponse = getLastResponse(); + lastResponse.endOffset = responseEndOffset; + responses.push(lastResponse); + }, + error = function (m) { + throw { + name: 'SyntaxError', + message: m, + at: at, + text: text, + }; + }, + reset = function (newAt) { + ch = text.charAt(newAt); + at = newAt + 1; + }, + next = function (c) { + if (c && c !== ch) { + error('Expected \'' + c + '\' instead of \'' + ch + '\''); + } + + ch = text.charAt(at); + at += 1; + return ch; + }, + nextUpTo = function (upTo, errorMessage) { + let currentAt = at, + i = text.indexOf(upTo, currentAt); + if (i < 0) { + error(errorMessage || 'Expected \'' + upTo + '\''); + } + reset(i + upTo.length); + return text.substring(currentAt, i); + }, + peek = function (offset) { + return text.charAt(at + offset); + }, + number = function () { + let number, + string = ''; + + if (ch === '-') { + string = '-'; + next('-'); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + if (ch === '.') { + string += '.'; + while (next() && ch >= '0' && ch <= '9') { + string += ch; + } + } + if (ch === 'e' || ch === 'E') { + string += ch; + next(); + if (ch === '-' || ch === '+') { + string += ch; + next(); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + } + number = +string; + if (isNaN(number)) { + error('Bad number'); + } else { + return number; + } + }, + string = function () { + let hex, + i, + string = '', + uffff; + + if (ch === '"') { + // If the current and the next characters are equal to "", empty string or start of triple quoted strings + if (peek(0) === '"' && peek(1) === '"') { + // literal + next('"'); + next('"'); + return nextUpTo('"""', 'failed to find closing \'"""\''); + } else { + while (next()) { + if (ch === '"') { + next(); + return string; + } else if (ch === '\\') { + next(); + if (ch === 'u') { + uffff = 0; + for (i = 0; i < 4; i += 1) { + hex = parseInt(next(), 16); + if (!isFinite(hex)) { + break; + } + uffff = uffff * 16 + hex; + } + string += String.fromCharCode(uffff); + } else if (typeof escapee[ch] === 'string') { + string += escapee[ch]; + } else { + break; + } + } else { + string += ch; + } + } + } + } + error('Bad string'); + }, + white = function () { + while (ch) { + // Skip whitespace. + while (ch && ch <= ' ') { + next(); + } + // if the current char in iteration is '#' or the char and the next char is equal to '//' + // we are on the single line comment + if (ch === '#' || ch === '/' && peek(0) === '/') { + // Until we are on the new line, skip to the next char + while (ch && ch !== '\n') { + next(); + } + } else if (ch === '/' && peek(0) === '*') { + // If the chars starts with '/*', we are on the multiline comment + next(); + next(); + while (ch && !(ch === '*' && peek(0) === '/')) { + // Until we have closing tags '*/', skip to the next char + next(); + } + if (ch) { + next(); + next(); + } + } else break; + } + }, + strictWhite = function () { + while (ch && (ch == ' ' || ch == '\t')) { + next(); + } + }, + newLine = function () { + if (ch == '\n') next(); + }, + word = function () { + switch (ch) { + case 't': + next('t'); + next('r'); + next('u'); + next('e'); + return true; + case 'f': + next('f'); + next('a'); + next('l'); + next('s'); + next('e'); + return false; + case 'n': + next('n'); + next('u'); + next('l'); + next('l'); + return null; + } + error('Unexpected \'' + ch + '\''); + }, + value, // Place holder for the value function. + array = function () { + const array = []; + + if (ch === '[') { + next('['); + white(); + if (ch === ']') { + next(']'); + return array; // empty array + } + while (ch) { + array.push(value()); + white(); + if (ch === ']') { + next(']'); + return array; + } + next(','); + white(); + } + } + error('Bad array'); + }, + object = function () { + let key, + object = {}; + + if (ch === '{') { + next('{'); + white(); + if (ch === '}') { + next('}'); + return object; // empty object + } + while (ch) { + key = string(); + white(); + next(':'); + if (Object.hasOwnProperty.call(object, key)) { + error('Duplicate key "' + key + '"'); + } + object[key] = value(); + white(); + if (ch === '}') { + next('}'); + return object; + } + next(','); + white(); + } + } + error('Bad object'); + }; + + value = function () { + white(); + switch (ch) { + case '{': + return object(); + case '[': + return array(); + case '"': + return string(); + case '-': + return number(); + default: + return ch >= '0' && ch <= '9' ? number() : word(); + } + }; + + let response = function () { + white(); + addResponseStart(); + // it can be an object + if (ch == '{') { + const parsedObject = object(); + addResponseData(parsedObject); + // but it could also be an array of objects + } else if (ch == '[') { + const parsedArray = array(); + parsedArray.forEach(item => { + if (typeof item === 'object') { + addResponseData(item); + } else { + error('Array elements must be objects'); + } + }); + } else { + error('Invalid input'); + } + // multi doc response + strictWhite(); // advance to one new line + newLine(); + strictWhite(); + while (ch == '{') { + // another object + const parsedObject = object(); + addResponseData(parsedObject); + strictWhite(); + newLine(); + strictWhite(); + } + addResponseEnd(); + }, + comment = function () { + while (ch == '#') { + while (ch && ch !== '\n') { + next(); + } + white(); + } + }, + multi_response = function () { + while (ch && ch != '') { + white(); + if (!ch) { + continue; + } + try { + comment(); + white(); + if (!ch) { + continue; + } + response(); + white(); + } catch (e) { + addError(e.message); + // snap + const substring = text.substr(at); + const nextMatch = substring.search(/[#{]/); + if (nextMatch < 1) return; + reset(at + nextMatch); + } + } + }; + + return function (source, reviver) { + let result; + + text = source; + at = 0; + errors = []; + responses = []; + next(); + multi_response(); + white(); + if (ch) { + addError('Syntax error'); + } + + result = { errors, responses }; + + return typeof reviver === 'function' + ? (function walk(holder, key) { + let k, + v, + value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + }({ '': result }, '')) + : result; + }; +} diff --git a/packages/kbn-monaco/src/console/output_parser.test.ts b/packages/kbn-monaco/src/console/output_parser.test.ts new file mode 100644 index 0000000000000..47ec0bbeb65e4 --- /dev/null +++ b/packages/kbn-monaco/src/console/output_parser.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { createOutputParser } from './output_parser'; +import { ConsoleOutputParserResult } from './types'; + +const parser = createOutputParser(); +describe('console output parser', () => { + it('returns errors if input is not correct', () => { + const input = 'x'; + const parserResult = parser(input) as ConsoleOutputParserResult; + + expect(parserResult.responses.length).toBe(1); + // the parser should generate an invalid input error + expect(parserResult.errors).toContainEqual({ text: 'Invalid input', offset: 1 }); + }); + + it('returns parsed responses if the input is correct', () => { + const input = `# 1: GET /my-index/_doc/0 \n { "_index": "my-index" }`; + const { responses, errors } = parser(input) as ConsoleOutputParserResult; + expect(responses.length).toBe(1); + expect(errors.length).toBe(0); + const { data } = responses[0]; + + const expected = [{ _index: 'my-index' }]; + expect(data).toEqual(expected); + }); + + it('parses several responses', () => { + const input = `# 1: GET /my-index/_doc/0 \n { "_index": "my-index" } \n # 2: GET /my-index/_doc/1 \n { "_index": "my-index" }`; + const { responses } = parser(input) as ConsoleOutputParserResult; + expect(responses.length).toBe(2); + }); +}); diff --git a/packages/kbn-monaco/src/console/types.ts b/packages/kbn-monaco/src/console/types.ts index 6c7573eabdb2c..a024e4696f8cd 100644 --- a/packages/kbn-monaco/src/console/types.ts +++ b/packages/kbn-monaco/src/console/types.ts @@ -21,6 +21,16 @@ export interface ConsoleParserResult { requests: ParsedRequest[]; } +export interface ConsoleOutputParsedResponse { + startOffset: number; + endOffset?: number; + data?: Array>; +} +export interface ConsoleOutputParserResult { + errors: ErrorAnnotation[]; + responses: ConsoleOutputParsedResponse[]; +} + export interface ConsoleWorkerDefinition { getParserResult: (modelUri: string) => ConsoleParserResult | undefined; } diff --git a/packages/serverless/settings/observability_project/index.ts b/packages/serverless/settings/observability_project/index.ts index 85f6327bf0a07..f8bb8dbe12542 100644 --- a/packages/serverless/settings/observability_project/index.ts +++ b/packages/serverless/settings/observability_project/index.ts @@ -34,8 +34,8 @@ export const OBSERVABILITY_PROJECT_SETTINGS = [ settings.OBSERVABILITY_APM_ENABLE_TABLE_SEARCH_BAR, settings.OBSERVABILITY_APM_ENABLE_SERVICE_INVENTORY_TABLE_SEARCH_BAR, settings.OBSERVABILITY_ENTITY_CENTRIC_EXPERIENCE, - settings.OBSERVABILITY_AI_ASSISTANT_LOGS_INDEX_PATTERN_ID, settings.OBSERVABILITY_AI_ASSISTANT_SIMULATED_FUNCTION_CALLING, settings.OBSERVABILITY_AI_ASSISTANT_SEARCH_CONNECTOR_INDEX_PATTERN, + settings.OBSERVABILITY_LOGS_DATA_ACCESS_LOG_SOURCES_ID, settings.OBSERVABILITY_SEARCH_EXCLUDED_DATA_TIERS, ]; diff --git a/src/dev/build/tasks/os_packages/docker_generator/run.ts b/src/dev/build/tasks/os_packages/docker_generator/run.ts index 23010717fe495..79239a1af7668 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/run.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/run.ts @@ -51,7 +51,7 @@ export async function runDockerGenerator( */ if (flags.baseImage === 'wolfi') baseImageName = - 'docker.elastic.co/wolfi/chainguard-base:latest@sha256:aad4cd4e5f6d849691748c6933761889db1a20a57231613b98bbff61fa7723ab'; + 'docker.elastic.co/wolfi/chainguard-base:latest@sha256:d4def25f2fd3b0ff9bc68091cd1d89524e41b7d3fc0d3b3a665720eb92145f3b'; let imageFlavor = ''; if (flags.baseImage === 'ubi') imageFlavor += `-ubi`; diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index 25aac790f08fe..c9680f9991955 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -22,7 +22,6 @@ export const storybookAliases = { language_documentation_popover: 'packages/kbn-language-documentation-popover/.storybook', chart_icons: 'packages/kbn-chart-icons/.storybook', content_management_examples: 'examples/content_management_examples/.storybook', - controls: 'src/plugins/controls/storybook', custom_icons: 'packages/kbn-custom-icons/.storybook', custom_integrations: 'src/plugins/custom_integrations/storybook', dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/.storybook', diff --git a/src/plugins/console/common/constants/autocomplete_definitions.ts b/src/plugins/console/common/constants/autocomplete_definitions.ts index b2ef4f1375419..0ab69c1fa9528 100644 --- a/src/plugins/console/common/constants/autocomplete_definitions.ts +++ b/src/plugins/console/common/constants/autocomplete_definitions.ts @@ -17,3 +17,5 @@ export const AUTOCOMPLETE_DEFINITIONS_FOLDER = resolve( export const GENERATED_SUBFOLDER = 'generated'; export const OVERRIDES_SUBFOLDER = 'overrides'; export const MANUAL_SUBFOLDER = 'manual'; + +export const API_DOCS_LINK = 'https://www.elastic.co/docs/api'; diff --git a/src/plugins/console/common/constants/index.ts b/src/plugins/console/common/constants/index.ts index ea572db743ef4..a00bcebcf38cc 100644 --- a/src/plugins/console/common/constants/index.ts +++ b/src/plugins/console/common/constants/index.ts @@ -15,6 +15,7 @@ export { GENERATED_SUBFOLDER, OVERRIDES_SUBFOLDER, MANUAL_SUBFOLDER, + API_DOCS_LINK, } from './autocomplete_definitions'; export { DEFAULT_INPUT_VALUE } from './editor_input'; export { DEFAULT_LANGUAGE, AVAILABLE_LANGUAGES } from './copy_as'; diff --git a/src/plugins/console/public/application/components/console_menu.tsx b/src/plugins/console/public/application/components/console_menu.tsx index 3ed1d67c3602b..4dee3ff06df0a 100644 --- a/src/plugins/console/public/application/components/console_menu.tsx +++ b/src/plugins/console/public/application/components/console_menu.tsx @@ -11,13 +11,7 @@ import React, { Component } from 'react'; import { NotificationsSetup } from '@kbn/core/public'; -import { - EuiIcon, - EuiContextMenuPanel, - EuiContextMenuItem, - EuiPopover, - EuiLink, -} from '@elastic/eui'; +import { EuiContextMenuPanel, EuiContextMenuItem, EuiPopover, EuiButtonIcon } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; @@ -115,15 +109,15 @@ export class ConsoleMenu extends Component { render() { const button = ( - - - + iconType="boxesVertical" + iconSize="s" + /> ); const items = [ diff --git a/src/plugins/console/public/application/components/console_tour_step.tsx b/src/plugins/console/public/application/components/console_tour_step.tsx new file mode 100644 index 0000000000000..578d590bfff4a --- /dev/null +++ b/src/plugins/console/public/application/components/console_tour_step.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { ReactNode, ReactElement } from 'react'; +import { EuiTourStep, PopoverAnchorPosition } from '@elastic/eui'; + +export interface ConsoleTourStepProps { + step: number; + stepsTotal: number; + isStepOpen: boolean; + title: ReactNode; + content: ReactNode; + onFinish: () => void; + footerAction: ReactNode | ReactNode[]; + dataTestSubj: string; + anchorPosition: string; + maxWidth: number; + css?: any; +} + +interface Props { + tourStepProps: ConsoleTourStepProps; + children: ReactNode & ReactElement; +} + +export const ConsoleTourStep = ({ tourStepProps, children }: Props) => { + const { + step, + isStepOpen, + stepsTotal, + title, + content, + onFinish, + footerAction, + dataTestSubj, + anchorPosition, + maxWidth, + css, + } = tourStepProps; + + return ( + + {children} + + ); +}; diff --git a/src/plugins/console/public/application/components/editor_content_spinner.tsx b/src/plugins/console/public/application/components/editor_content_spinner.tsx index eecd9aebf67cc..a4d0ccc98b76c 100644 --- a/src/plugins/console/public/application/components/editor_content_spinner.tsx +++ b/src/plugins/console/public/application/components/editor_content_spinner.tsx @@ -8,12 +8,12 @@ */ import React, { FunctionComponent } from 'react'; -import { EuiSkeletonText, EuiPageSection } from '@elastic/eui'; +import { EuiLoadingSpinner, EuiPageSection } from '@elastic/eui'; export const EditorContentSpinner: FunctionComponent = () => { return ( - - + + ); }; diff --git a/src/plugins/console/public/application/components/editor_example.tsx b/src/plugins/console/public/application/components/editor_example.tsx deleted file mode 100644 index 6a5ab6333c3b5..0000000000000 --- a/src/plugins/console/public/application/components/editor_example.tsx +++ /dev/null @@ -1,91 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { EuiScreenReaderOnly, withEuiTheme } from '@elastic/eui'; -import type { WithEuiThemeProps } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useEffect, useRef } from 'react'; -import { createReadOnlyAceEditor, CustomAceEditor } from '../models/sense_editor'; -// @ts-ignore -import { Mode as InputMode } from '../models/legacy_core_editor/mode/input'; -import { Mode as OutputMode } from '../models/legacy_core_editor/mode/output'; - -interface EditorExampleProps { - panel: string; - example?: string; - theme: WithEuiThemeProps['theme']; - linesOfExampleCode?: number; - mode?: string; -} - -const exampleText = ` -GET _search -{ - "query": { - "match_all": {} - } -} -`; - -const EditorExample = ({ - panel, - example, - theme, - linesOfExampleCode = 6, - mode = 'input', -}: EditorExampleProps) => { - const inputId = `help-example-${panel}-input`; - const wrapperDivRef = useRef(null); - const editorRef = useRef(); - - useEffect(() => { - if (wrapperDivRef.current) { - editorRef.current = createReadOnlyAceEditor(wrapperDivRef.current); - - const editor = editorRef.current; - const editorMode = mode === 'input' ? new InputMode() : new OutputMode(); - editor.update((example || exampleText).trim(), editorMode); - editor.session.setUseWorker(false); - editor.setHighlightActiveLine(false); - - const textareaElement = wrapperDivRef.current.querySelector('textarea'); - if (textareaElement) { - textareaElement.setAttribute('id', inputId); - textareaElement.setAttribute('readonly', 'true'); - } - } - - return () => { - if (editorRef.current) { - editorRef.current.destroy(); - } - }; - }, [example, inputId, mode]); - - const wrapperDivStyle = { - height: `${parseInt(theme.euiTheme.size.base, 10) * linesOfExampleCode}px`, - margin: `${theme.euiTheme.size.base} 0`, - }; - - return ( - <> - - - -
- - ); -}; - -// eslint-disable-next-line import/no-default-export -export default withEuiTheme(EditorExample); diff --git a/src/plugins/console/public/application/components/help_panel.tsx b/src/plugins/console/public/application/components/help_panel.tsx deleted file mode 100644 index 30a356e27002e..0000000000000 --- a/src/plugins/console/public/application/components/help_panel.tsx +++ /dev/null @@ -1,163 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiText, - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiTitle, - EuiSpacer, - EuiLink, -} from '@elastic/eui'; -import EditorExample from './editor_example'; -import { useServicesContext } from '../contexts'; - -interface Props { - onClose: () => void; -} - -export function HelpPanel(props: Props) { - const { docLinks } = useServicesContext(); - - return ( - - - -

- -

-
-
- - -

- -

-

- -

-

- - Console - - ), - queryDsl: ( - - Query DSL - - ), - }} - /> -

- -

- -

- -
-
Ctrl/Cmd + I
-
- -
-
Ctrl/Cmd + /
-
- -
-
Ctrl + Space
-
- -
-
Ctrl/Cmd + Enter
-
- -
-
Ctrl/Cmd + Up/Down
-
- -
-
Ctrl/Cmd + Alt + L
-
- -
-
Ctrl/Cmd + Option + 0
-
- -
-
Down arrow
-
- -
-
Enter/Tab
-
- -
-
Ctrl/Cmd + L
-
- -
-
Esc
-
- -
-
-
-
-
- ); -} diff --git a/src/plugins/console/public/application/components/help_popover.tsx b/src/plugins/console/public/application/components/help_popover.tsx new file mode 100644 index 0000000000000..16e9465d4d388 --- /dev/null +++ b/src/plugins/console/public/application/components/help_popover.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiPopover, + EuiTitle, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, + EuiSpacer, +} from '@elastic/eui'; +import { useServicesContext } from '../contexts'; + +interface HelpPopoverProps { + button: any; + isOpen: boolean; + closePopover: () => void; + resetTour: () => void; +} + +export const HelpPopover = ({ button, isOpen, closePopover, resetTour }: HelpPopoverProps) => { + const { docLinks } = useServicesContext(); + + return ( + + +

+ {i18n.translate('console.helpPopover.title', { + defaultMessage: 'Elastic Console', + })} +

+
+ + + + +

+ {i18n.translate('console.helpPopover.description', { + defaultMessage: + 'Console is an interactive UI for calling Elasticsearch and Kibana APIs and viewing their responses. Search your data, manage settings, and more, using Query DSL and REST API syntax.', + })} +

+
+ + + + + + + +

+ {i18n.translate('console.helpPopover.aboutConsoleLabel', { + defaultMessage: 'About Console', + })} +

+
+ + + +
+
+ + + + +

+ {i18n.translate('console.helpPopover.aboutQueryDSLLabel', { + defaultMessage: 'About Query DSL', + })} +

+
+ + + +
+
+ + + + +

+ {i18n.translate('console.helpPopover.rerunTourLabel', { + defaultMessage: 'Re-run feature tour', + })} +

+
+ + + +
+
+
+
+ ); +}; diff --git a/src/plugins/console/public/application/components/index.ts b/src/plugins/console/public/application/components/index.ts index e091fb5f2f8a5..111778d8fa776 100644 --- a/src/plugins/console/public/application/components/index.ts +++ b/src/plugins/console/public/application/components/index.ts @@ -7,50 +7,14 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; -import { withSuspense } from '@kbn/shared-ux-utility'; - export { NetworkRequestStatusBar } from './network_request_status_bar'; export { SomethingWentWrongCallout } from './something_went_wrong_callout'; export type { TopNavMenuItem } from './top_nav_menu'; export { TopNavMenu } from './top_nav_menu'; export { ConsoleMenu } from './console_menu'; -export { WelcomePanel } from './welcome_panel'; -export type { AutocompleteOptions } from './settings_modal'; -export { HelpPanel } from './help_panel'; export { EditorContentSpinner } from './editor_content_spinner'; -export type { DevToolsVariable } from './variables'; - -/** - * The Lazily-loaded `DevToolsSettingsModal` component. Consumers should use `React.Suspense` or - * the withSuspense` HOC to load this component. - */ -export const DevToolsSettingsModalLazy = React.lazy(() => - import('./settings_modal').then(({ DevToolsSettingsModal }) => ({ - default: DevToolsSettingsModal, - })) -); - -/** - * A `DevToolsSettingsModal` component that is wrapped by the `withSuspense` HOC. This component can - * be used directly by consumers and will load the `DevToolsSettingsModalLazy` component lazily with - * a predefined fallback and error boundary. - */ -export const DevToolsSettingsModal = withSuspense(DevToolsSettingsModalLazy); - -/** - * The Lazily-loaded `DevToolsVariablesFlyout` component. Consumers should use `React.Suspense` or - * the withSuspense` HOC to load this component. - */ -export const DevToolsVariablesFlyoutLazy = React.lazy(() => - import('./variables').then(({ DevToolsVariablesFlyout }) => ({ - default: DevToolsVariablesFlyout, - })) -); - -/** - * A `DevToolsVariablesFlyout` component that is wrapped by the `withSuspense` HOC. This component can - * be used directly by consumers and will load the `DevToolsVariablesFlyoutLazy` component lazily with - * a predefined fallback and error boundary. - */ -export const DevToolsVariablesFlyout = withSuspense(DevToolsVariablesFlyoutLazy); +export { OutputPanelEmptyState } from './output_panel_empty_state'; +export { HelpPopover } from './help_popover'; +export { ShortcutsPopover } from './shortcuts_popover'; +export type { DevToolsVariable } from './variables/types'; +export { ConsoleTourStep, type ConsoleTourStepProps } from './console_tour_step'; diff --git a/src/plugins/console/public/application/components/output_panel_empty_state.tsx b/src/plugins/console/public/application/components/output_panel_empty_state.tsx new file mode 100644 index 0000000000000..6fdda1b5e3c5f --- /dev/null +++ b/src/plugins/console/public/application/components/output_panel_empty_state.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { FunctionComponent } from 'react'; +import { EuiEmptyPrompt, EuiTitle, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useServicesContext } from '../contexts'; + +export const OutputPanelEmptyState: FunctionComponent = () => { + const { docLinks } = useServicesContext(); + + return ( + + + + } + body={ +

+ +

+ } + footer={ + <> + +

+ +

+
+ + + + + } + data-test-subj="consoleOutputPanelEmptyState" + /> + ); +}; diff --git a/src/plugins/console/public/application/components/settings/index.ts b/src/plugins/console/public/application/components/settings/index.ts new file mode 100644 index 0000000000000..b446307a04a01 --- /dev/null +++ b/src/plugins/console/public/application/components/settings/index.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 + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { withSuspense } from '@kbn/shared-ux-utility'; + +export { type Props } from './settings_editor'; +export { type AutocompleteOptions } from './types'; + +/** + * The Lazily-loaded `SettingsEditorLazy` component. Consumers should use `React.Suspense` or + * the withSuspense` HOC to load this component. + */ +export const SettingsEditorLazy = React.lazy(() => + import('./settings_editor').then(({ SettingsEditor }) => ({ + default: SettingsEditor, + })) +); + +/** + * A `SettingsEditor` component that is wrapped by the `withSuspense` HOC. This component can + * be used directly by consumers and will load the `SettingsEditorLazy` component lazily with + * a predefined fallback and error boundary. + */ +export const SettingsEditor = withSuspense(SettingsEditorLazy); diff --git a/src/plugins/console/public/application/components/settings/settings_editor.tsx b/src/plugins/console/public/application/components/settings/settings_editor.tsx new file mode 100644 index 0000000000000..6f2bef834a559 --- /dev/null +++ b/src/plugins/console/public/application/components/settings/settings_editor.tsx @@ -0,0 +1,371 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { debounce } from 'lodash'; +import React, { useState, useCallback, useEffect, useRef } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { + EuiButton, + EuiFieldNumber, + EuiSwitch, + EuiSuperSelect, + EuiTitle, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { SettingsGroup } from './settings_group'; +import { SettingsFormRow } from './settings_form_row'; +import { DevToolsSettings } from '../../../services'; + +const DEBOUNCE_DELAY = 500; +const ON_LABEL = i18n.translate('console.settingsPage.onLabel', { defaultMessage: 'On' }); +const OFF_LABEL = i18n.translate('console.settingsPage.offLabel', { defaultMessage: 'Off' }); + +const onceTimeInterval = () => + i18n.translate('console.settingsPage.refreshInterval.onceTimeInterval', { + defaultMessage: 'Once, when console loads', + }); + +const everyNMinutesTimeInterval = (value: number) => + i18n.translate('console.settingsPage.refreshInterval.everyNMinutesTimeInterval', { + defaultMessage: 'Every {value} {value, plural, one {minute} other {minutes}}', + values: { value }, + }); + +const everyHourTimeInterval = () => + i18n.translate('console.settingsPage.refreshInterval.everyHourTimeInterval', { + defaultMessage: 'Every hour', + }); + +const PRESETS_IN_MINUTES = [0, 1, 10, 20, 60]; +const intervalOptions = PRESETS_IN_MINUTES.map((value) => ({ + value: (value * 60000).toString(), + inputDisplay: + value === 0 + ? onceTimeInterval() + : value === 60 + ? everyHourTimeInterval() + : everyNMinutesTimeInterval(value), +})); + +export interface Props { + onSaveSettings: (newSettings: DevToolsSettings) => void; + refreshAutocompleteSettings: (selectedSettings: DevToolsSettings['autocomplete']) => void; + settings: DevToolsSettings; +} + +export const SettingsEditor = (props: Props) => { + const isMounted = useRef(false); + + const [fontSize, setFontSize] = useState(props.settings.fontSize); + const [wrapMode, setWrapMode] = useState(props.settings.wrapMode); + const [fields, setFields] = useState(props.settings.autocomplete.fields); + const [indices, setIndices] = useState(props.settings.autocomplete.indices); + const [templates, setTemplates] = useState(props.settings.autocomplete.templates); + const [dataStreams, setDataStreams] = useState(props.settings.autocomplete.dataStreams); + const [polling, setPolling] = useState(props.settings.polling); + const [pollInterval, setPollInterval] = useState(props.settings.pollInterval); + const [tripleQuotes, setTripleQuotes] = useState(props.settings.tripleQuotes); + const [isHistoryEnabled, setIsHistoryEnabled] = useState(props.settings.isHistoryEnabled); + const [isKeyboardShortcutsEnabled, setIsKeyboardShortcutsEnabled] = useState( + props.settings.isKeyboardShortcutsEnabled + ); + const [isAccessibilityOverlayEnabled, setIsAccessibilityOverlayEnabled] = useState( + props.settings.isAccessibilityOverlayEnabled + ); + + const autoCompleteCheckboxes = [ + { + id: 'fields', + label: i18n.translate('console.settingsPage.fieldsLabelText', { + defaultMessage: 'Fields', + }), + stateSetter: setFields, + checked: fields, + }, + { + id: 'indices', + label: i18n.translate('console.settingsPage.indicesAndAliasesLabelText', { + defaultMessage: 'Indices and aliases', + }), + stateSetter: setIndices, + checked: indices, + }, + { + id: 'templates', + label: i18n.translate('console.settingsPage.templatesLabelText', { + defaultMessage: 'Templates', + }), + stateSetter: setTemplates, + checked: templates, + }, + { + id: 'dataStreams', + label: i18n.translate('console.settingsPage.dataStreamsLabelText', { + defaultMessage: 'Data streams', + }), + stateSetter: setDataStreams, + checked: dataStreams, + }, + ]; + + const saveSettings = () => { + props.onSaveSettings({ + fontSize, + wrapMode, + autocomplete: { + fields, + indices, + templates, + dataStreams, + }, + polling, + pollInterval, + tripleQuotes, + isHistoryEnabled, + isKeyboardShortcutsEnabled, + isAccessibilityOverlayEnabled, + }); + }; + const debouncedSaveSettings = debounce(saveSettings, DEBOUNCE_DELAY); + + useEffect(() => { + if (isMounted.current) { + debouncedSaveSettings(); + } else { + isMounted.current = true; + } + }, [ + fontSize, + wrapMode, + fields, + indices, + templates, + dataStreams, + polling, + pollInterval, + tripleQuotes, + isHistoryEnabled, + isKeyboardShortcutsEnabled, + isAccessibilityOverlayEnabled, + debouncedSaveSettings, + ]); + + const onPollingIntervalChange = useCallback((value: string) => { + const sanitizedValue = parseInt(value, 10); + + setPolling(!!sanitizedValue); + setPollInterval(sanitizedValue); + }, []); + + const toggleKeyboardShortcuts = useCallback((isEnabled: boolean) => { + setIsKeyboardShortcutsEnabled(isEnabled); + }, []); + + const toggleAccessibilityOverlay = useCallback( + (isEnabled: boolean) => setIsAccessibilityOverlayEnabled(isEnabled), + [] + ); + + const toggleSavingToHistory = useCallback( + (isEnabled: boolean) => setIsHistoryEnabled(isEnabled), + [] + ); + + return ( + <> + +

+ +

+
+ + +

+ +

+
+ + {/* GENERAL SETTINGS */} + + + toggleSavingToHistory(e.target.checked)} + /> + + + toggleKeyboardShortcuts(e.target.checked)} + /> + + + toggleAccessibilityOverlay(e.target.checked)} + /> + + + {/* DISPLAY SETTINGS */} + + + { + const val = parseInt(e.target.value, 10); + if (!val) return; + setFontSize(val); + }} + /> + + + setWrapMode(e.target.checked)} + id="wrapLines" + /> + + + setTripleQuotes(e.target.checked)} + id="tripleQuotes" + /> + + + {/* AUTOCOMPLETE SETTINGS */} + + {autoCompleteCheckboxes.map((opts) => ( + + opts.stateSetter(e.target.checked)} + /> + + ))} + + {/* AUTOCOMPLETE REFRESH SETTINGS */} + {(fields || indices || templates || dataStreams) && ( + <> + + + + + + + { + // Only refresh the currently selected settings. + props.refreshAutocompleteSettings({ + fields, + indices, + templates, + dataStreams, + }); + }} + > + + + + + )} + + ); +}; diff --git a/src/plugins/console/public/application/components/settings/settings_form_row.tsx b/src/plugins/console/public/application/components/settings/settings_form_row.tsx new file mode 100644 index 0000000000000..383eabfb93bd2 --- /dev/null +++ b/src/plugins/console/public/application/components/settings/settings_form_row.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; + +export interface DevToolsSettingsModalProps { + label: string; + children: React.ReactNode; +} + +export const SettingsFormRow = ({ label, children }: DevToolsSettingsModalProps) => { + return ( + + + + + {label} + + + + {children} + + + ); +}; diff --git a/src/plugins/console/public/application/components/settings/settings_group.tsx b/src/plugins/console/public/application/components/settings/settings_group.tsx new file mode 100644 index 0000000000000..d6feb8af1c90a --- /dev/null +++ b/src/plugins/console/public/application/components/settings/settings_group.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; + +import { EuiTitle, EuiSpacer, EuiText, EuiHorizontalRule } from '@elastic/eui'; + +export interface DevToolsSettingsModalProps { + title: string; + description?: string; +} + +export const SettingsGroup = ({ title, description }: DevToolsSettingsModalProps) => { + return ( + <> + + +

{title}

+
+ {description && ( + <> + + +

{description}

+
+ + )} + + + ); +}; diff --git a/src/plugins/controls/storybook/main.ts b/src/plugins/console/public/application/components/settings/types.ts similarity index 85% rename from src/plugins/controls/storybook/main.ts rename to src/plugins/console/public/application/components/settings/types.ts index e3b5b1c6c596a..f524e37124746 100644 --- a/src/plugins/controls/storybook/main.ts +++ b/src/plugins/console/public/application/components/settings/types.ts @@ -7,6 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { defaultConfig } from '@kbn/storybook'; - -module.exports = defaultConfig; +export type AutocompleteOptions = 'fields' | 'indices' | 'templates'; diff --git a/src/plugins/console/public/application/components/settings_modal.tsx b/src/plugins/console/public/application/components/settings_modal.tsx deleted file mode 100644 index 9b7740b4affdf..0000000000000 --- a/src/plugins/console/public/application/components/settings_modal.tsx +++ /dev/null @@ -1,391 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import _ from 'lodash'; -import React, { Fragment, useState, useCallback } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { - EuiButton, - EuiButtonEmpty, - EuiFieldNumber, - EuiFormRow, - EuiCheckboxGroup, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiSwitch, - EuiSuperSelect, -} from '@elastic/eui'; - -import { DevToolsSettings } from '../../services'; -import { unregisterCommands } from '../containers/editor/legacy/console_editor/keyboard_shortcuts'; -import type { SenseEditor } from '../models'; - -export type AutocompleteOptions = 'fields' | 'indices' | 'templates'; - -const onceTimeInterval = () => - i18n.translate('console.settingsPage.refreshInterval.onceTimeInterval', { - defaultMessage: 'Once, when console loads', - }); - -const everyNMinutesTimeInterval = (value: number) => - i18n.translate('console.settingsPage.refreshInterval.everyNMinutesTimeInterval', { - defaultMessage: 'Every {value} {value, plural, one {minute} other {minutes}}', - values: { value }, - }); - -const everyHourTimeInterval = () => - i18n.translate('console.settingsPage.refreshInterval.everyHourTimeInterval', { - defaultMessage: 'Every hour', - }); - -const PRESETS_IN_MINUTES = [0, 1, 10, 20, 60]; -const intervalOptions = PRESETS_IN_MINUTES.map((value) => ({ - value: (value * 60000).toString(), - inputDisplay: - value === 0 - ? onceTimeInterval() - : value === 60 - ? everyHourTimeInterval() - : everyNMinutesTimeInterval(value), -})); - -export interface DevToolsSettingsModalProps { - onSaveSettings: (newSettings: DevToolsSettings) => void; - onClose: () => void; - refreshAutocompleteSettings: (selectedSettings: DevToolsSettings['autocomplete']) => void; - settings: DevToolsSettings; - editorInstance: SenseEditor | null; -} - -export const DevToolsSettingsModal = (props: DevToolsSettingsModalProps) => { - const [fontSize, setFontSize] = useState(props.settings.fontSize); - const [wrapMode, setWrapMode] = useState(props.settings.wrapMode); - const [fields, setFields] = useState(props.settings.autocomplete.fields); - const [indices, setIndices] = useState(props.settings.autocomplete.indices); - const [templates, setTemplates] = useState(props.settings.autocomplete.templates); - const [dataStreams, setDataStreams] = useState(props.settings.autocomplete.dataStreams); - const [polling, setPolling] = useState(props.settings.polling); - const [pollInterval, setPollInterval] = useState(props.settings.pollInterval); - const [tripleQuotes, setTripleQuotes] = useState(props.settings.tripleQuotes); - const [isHistoryEnabled, setIsHistoryEnabled] = useState(props.settings.isHistoryEnabled); - const [isKeyboardShortcutsEnabled, setIsKeyboardShortcutsEnabled] = useState( - props.settings.isKeyboardShortcutsEnabled - ); - const [isAccessibilityOverlayEnabled, setIsAccessibilityOverlayEnabled] = useState( - props.settings.isAccessibilityOverlayEnabled - ); - - const autoCompleteCheckboxes = [ - { - id: 'fields', - label: i18n.translate('console.settingsPage.fieldsLabelText', { - defaultMessage: 'Fields', - }), - stateSetter: setFields, - }, - { - id: 'indices', - label: i18n.translate('console.settingsPage.indicesAndAliasesLabelText', { - defaultMessage: 'Indices and aliases', - }), - stateSetter: setIndices, - }, - { - id: 'templates', - label: i18n.translate('console.settingsPage.templatesLabelText', { - defaultMessage: 'Templates', - }), - stateSetter: setTemplates, - }, - { - id: 'dataStreams', - label: i18n.translate('console.settingsPage.dataStreamsLabelText', { - defaultMessage: 'Data streams', - }), - stateSetter: setDataStreams, - }, - ]; - - const checkboxIdToSelectedMap = { - fields, - indices, - templates, - dataStreams, - }; - - const onAutocompleteChange = (optionId: AutocompleteOptions) => { - const option = _.find(autoCompleteCheckboxes, (item) => item.id === optionId); - if (option) { - option.stateSetter(!checkboxIdToSelectedMap[optionId]); - } - }; - - function saveSettings() { - props.onSaveSettings({ - fontSize, - wrapMode, - autocomplete: { - fields, - indices, - templates, - dataStreams, - }, - polling, - pollInterval, - tripleQuotes, - isHistoryEnabled, - isKeyboardShortcutsEnabled, - isAccessibilityOverlayEnabled, - }); - } - - const onPollingIntervalChange = useCallback((value: string) => { - const sanitizedValue = parseInt(value, 10); - - setPolling(!!sanitizedValue); - setPollInterval(sanitizedValue); - }, []); - - const toggleKeyboardShortcuts = useCallback( - (isEnabled: boolean) => { - if (props.editorInstance) { - unregisterCommands(props.editorInstance); - } - - setIsKeyboardShortcutsEnabled(isEnabled); - }, - [props.editorInstance] - ); - - const toggleAccessibilityOverlay = useCallback( - (isEnabled: boolean) => setIsAccessibilityOverlayEnabled(isEnabled), - [] - ); - - const toggleSavingToHistory = useCallback( - (isEnabled: boolean) => setIsHistoryEnabled(isEnabled), - [] - ); - - // It only makes sense to show polling options if the user needs to fetch any data. - const pollingFields = - fields || indices || templates || dataStreams ? ( - - - } - helpText={ - - } - > - - - - { - // Only refresh the currently selected settings. - props.refreshAutocompleteSettings({ - fields, - indices, - templates, - dataStreams, - }); - }} - > - - - - ) : undefined; - - return ( - - - - - - - - - - } - > - { - const val = parseInt(e.target.value, 10); - if (!val) return; - setFontSize(val); - }} - /> - - - - - } - onChange={(e) => setWrapMode(e.target.checked)} - /> - - - - } - > - - } - onChange={(e) => setTripleQuotes(e.target.checked)} - /> - - - - } - > - - } - onChange={(e) => toggleSavingToHistory(e.target.checked)} - /> - - - - } - > - - } - onChange={(e) => toggleKeyboardShortcuts(e.target.checked)} - /> - - - - } - > - - } - onChange={(e) => toggleAccessibilityOverlay(e.target.checked)} - /> - - - - } - > - { - const { stateSetter, ...rest } = opts; - return rest; - })} - idToSelectedMap={checkboxIdToSelectedMap} - onChange={(e: unknown) => { - onAutocompleteChange(e as AutocompleteOptions); - }} - /> - - - {pollingFields} - - - - - - - - - - - - - ); -}; diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/index.ts b/src/plugins/console/public/application/components/shortcuts_popover/index.ts similarity index 84% rename from src/plugins/console/public/application/containers/editor/legacy/console_editor/index.ts rename to src/plugins/console/public/application/components/shortcuts_popover/index.ts index 3df103aa3be70..67b91a4e6dffb 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/index.ts +++ b/src/plugins/console/public/application/components/shortcuts_popover/index.ts @@ -7,5 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { Editor } from './editor'; -export { EditorOutput } from './editor_output'; +export { ShortcutsPopover } from './shortcuts_popover'; diff --git a/src/plugins/console/public/application/components/shortcuts_popover/keys.tsx b/src/plugins/console/public/application/components/shortcuts_popover/keys.tsx new file mode 100644 index 0000000000000..9a4a0329cbf5c --- /dev/null +++ b/src/plugins/console/public/application/components/shortcuts_popover/keys.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIcon } from '@elastic/eui'; + +export const KEYS = { + keyCtrlCmd: i18n.translate('console.shortcutKeys.keyCtrlCmd', { + defaultMessage: 'Ctrl/Cmd', + }), + keyEnter: i18n.translate('console.shortcutKeys.keyEnter', { + defaultMessage: 'Enter', + }), + keyAltOption: i18n.translate('console.shortcutKeys.keyAltOption', { + defaultMessage: 'Alt/Option', + }), + keyOption: i18n.translate('console.shortcutKeys.keyOption', { + defaultMessage: 'Option', + }), + keyShift: i18n.translate('console.shortcutKeys.keyShift', { + defaultMessage: 'Shift', + }), + keyTab: i18n.translate('console.shortcutKeys.keyTab', { + defaultMessage: 'Tab', + }), + keyEsc: i18n.translate('console.shortcutKeys.keyEsc', { + defaultMessage: 'Esc', + }), + keyUp: ( + + ), + keyDown: ( + + ), + keySlash: i18n.translate('console.shortcutKeys.keySlash', { + defaultMessage: '/', + }), + keySpace: i18n.translate('console.shortcutKeys.keySpace', { + defaultMessage: 'Space', + }), + keyI: i18n.translate('console.shortcutKeys.keyI', { + defaultMessage: 'I', + }), + keyO: i18n.translate('console.shortcutKeys.keyO', { + defaultMessage: 'O', + }), + keyL: i18n.translate('console.shortcutKeys.keyL', { + defaultMessage: 'L', + }), +}; diff --git a/src/plugins/console/public/application/components/shortcuts_popover/shortcut_line.tsx b/src/plugins/console/public/application/components/shortcuts_popover/shortcut_line.tsx new file mode 100644 index 0000000000000..a57c256ce3ce5 --- /dev/null +++ b/src/plugins/console/public/application/components/shortcuts_popover/shortcut_line.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { EuiCode, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; + +interface ShortcutLineFlexItemProps { + id: string; + description: string; + keys: any[]; + alternativeKeys?: any[]; +} + +const renderKeys = (keys: string[]) => { + return keys.map((key, index) => ( + + {index > 0 && ' + '} + {key} + + )); +}; + +export const ShortcutLineFlexItem = ({ + id, + description, + keys, + alternativeKeys, +}: ShortcutLineFlexItemProps) => { + return ( + + + + + {i18n.translate('console.shortcutDescription.' + id, { + defaultMessage: description, + })} + + + + + {renderKeys(keys)} + {alternativeKeys && ( + <> + + {' '} + {i18n.translate('console.shortcuts.alternativeKeysOrDivider', { + defaultMessage: 'or', + })}{' '} + + {renderKeys(alternativeKeys)} + + )} + + + + + ); +}; diff --git a/src/plugins/console/public/application/components/shortcuts_popover/shortcuts_popover.tsx b/src/plugins/console/public/application/components/shortcuts_popover/shortcuts_popover.tsx new file mode 100644 index 0000000000000..9ceaf13dc5dba --- /dev/null +++ b/src/plugins/console/public/application/components/shortcuts_popover/shortcuts_popover.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { EuiPopover, EuiTitle, EuiHorizontalRule, EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ShortcutLineFlexItem } from './shortcut_line'; +import { KEYS } from './keys'; + +interface ShortcutsPopoverProps { + button: any; + isOpen: boolean; + closePopover: () => void; +} + +export const ShortcutsPopover = ({ button, isOpen, closePopover }: ShortcutsPopoverProps) => { + return ( + + +
+ {i18n.translate('console.shortcuts.navigationShortcutsSubtitle', { + defaultMessage: 'Navigation shortcuts', + })} +
+
+ + + + + + + +
+ {i18n.translate('console.shortcuts.requestShortcutsSubtitle', { + defaultMessage: 'Request shortcuts', + })} +
+
+ + + + + + + + + + + +
+ {i18n.translate('console.shortcuts.autocompleteShortcutsSubtitle', { + defaultMessage: 'Autocomplete menu shortcuts', + })} +
+
+ + + + + + +
+ ); +}; diff --git a/src/plugins/console/public/application/components/top_nav_menu.tsx b/src/plugins/console/public/application/components/top_nav_menu.tsx index cddbe95f8f1b5..2309c01afc18a 100644 --- a/src/plugins/console/public/application/components/top_nav_menu.tsx +++ b/src/plugins/console/public/application/components/top_nav_menu.tsx @@ -9,6 +9,7 @@ import React, { FunctionComponent } from 'react'; import { EuiTabs, EuiTab } from '@elastic/eui'; +import { ConsoleTourStep, ConsoleTourStepProps } from './console_tour_step'; export interface TopNavMenuItem { id: string; @@ -16,28 +17,42 @@ export interface TopNavMenuItem { description: string; onClick: () => void; testId: string; + isSelected: boolean; + tourStep?: number; } interface Props { disabled?: boolean; items: TopNavMenuItem[]; + tourStepProps: ConsoleTourStepProps[]; } -export const TopNavMenu: FunctionComponent = ({ items, disabled }) => { +export const TopNavMenu: FunctionComponent = ({ items, disabled, tourStepProps }) => { return ( - + {items.map((item, idx) => { - return ( + const tab = ( {item.label} ); + + if (item.tourStep) { + return ( + + {tab} + + ); + } + + return tab; })} ); diff --git a/src/plugins/console/public/application/components/variables/index.ts b/src/plugins/console/public/application/components/variables/index.ts index 108befce39f51..8051ed2ddaa93 100644 --- a/src/plugins/console/public/application/components/variables/index.ts +++ b/src/plugins/console/public/application/components/variables/index.ts @@ -7,5 +7,25 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export * from './variables_flyout'; -export * from './utils'; +import React from 'react'; +import { withSuspense } from '@kbn/shared-ux-utility'; + +export { type Props } from './variables_editor'; +export { type DevToolsVariable } from './types'; + +/** + * The Lazily-loaded `VariablesEditorLazy` component. Consumers should use `React.Suspense` or + * the withSuspense` HOC to load this component. + */ +export const VariablesEditorLazy = React.lazy(() => + import('./variables_editor').then(({ VariablesEditor }) => ({ + default: VariablesEditor, + })) +); + +/** + * A `VariablesEditor` component that is wrapped by the `withSuspense` HOC. This component can + * be used directly by consumers and will load the `VariablesEditorLazy` component lazily with + * a predefined fallback and error boundary. + */ +export const VariablesEditor = withSuspense(VariablesEditorLazy); diff --git a/src/plugins/console/public/application/components/variables/types.ts b/src/plugins/console/public/application/components/variables/types.ts new file mode 100644 index 0000000000000..40a2ac86c361f --- /dev/null +++ b/src/plugins/console/public/application/components/variables/types.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export interface DevToolsVariable { + id: string; + name: string; + value: string; +} diff --git a/src/plugins/console/public/application/components/variables/utils.ts b/src/plugins/console/public/application/components/variables/utils.ts index 50664e0a99cf6..b636b9b0a6266 100644 --- a/src/plugins/console/public/application/components/variables/utils.ts +++ b/src/plugins/console/public/application/components/variables/utils.ts @@ -7,38 +7,18 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { v4 as uuidv4 } from 'uuid'; -import type { DevToolsVariable } from './variables_flyout'; +import { type DevToolsVariable } from './types'; -export const editVariable = ( - name: string, - value: string, - id: string, - variables: DevToolsVariable[] -) => { - const index = variables.findIndex((v) => v.id === id); - - if (index === -1) { - return variables; - } - - return [ - ...variables.slice(0, index), - { ...variables[index], [name]: value }, - ...variables.slice(index + 1), - ]; +export const editVariable = (newVariable: DevToolsVariable, variables: DevToolsVariable[]) => { + return variables.map((variable: DevToolsVariable) => { + return variable.id === newVariable.id ? newVariable : variable; + }); }; export const deleteVariable = (variables: DevToolsVariable[], id: string) => { return variables.filter((v) => v.id !== id); }; -export const generateEmptyVariableField = (): DevToolsVariable => ({ - id: uuidv4(), - name: '', - value: '', -}); - export const isValidVariableName = (name: string) => { /* * MUST avoid characters that get URL-encoded, because they'll result in unusable variable names. diff --git a/src/plugins/console/public/application/components/variables/variables_editor.tsx b/src/plugins/console/public/application/components/variables/variables_editor.tsx new file mode 100644 index 0000000000000..197fdec7f49c7 --- /dev/null +++ b/src/plugins/console/public/application/components/variables/variables_editor.tsx @@ -0,0 +1,255 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { useState, useCallback, useEffect, useRef } from 'react'; +import { BehaviorSubject } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { + EuiTitle, + EuiButton, + EuiBasicTable, + EuiButtonIcon, + EuiSpacer, + EuiText, + EuiCode, + useGeneratedHtmlId, + EuiConfirmModal, + type EuiBasicTableColumn, +} from '@elastic/eui'; + +import { VariableEditorForm } from './variables_editor_form'; +import * as utils from './utils'; +import { type DevToolsVariable } from './types'; + +export interface Props { + onSaveVariables: (newVariables: DevToolsVariable[]) => void; + variables: []; +} + +export const VariablesEditor = (props: Props) => { + const isMounted = useRef(false); + const [isAddingVariable, setIsAddingVariable] = useState(false); + const [deleteModalForVariable, setDeleteModalForVariable] = useState(null); + const [variables, setVariables] = useState(props.variables); + const deleteModalTitleId = useGeneratedHtmlId(); + + // Use a ref to persist the BehaviorSubject across renders + const itemIdToExpandedRowMap$ = useRef(new BehaviorSubject>({})); + // Subscribe to the BehaviorSubject and update local state on change + const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState< + Record + >({}); + // Clear the expanded row map and dispose all the expanded rows + const collapseExpandedRows = () => itemIdToExpandedRowMap$.current.next({}); + + // Subscribe to the BehaviorSubject on mount + useEffect(() => { + const subscription = itemIdToExpandedRowMap$.current.subscribe(setItemIdToExpandedRowMap); + return () => subscription.unsubscribe(); + }, []); + + // Always save variables when they change + useEffect(() => { + if (isMounted.current) { + props.onSaveVariables(variables); + } else { + isMounted.current = true; + } + }, [variables, props]); + + const toggleDetails = (variableId: string) => { + const currentMap = itemIdToExpandedRowMap$.current.getValue(); + let itemIdToExpandedRowMapValues = { ...currentMap }; + + if (itemIdToExpandedRowMapValues[variableId]) { + delete itemIdToExpandedRowMapValues[variableId]; + } else { + // Always close the add variable form when editing a variable + setIsAddingVariable(false); + // We only allow one expanded row at a time + itemIdToExpandedRowMapValues = {}; + itemIdToExpandedRowMapValues[variableId] = ( + { + const updatedVariables = utils.editVariable(data, variables); + setVariables(updatedVariables); + collapseExpandedRows(); + }} + onCancel={() => { + collapseExpandedRows(); + }} + defaultValue={variables.find((v) => v.id === variableId)} + /> + ); + } + + // Update the BehaviorSubject with the new state + itemIdToExpandedRowMap$.current.next(itemIdToExpandedRowMapValues); + }; + + const deleteVariable = useCallback( + (id: string) => { + const updatedVariables = utils.deleteVariable(variables, id); + setVariables(updatedVariables); + setDeleteModalForVariable(null); + }, + [variables, setDeleteModalForVariable] + ); + + const onAddVariable = (data: DevToolsVariable) => { + setVariables((v: DevToolsVariable[]) => [...v, data]); + setIsAddingVariable(false); + }; + + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('console.variablesPage.variablesTable.columns.variableHeader', { + defaultMessage: 'Variable name', + }), + 'data-test-subj': 'variableNameCell', + render: (name: string) => { + return {`\$\{${name}\}`}; + }, + }, + { + field: 'value', + name: i18n.translate('console.variablesPage.variablesTable.columns.valueHeader', { + defaultMessage: 'Value', + }), + 'data-test-subj': 'variableValueCell', + render: (value: string) => {value}, + }, + { + field: 'id', + name: '', + width: '40px', + isExpander: true, + render: (id: string, variable: DevToolsVariable) => { + const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; + + return ( + toggleDetails(id)} + data-test-subj="variableEditButton" + /> + ); + }, + }, + { + field: 'id', + name: '', + width: '40px', + render: (id: string, variable: DevToolsVariable) => ( + setDeleteModalForVariable(id)} + data-test-subj="variablesRemoveButton" + /> + ), + }, + ]; + + return ( + <> + +

+ +

+
+ + +

+ +

+
+ + + + + {isAddingVariable && ( + setIsAddingVariable(false)} /> + )} + + + +
+ { + setIsAddingVariable(true); + collapseExpandedRows(); + }} + disabled={isAddingVariable} + > + + +
+ + {deleteModalForVariable && ( + setDeleteModalForVariable(null)} + onConfirm={() => deleteVariable(deleteModalForVariable)} + cancelButtonText={i18n.translate('console.variablesPage.deleteModal.cancelButtonText', { + defaultMessage: 'Cancel', + })} + confirmButtonText={i18n.translate('console.variablesPage.deleteModal.confirmButtonText', { + defaultMessage: 'Delete variable', + })} + buttonColor="danger" + > +

+ +

+
+ )} + + ); +}; diff --git a/src/plugins/console/public/application/components/variables/variables_editor_form.tsx b/src/plugins/console/public/application/components/variables/variables_editor_form.tsx new file mode 100644 index 0000000000000..446aaab0d4e94 --- /dev/null +++ b/src/plugins/console/public/application/components/variables/variables_editor_form.tsx @@ -0,0 +1,184 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { v4 as uuidv4 } from 'uuid'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiSpacer, + EuiPanel, + EuiButtonEmpty, +} from '@elastic/eui'; + +import { + useForm, + Form, + UseField, + TextField, + FieldConfig, + fieldValidators, + FormConfig, + ValidationFuncArg, +} from '../../../shared_imports'; + +import { type DevToolsVariable } from './types'; +import { isValidVariableName } from './utils'; + +export interface VariableEditorFormProps { + onSubmit: (data: DevToolsVariable) => void; + onCancel: () => void; + defaultValue?: DevToolsVariable; + title?: string; +} + +const fieldsConfig: Record = { + variableName: { + label: i18n.translate('console.variablesPage.form.variableNameFieldLabel', { + defaultMessage: 'Variable name', + }), + validations: [ + { + validator: ({ value }: ValidationFuncArg) => { + if (value.trim() === '') { + return { + message: i18n.translate('console.variablesPage.form.variableNameRequiredLabel', { + defaultMessage: 'This is a required field', + }), + }; + } + + if (!isValidVariableName(value)) { + return { + message: i18n.translate('console.variablesPage.form.variableNameInvalidLabel', { + defaultMessage: 'Only letters, numbers and underscores are allowed', + }), + }; + } + }, + }, + ], + }, + value: { + label: i18n.translate('console.variablesPage.form.valueFieldLabel', { + defaultMessage: 'Value', + }), + validations: [ + { + validator: fieldValidators.emptyField( + i18n.translate('console.variablesPage.form.valueRequiredLabel', { + defaultMessage: 'Value is required', + }) + ), + }, + ], + }, +}; + +export const VariableEditorForm = (props: VariableEditorFormProps) => { + const onSubmit: FormConfig['onSubmit'] = async (data, isValid) => { + if (isValid) { + props.onSubmit({ + ...props.defaultValue, + ...data, + ...(props.defaultValue ? {} : { id: uuidv4() }), + } as DevToolsVariable); + } + }; + + const { form } = useForm({ onSubmit, defaultValue: props.defaultValue }); + + return ( + <> + + +

+ {props.title ?? ( + + )} +

+
+ + +
+ + + + + + + + + props.onCancel()}> + + + + + + + + + + + +
+ + ); +}; diff --git a/src/plugins/console/public/application/components/variables/variables_flyout.tsx b/src/plugins/console/public/application/components/variables/variables_flyout.tsx deleted file mode 100644 index 9211d2a7e524f..0000000000000 --- a/src/plugins/console/public/application/components/variables/variables_flyout.tsx +++ /dev/null @@ -1,215 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React, { useState, useCallback, ChangeEvent, FormEvent } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { - EuiFlyout, - EuiFlyoutHeader, - EuiTitle, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiButtonEmpty, - EuiBasicTable, - EuiFieldText, - useGeneratedHtmlId, - EuiForm, - EuiFormRow, - EuiButtonIcon, - EuiSpacer, - EuiText, - type EuiBasicTableColumn, -} from '@elastic/eui'; - -import * as utils from './utils'; - -export interface DevToolsVariablesFlyoutProps { - onClose: () => void; - onSaveVariables: (newVariables: DevToolsVariable[]) => void; - variables: []; -} - -export interface DevToolsVariable { - id: string; - name: string; - value: string; -} - -export const DevToolsVariablesFlyout = (props: DevToolsVariablesFlyoutProps) => { - const [variables, setVariables] = useState(props.variables); - const formId = useGeneratedHtmlId({ prefix: '__console' }); - - const addNewVariable = useCallback(() => { - setVariables((v) => [...v, utils.generateEmptyVariableField()]); - }, []); - - const deleteVariable = useCallback( - (id: string) => { - const updatedVariables = utils.deleteVariable(variables, id); - setVariables(updatedVariables); - }, - [variables] - ); - - const onSubmit = useCallback( - (e: FormEvent) => { - e.preventDefault(); - props.onSaveVariables(variables.filter(({ name, value }) => name.trim() && value)); - }, - [props, variables] - ); - - const onChange = useCallback( - (event: ChangeEvent, id: string) => { - const { name, value } = event.target; - const editedVariables = utils.editVariable(name, value, id, variables); - setVariables(editedVariables); - }, - [variables] - ); - - const columns: Array> = [ - { - field: 'name', - name: i18n.translate('console.variablesPage.variablesTable.columns.variableHeader', { - defaultMessage: 'Variable name', - }), - render: (name, { id }) => { - const isInvalid = !utils.isValidVariableName(name); - return ( - , - ]} - fullWidth={true} - css={{ flexGrow: 1 }} - > - onChange(e, id)} - isInvalid={isInvalid} - fullWidth={true} - aria-label={i18n.translate( - 'console.variablesPage.variablesTable.variableInput.ariaLabel', - { - defaultMessage: 'Variable name', - } - )} - /> - - ); - }, - }, - { - field: 'value', - name: i18n.translate('console.variablesPage.variablesTable.columns.valueHeader', { - defaultMessage: 'Value', - }), - render: (value, { id }) => ( - onChange(e, id)} - value={value} - aria-label={i18n.translate('console.variablesPage.variablesTable.valueInput.ariaLabel', { - defaultMessage: 'Variable value', - })} - /> - ), - }, - { - field: 'id', - name: '', - width: '5%', - render: (id: string) => ( - deleteVariable(id)} - data-test-subj="variablesRemoveButton" - /> - ), - }, - ]; - - return ( - - - -

- -

-
- - -

- - - - ), - }} - /> -

-
-
- - - - - - - - - - - - - - - - - - - - - - -
- ); -}; diff --git a/src/plugins/console/public/application/components/welcome_panel.tsx b/src/plugins/console/public/application/components/welcome_panel.tsx deleted file mode 100644 index 967d173821e65..0000000000000 --- a/src/plugins/console/public/application/components/welcome_panel.tsx +++ /dev/null @@ -1,167 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; - -import { - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiTitle, - EuiButton, - EuiText, - EuiFlyoutFooter, - EuiCode, -} from '@elastic/eui'; -import EditorExample from './editor_example'; -import * as examples from '../../../common/constants/welcome_panel'; - -interface Props { - onDismiss: () => void; -} - -export function WelcomePanel(props: Props) { - return ( - - - -

- -

-
-
- - -

- -

- -

- kbn:, - }} - /> -

- -

- -

-

- -

- - -

- -

-

- #, - doubleSlash: //, - slashAsterisk: /*, - asteriskSlash: */, - }} - /> -

- -

- -

-

- ${variableName}, - }} - /> -

- -
    -
  1. - Variables, - }} - /> -
  2. -
  3. - -
  4. -
- -
-
- - - - - -
- ); -} diff --git a/src/plugins/console/public/application/containers/config/config.tsx b/src/plugins/console/public/application/containers/config/config.tsx new file mode 100644 index 0000000000000..503fdbd9c7354 --- /dev/null +++ b/src/plugins/console/public/application/containers/config/config.tsx @@ -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 + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui'; + +import { Settings } from './settings'; +import { Variables } from './variables'; + +export interface Props { + isVerticalLayout: boolean; +} + +export function Config({ isVerticalLayout }: Props) { + return ( + + + + + + + + + + + + + ); +} diff --git a/src/plugins/console/public/application/containers/editor/utilities/index.ts b/src/plugins/console/public/application/containers/config/index.ts similarity index 93% rename from src/plugins/console/public/application/containers/editor/utilities/index.ts rename to src/plugins/console/public/application/containers/config/index.ts index 7561f02006235..b582701ab4481 100644 --- a/src/plugins/console/public/application/containers/editor/utilities/index.ts +++ b/src/plugins/console/public/application/containers/config/index.ts @@ -7,4 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export * from './output_data'; +export { Config } from './config'; diff --git a/src/plugins/console/public/application/containers/settings.tsx b/src/plugins/console/public/application/containers/config/settings.tsx similarity index 88% rename from src/plugins/console/public/application/containers/settings.tsx rename to src/plugins/console/public/application/containers/config/settings.tsx index 2c952f4c5d7f9..d5e10f4d2c337 100644 --- a/src/plugins/console/public/application/containers/settings.tsx +++ b/src/plugins/console/public/application/containers/config/settings.tsx @@ -9,11 +9,10 @@ import React from 'react'; -import { AutocompleteOptions, DevToolsSettingsModal } from '../components'; +import { AutocompleteOptions, SettingsEditor } from '../../components/settings'; -import { useServicesContext, useEditorActionContext } from '../contexts'; -import { DevToolsSettings, Settings as SettingsService } from '../../services'; -import type { SenseEditor } from '../models'; +import { useServicesContext, useEditorActionContext } from '../../contexts'; +import { DevToolsSettings, Settings as SettingsService } from '../../../services'; const getAutocompleteDiff = ( newSettings: DevToolsSettings, @@ -25,12 +24,7 @@ const getAutocompleteDiff = ( }) as AutocompleteOptions[]; }; -export interface Props { - onClose: () => void; - editorInstance: SenseEditor | null; -} - -export function Settings({ onClose, editorInstance }: Props) { +export function Settings() { const { services: { settings, autocompleteInfo }, } = useServicesContext(); @@ -92,18 +86,15 @@ export function Settings({ onClose, editorInstance }: Props) { type: 'updateSettings', payload: newSettings, }); - onClose(); }; return ( - refreshAutocompleteSettings(settings, selectedSettings) } settings={settings.toJSON()} - editorInstance={editorInstance} /> ); } diff --git a/src/plugins/console/public/application/containers/variables.tsx b/src/plugins/console/public/application/containers/config/variables.tsx similarity index 66% rename from src/plugins/console/public/application/containers/variables.tsx rename to src/plugins/console/public/application/containers/config/variables.tsx index 54e191d04a9de..32b9615f529aa 100644 --- a/src/plugins/console/public/application/containers/variables.tsx +++ b/src/plugins/console/public/application/containers/config/variables.tsx @@ -8,27 +8,22 @@ */ import React from 'react'; -import { DevToolsVariablesFlyout, DevToolsVariable } from '../components'; -import { useServicesContext } from '../contexts'; -import { StorageKeys } from '../../services'; -import { DEFAULT_VARIABLES } from '../../../common/constants'; +import { type DevToolsVariable, VariablesEditor } from '../../components/variables'; +import { useServicesContext } from '../../contexts'; +import { StorageKeys } from '../../../services'; +import { DEFAULT_VARIABLES } from '../../../../common/constants'; -interface VariablesProps { - onClose: () => void; -} - -export function Variables({ onClose }: VariablesProps) { +export function Variables() { const { services: { storage }, } = useServicesContext(); const onSaveVariables = (newVariables: DevToolsVariable[]) => { storage.set(StorageKeys.VARIABLES, newVariables); - onClose(); }; + return ( - diff --git a/src/plugins/console/public/application/containers/console_history/console_history.tsx b/src/plugins/console/public/application/containers/console_history/console_history.tsx deleted file mode 100644 index 220e0b6a998aa..0000000000000 --- a/src/plugins/console/public/application/containers/console_history/console_history.tsx +++ /dev/null @@ -1,242 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; -import { i18n } from '@kbn/i18n'; -import { memoize } from 'lodash'; -import moment from 'moment'; -import { - keys, - EuiSpacer, - EuiIcon, - EuiTitle, - EuiFlexItem, - EuiFlexGroup, - EuiButtonEmpty, - EuiButton, -} from '@elastic/eui'; - -import { useServicesContext } from '../../contexts'; -import { HistoryViewer } from './history_viewer'; -import { HistoryViewer as HistoryViewerMonaco } from './history_viewer_monaco'; -import { useEditorReadContext } from '../../contexts/editor_context'; -import { useRestoreRequestFromHistory } from '../../hooks'; - -interface Props { - close: () => void; -} - -const CHILD_ELEMENT_PREFIX = 'historyReq'; - -export function ConsoleHistory({ close }: Props) { - const { - services: { history }, - config: { isMonacoEnabled }, - } = useServicesContext(); - - const { settings: readOnlySettings } = useEditorReadContext(); - - const [requests, setPastRequests] = useState(history.getHistory()); - - const clearHistory = useCallback(() => { - history.clearHistory(); - setPastRequests(history.getHistory()); - }, [history]); - - const listRef = useRef(null); - - const [viewingReq, setViewingReq] = useState(null); - const [selectedIndex, setSelectedIndex] = useState(0); - const selectedReq = useRef(null); - - const describeReq = useMemo(() => { - const _describeReq = (req: { endpoint: string; time: string }) => { - const endpoint = req.endpoint; - const date = moment(req.time); - - let formattedDate = date.format('MMM D'); - if (date.diff(moment(), 'days') > -7) { - formattedDate = date.fromNow(); - } - - return `${endpoint} (${formattedDate})`; - }; - - (_describeReq as any).cache = new WeakMap(); - - return memoize(_describeReq); - }, []); - - const scrollIntoView = useCallback((idx: number) => { - const activeDescendant = listRef.current!.querySelector(`#${CHILD_ELEMENT_PREFIX}${idx}`); - if (activeDescendant) { - activeDescendant.scrollIntoView(); - } - }, []); - - const initialize = useCallback(() => { - const nextSelectedIndex = 0; - (describeReq as any).cache = new WeakMap(); - setViewingReq(requests[nextSelectedIndex]); - selectedReq.current = requests[nextSelectedIndex]; - setSelectedIndex(nextSelectedIndex); - scrollIntoView(nextSelectedIndex); - }, [describeReq, requests, scrollIntoView]); - - const clear = () => { - clearHistory(); - initialize(); - }; - - const restoreRequestFromHistory = useRestoreRequestFromHistory(isMonacoEnabled); - - useEffect(() => { - initialize(); - }, [initialize]); - - useEffect(() => { - const done = history.change(setPastRequests); - return () => done(); - }, [history]); - - /* eslint-disable jsx-a11y/no-noninteractive-element-to-interactive-role,jsx-a11y/click-events-have-key-events */ - return ( - <> -
- -

{i18n.translate('console.historyPage.pageTitle', { defaultMessage: 'History' })}

-
- -
-
    { - if (ev.key === keys.ENTER) { - restoreRequestFromHistory(selectedReq.current); - return; - } - - let currentIdx = selectedIndex; - - if (ev.key === keys.ARROW_UP) { - ev.preventDefault(); - --currentIdx; - } else if (ev.key === keys.ARROW_DOWN) { - ev.preventDefault(); - ++currentIdx; - } - - const nextSelectedIndex = Math.min(Math.max(0, currentIdx), requests.length - 1); - - setViewingReq(requests[nextSelectedIndex]); - selectedReq.current = requests[nextSelectedIndex]; - setSelectedIndex(nextSelectedIndex); - scrollIntoView(nextSelectedIndex); - }} - role="listbox" - className="list-group conHistory__reqs" - tabIndex={0} - aria-activedescendant={`${CHILD_ELEMENT_PREFIX}${selectedIndex}`} - aria-label={i18n.translate('console.historyPage.requestListAriaLabel', { - defaultMessage: 'History of sent requests', - })} - > - {requests.map((req, idx) => { - const reqDescription = describeReq(req); - const isSelected = viewingReq === req; - return ( - // Ignore a11y issues on li's -
  • { - setViewingReq(req); - selectedReq.current = req; - setSelectedIndex(idx); - }} - role="option" - onMouseEnter={() => setViewingReq(req)} - onMouseLeave={() => setViewingReq(selectedReq.current)} - onDoubleClick={() => restoreRequestFromHistory(selectedReq.current)} - aria-label={i18n.translate('console.historyPage.itemOfRequestListAriaLabel', { - defaultMessage: 'Request: {historyItem}', - values: { historyItem: reqDescription }, - })} - aria-selected={isSelected} - > - {reqDescription} - - - -
  • - ); - })} -
- -
- - {isMonacoEnabled ? ( - - ) : ( - - )} -
- - - - - - clear()} - > - {i18n.translate('console.historyPage.clearHistoryButtonLabel', { - defaultMessage: 'Clear', - })} - - - - - - - close()} - > - {i18n.translate('console.historyPage.closehistoryButtonLabel', { - defaultMessage: 'Close', - })} - - - - - restoreRequestFromHistory(selectedReq.current)} - > - {i18n.translate('console.historyPage.applyHistoryButtonLabel', { - defaultMessage: 'Apply', - })} - - - - - -
- - - ); -} diff --git a/src/plugins/console/public/application/containers/console_history/history_viewer.tsx b/src/plugins/console/public/application/containers/console_history/history_viewer.tsx deleted file mode 100644 index 92d58e557cd89..0000000000000 --- a/src/plugins/console/public/application/containers/console_history/history_viewer.tsx +++ /dev/null @@ -1,61 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React, { useEffect, useRef } from 'react'; -import { i18n } from '@kbn/i18n'; - -import { DevToolsSettings } from '../../../services'; -import { subscribeResizeChecker } from '../editor/legacy/subscribe_console_resize_checker'; - -import * as InputMode from '../../models/legacy_core_editor/mode/input'; -const inputMode = new InputMode.Mode(); -import * as editor from '../../models/legacy_core_editor'; -import { applyCurrentSettings } from '../editor/legacy/console_editor/apply_editor_settings'; -import { formatRequestBodyDoc } from '../../../lib/utils'; - -interface Props { - settings: DevToolsSettings; - req: { method: string; endpoint: string; data: string; time: string } | null; -} - -export function HistoryViewer({ settings, req }: Props) { - const divRef = useRef(null); - const viewerRef = useRef(null); - - useEffect(() => { - const viewer = editor.createReadOnlyAceEditor(divRef.current!); - viewerRef.current = viewer; - const unsubscribe = subscribeResizeChecker(divRef.current!, viewer); - return () => unsubscribe(); - }, []); - - useEffect(() => { - applyCurrentSettings(viewerRef.current!, settings); - }, [settings]); - - if (viewerRef.current) { - const { current: viewer } = viewerRef; - if (req) { - const indent = true; - const formattedData = req.data ? formatRequestBodyDoc([req.data], indent).data : ''; - const s = req.method + ' ' + req.endpoint + '\n' + formattedData; - viewer.update(s, inputMode); - viewer.clearSelection(); - } else { - viewer.update( - i18n.translate('console.historyPage.noHistoryTextMessage', { - defaultMessage: 'No history available', - }), - inputMode - ); - } - } - - return
; -} diff --git a/src/plugins/console/public/application/containers/editor/monaco/components/context_menu/context_menu.tsx b/src/plugins/console/public/application/containers/editor/components/context_menu/context_menu.tsx similarity index 90% rename from src/plugins/console/public/application/containers/editor/monaco/components/context_menu/context_menu.tsx rename to src/plugins/console/public/application/containers/editor/components/context_menu/context_menu.tsx index 34001018900b5..3860f0b7bc704 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/components/context_menu/context_menu.tsx +++ b/src/plugins/console/public/application/containers/editor/components/context_menu/context_menu.tsx @@ -9,7 +9,7 @@ import React, { useState } from 'react'; import { - EuiIcon, + EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover, @@ -18,16 +18,17 @@ import { EuiLink, EuiLoadingSpinner, } from '@elastic/eui'; +import { css } from '@emotion/react'; import { NotificationsSetup } from '@kbn/core/public'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { LanguageSelectorModal } from './language_selector_modal'; -import { convertRequestToLanguage } from '../../../../../../services'; +import { convertRequestToLanguage } from '../../../../../services'; import type { EditorRequest } from '../../types'; -import { useServicesContext } from '../../../../../contexts'; -import { StorageKeys } from '../../../../../../services'; -import { DEFAULT_LANGUAGE, AVAILABLE_LANGUAGES } from '../../../../../../../common/constants'; +import { useServicesContext } from '../../../../contexts'; +import { StorageKeys } from '../../../../../services'; +import { DEFAULT_LANGUAGE, AVAILABLE_LANGUAGES } from '../../../../../../common/constants'; interface Props { getRequests: () => Promise; @@ -36,6 +37,20 @@ interface Props { notifications: NotificationsSetup; } +const styles = { + // Remove the default underline on hover for the context menu items since it + // will also be applied to the language selector button, and apply it only to + // the text in the context menu item. + button: css` + &:hover { + text-decoration: none !important; + .languageSelector { + text-decoration: underline; + } + } + `, +}; + const DELAY_FOR_HIDING_SPINNER = 500; const getLanguageLabelByValue = (value: string) => { @@ -158,15 +173,15 @@ export const ContextMenu = ({ }; const button = ( - setIsPopoverOpen((prev) => !prev)} data-test-subj="toggleConsoleMenu" aria-label={i18n.translate('console.requestOptionsButtonAriaLabel', { defaultMessage: 'Request options', })} - > - - + iconType="boxesVertical" + iconSize="s" + /> ); const items = [ @@ -187,10 +202,11 @@ export const ContextMenu = ({ onCopyAsSubmit(); }} icon="copyClipboard" + css={styles.button} > - + void; diff --git a/src/plugins/console/public/application/containers/editor/monaco/components/index.ts b/src/plugins/console/public/application/containers/editor/components/index.ts similarity index 100% rename from src/plugins/console/public/application/containers/editor/monaco/components/index.ts rename to src/plugins/console/public/application/containers/editor/components/index.ts diff --git a/src/plugins/console/public/application/containers/editor/editor.tsx b/src/plugins/console/public/application/containers/editor/editor.tsx index 3eff2d97b3499..c999deee78637 100644 --- a/src/plugins/console/public/application/containers/editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/editor.tsx @@ -7,95 +7,283 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useCallback, memo, useEffect, useState } from 'react'; +import React, { useRef, useCallback, memo, useEffect, useState } from 'react'; import { debounce } from 'lodash'; -import { EuiProgress } from '@elastic/eui'; +import { + EuiProgress, + EuiSplitPanel, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiResizableContainer, +} from '@elastic/eui'; +import { euiThemeVars } from '@kbn/ui-theme'; -import { EditorContentSpinner } from '../../components'; -import { Panel, PanelsContainer } from '..'; -import { Editor as EditorUI, EditorOutput } from './legacy/console_editor'; +import { i18n } from '@kbn/i18n'; +import { TextObject } from '../../../../common/text_object'; + +import { + EditorContentSpinner, + OutputPanelEmptyState, + NetworkRequestStatusBar, +} from '../../components'; import { getAutocompleteInfo, StorageKeys } from '../../../services'; -import { useEditorReadContext, useServicesContext, useRequestReadContext } from '../../contexts'; -import type { SenseEditor } from '../../models'; -import { MonacoEditor, MonacoEditorOutput } from './monaco'; +import { + useEditorReadContext, + useServicesContext, + useRequestReadContext, + useRequestActionContext, + useEditorActionContext, +} from '../../contexts'; +import { MonacoEditor } from './monaco_editor'; +import { MonacoEditorOutput } from './monaco_editor_output'; +import { getResponseWithMostSevereStatusCode } from '../../../lib/utils'; -const INITIAL_PANEL_WIDTH = 50; -const PANEL_MIN_WIDTH = '100px'; +const INITIAL_PANEL_SIZE = 50; +const PANEL_MIN_SIZE = '20%'; +const DEBOUNCE_DELAY = 500; interface Props { loading: boolean; - setEditorInstance: (instance: SenseEditor) => void; + isVerticalLayout: boolean; + inputEditorValue: string; + setInputEditorValue: (value: string) => void; } -export const Editor = memo(({ loading, setEditorInstance }: Props) => { - const { - services: { storage }, - config: { isMonacoEnabled } = {}, - } = useServicesContext(); - - const { currentTextObject } = useEditorReadContext(); - const { requestInFlight } = useRequestReadContext(); - - const [fetchingMappings, setFetchingMappings] = useState(false); - - useEffect(() => { - const subscription = getAutocompleteInfo().mapping.isLoading$.subscribe(setFetchingMappings); - return () => { - subscription.unsubscribe(); - }; - }, []); - - const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [ - INITIAL_PANEL_WIDTH, - INITIAL_PANEL_WIDTH, - ]); - - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - const onPanelWidthChange = useCallback( - debounce((widths: number[]) => { - storage.set(StorageKeys.WIDTH, widths); - }, 300), - [] - ); - - if (!currentTextObject) return null; - - return ( - <> - {requestInFlight || fetchingMappings ? ( -
- -
- ) : null} - - - {loading ? ( - - ) : isMonacoEnabled ? ( - - ) : ( - - )} - - { + const { + services: { storage, objectStorageClient }, + } = useServicesContext(); + + const editorValueRef = useRef(null); + const { currentTextObject } = useEditorReadContext(); + const { + requestInFlight, + lastResult: { data: requestData, error: requestError }, + } = useRequestReadContext(); + + const dispatch = useRequestActionContext(); + const editorDispatch = useEditorActionContext(); + + const [fetchingAutocompleteEntities, setFetchingAutocompleteEntities] = useState(false); + + useEffect(() => { + const debouncedSetFechingAutocompleteEntities = debounce( + setFetchingAutocompleteEntities, + DEBOUNCE_DELAY + ); + const subscription = getAutocompleteInfo().isLoading$.subscribe( + debouncedSetFechingAutocompleteEntities + ); + + return () => { + subscription.unsubscribe(); + debouncedSetFechingAutocompleteEntities.cancel(); + }; + }, []); + + const [firstPanelSize, secondPanelSize] = storage.get(StorageKeys.SIZE, [ + INITIAL_PANEL_SIZE, + INITIAL_PANEL_SIZE, + ]); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const onPanelSizeChange = useCallback( + debounce((sizes) => { + storage.set(StorageKeys.SIZE, Object.values(sizes)); + }, 300), + [] + ); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + const debouncedUpdateLocalStorageValue = useCallback( + debounce((textObject: TextObject) => { + editorValueRef.current = textObject; + objectStorageClient.text.update(textObject); + }, DEBOUNCE_DELAY), + [] + ); + + useEffect(() => { + return () => { + editorDispatch({ + type: 'setCurrentTextObject', + payload: editorValueRef.current!, + }); + }; + }, [editorDispatch]); + + // Always keep the localstorage in sync with the value in the editor + // to avoid losing the text object when the user navigates away from the shell + useEffect(() => { + // Only update when its not empty, this is to avoid setting the localstorage value + // to an empty string that will then be replaced by the example request. + if (inputEditorValue !== '') { + const textObject = { + ...currentTextObject, + text: inputEditorValue, + updatedAt: Date.now(), + } as TextObject; + + debouncedUpdateLocalStorageValue(textObject); + } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [inputEditorValue, debouncedUpdateLocalStorageValue]); + + const data = getResponseWithMostSevereStatusCode(requestData) ?? requestError; + const isLoading = loading || requestInFlight; + + if (!currentTextObject) return null; + + return ( + <> + {fetchingAutocompleteEntities ? ( +
+ +
+ ) : null} + onPanelSizeChange(sizes)} + data-test-subj="consoleEditorContainer" > - {loading ? ( - - ) : isMonacoEnabled ? ( - - ) : ( - + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + {loading ? ( + + ) : ( + + )} + + + {!loading && ( + + setInputEditorValue('')} + > + {i18n.translate('console.editor.clearConsoleInputButton', { + defaultMessage: 'Clear this input', + })} + + + )} + + + + + + + + + {data ? ( + + ) : isLoading ? ( + + ) : ( + + )} + + + {(data || isLoading) && ( + + + + dispatch({ type: 'cleanRequest', payload: undefined })} + > + {i18n.translate('console.editor.clearConsoleOutputButton', { + defaultMessage: 'Clear this output', + })} + + + + + + + + + )} + + + )} -
-
- - ); -}); + + + ); + } +); diff --git a/src/plugins/console/public/application/containers/editor/monaco/hooks/index.ts b/src/plugins/console/public/application/containers/editor/hooks/index.ts similarity index 100% rename from src/plugins/console/public/application/containers/editor/monaco/hooks/index.ts rename to src/plugins/console/public/application/containers/editor/hooks/index.ts diff --git a/src/plugins/console/public/application/containers/editor/monaco/hooks/use_register_keyboard_commands.ts b/src/plugins/console/public/application/containers/editor/hooks/use_register_keyboard_commands.ts similarity index 100% rename from src/plugins/console/public/application/containers/editor/monaco/hooks/use_register_keyboard_commands.ts rename to src/plugins/console/public/application/containers/editor/hooks/use_register_keyboard_commands.ts diff --git a/src/plugins/console/public/application/containers/editor/monaco/hooks/use_resize_checker_utils.ts b/src/plugins/console/public/application/containers/editor/hooks/use_resize_checker_utils.ts similarity index 100% rename from src/plugins/console/public/application/containers/editor/monaco/hooks/use_resize_checker_utils.ts rename to src/plugins/console/public/application/containers/editor/hooks/use_resize_checker_utils.ts diff --git a/src/plugins/console/public/application/containers/editor/monaco/hooks/use_set_initial_value.ts b/src/plugins/console/public/application/containers/editor/hooks/use_set_initial_value.ts similarity index 91% rename from src/plugins/console/public/application/containers/editor/monaco/hooks/use_set_initial_value.ts rename to src/plugins/console/public/application/containers/editor/hooks/use_set_initial_value.ts index 41a3b77a105cd..961ea586bc291 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/hooks/use_set_initial_value.ts +++ b/src/plugins/console/public/application/containers/editor/hooks/use_set_initial_value.ts @@ -13,7 +13,7 @@ import { IToasts } from '@kbn/core-notifications-browser'; import { decompressFromEncodedURIComponent } from 'lz-string'; import { i18n } from '@kbn/i18n'; import { useEffect } from 'react'; -import { DEFAULT_INPUT_VALUE } from '../../../../../../common/constants'; +import { DEFAULT_INPUT_VALUE } from '../../../../../common/constants'; interface QueryParams { load_from: string; @@ -21,7 +21,7 @@ interface QueryParams { interface SetInitialValueParams { /** The text value that is initially in the console editor. */ - initialTextValue?: string; + localStorageValue?: string; /** The function that sets the state of the value in the console editor. */ setValue: (value: string) => void; /** The toasts service. */ @@ -45,7 +45,7 @@ export const readLoadFromParam = () => { * @param params The {@link SetInitialValueParams} to use. */ export const useSetInitialValue = (params: SetInitialValueParams) => { - const { initialTextValue, setValue, toasts } = params; + const { localStorageValue, setValue, toasts } = params; useEffect(() => { const loadBufferFromRemote = async (url: string) => { @@ -61,7 +61,7 @@ export const useSetInitialValue = (params: SetInitialValueParams) => { if (parsedURL.origin === 'https://www.elastic.co') { const resp = await fetch(parsedURL); const data = await resp.text(); - setValue(`${initialTextValue}\n\n${data}`); + setValue(`${localStorageValue}\n\n${data}`); } else { toasts.addWarning( i18n.translate('console.monaco.loadFromDataUnrecognizedUrlErrorMessage', { @@ -107,11 +107,11 @@ export const useSetInitialValue = (params: SetInitialValueParams) => { if (loadFromParam) { loadBufferFromRemote(loadFromParam); } else { - setValue(initialTextValue || DEFAULT_INPUT_VALUE); + setValue(localStorageValue || DEFAULT_INPUT_VALUE); } return () => { window.removeEventListener('hashchange', onHashChange); }; - }, [initialTextValue, setValue, toasts]); + }, [localStorageValue, setValue, toasts]); }; diff --git a/src/plugins/console/public/application/containers/editor/monaco/hooks/use_setup_autocomplete_polling.ts b/src/plugins/console/public/application/containers/editor/hooks/use_setup_autocomplete_polling.ts similarity index 94% rename from src/plugins/console/public/application/containers/editor/monaco/hooks/use_setup_autocomplete_polling.ts rename to src/plugins/console/public/application/containers/editor/hooks/use_setup_autocomplete_polling.ts index 1a1a5bb77dd1e..4785be4054ee0 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/hooks/use_setup_autocomplete_polling.ts +++ b/src/plugins/console/public/application/containers/editor/hooks/use_setup_autocomplete_polling.ts @@ -8,7 +8,7 @@ */ import { useEffect } from 'react'; -import { AutocompleteInfo, Settings } from '../../../../../services'; +import { AutocompleteInfo, Settings } from '../../../../services'; interface SetupAutocompletePollingParams { /** The Console autocomplete service. */ diff --git a/src/plugins/console/public/application/containers/editor/monaco/hooks/use_setup_autosave.ts b/src/plugins/console/public/application/containers/editor/hooks/use_setup_autosave.ts similarity index 96% rename from src/plugins/console/public/application/containers/editor/monaco/hooks/use_setup_autosave.ts rename to src/plugins/console/public/application/containers/editor/hooks/use_setup_autosave.ts index 6f46b8ce6589d..8b4bfaa888649 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/hooks/use_setup_autosave.ts +++ b/src/plugins/console/public/application/containers/editor/hooks/use_setup_autosave.ts @@ -8,7 +8,7 @@ */ import { useEffect, useRef } from 'react'; -import { useSaveCurrentTextObject } from '../../../../hooks'; +import { useSaveCurrentTextObject } from '../../../hooks'; import { readLoadFromParam } from './use_set_initial_value'; interface SetupAutosaveParams { diff --git a/src/plugins/console/public/application/containers/editor/index.ts b/src/plugins/console/public/application/containers/editor/index.ts index c9fbe97f01d8d..696806097badd 100644 --- a/src/plugins/console/public/application/containers/editor/index.ts +++ b/src/plugins/console/public/application/containers/editor/index.ts @@ -7,5 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { autoIndent, getDocumentation } from './legacy'; export { Editor } from './editor'; diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/apply_editor_settings.ts b/src/plugins/console/public/application/containers/editor/legacy/console_editor/apply_editor_settings.ts deleted file mode 100644 index 75e2516a52a7a..0000000000000 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/apply_editor_settings.ts +++ /dev/null @@ -1,27 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { DevToolsSettings } from '../../../../../services'; -import { CoreEditor } from '../../../../../types'; -import { CustomAceEditor } from '../../../../models/legacy_core_editor'; - -export function applyCurrentSettings( - editor: CoreEditor | CustomAceEditor, - settings: DevToolsSettings -) { - if ((editor as { setStyles?: Function }).setStyles) { - (editor as CoreEditor).setStyles({ - wrapLines: settings.wrapMode, - fontSize: settings.fontSize + 'px', - }); - } else { - (editor as CustomAceEditor).getSession().setUseWrapMode(settings.wrapMode); - (editor as CustomAceEditor).container.style.fontSize = settings.fontSize + 'px'; - } -} diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx deleted file mode 100644 index f0371562a77bb..0000000000000 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.mock.tsx +++ /dev/null @@ -1,50 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -// TODO(jbudz): should be removed when upgrading to TS@4.8 -// this is a skip for the errors created when typechecking with isolatedModules -export {}; - -jest.mock('../../../../contexts/editor_context/editor_registry', () => ({ - instance: { - setInputEditor: () => {}, - getInputEditor: () => ({ - getRequestsInRange: async () => [{ test: 'test' }], - getCoreEditor: () => ({ getCurrentPosition: jest.fn() }), - }), - }, -})); -jest.mock('../../../../components/editor_example', () => {}); -jest.mock('../../../../models/sense_editor', () => { - return { - create: () => ({ - getCoreEditor: () => ({ - registerKeyboardShortcut: jest.fn(), - setStyles: jest.fn(), - getContainer: () => ({ - focus: () => {}, - }), - on: jest.fn(), - addFoldsAtRanges: jest.fn(), - getAllFoldRanges: jest.fn(), - }), - update: jest.fn(), - commands: { - addCommand: () => {}, - }, - }), - }; -}); - -jest.mock('../../../../hooks/use_send_current_request/send_request', () => ({ - sendRequest: jest.fn(), -})); -jest.mock('../../../../../lib/autocomplete/get_endpoint_from_position', () => ({ - getEndpointFromPosition: jest.fn(), -})); diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx deleted file mode 100644 index 589be10596b9b..0000000000000 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.test.tsx +++ /dev/null @@ -1,100 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -jest.mock('../../../../../lib/utils', () => ({ replaceVariables: jest.fn() })); - -import './editor.test.mock'; - -import React from 'react'; -import { mount } from 'enzyme'; -import { I18nProvider } from '@kbn/i18n-react'; -import { act } from 'react-dom/test-utils'; -import * as sinon from 'sinon'; - -import { serviceContextMock } from '../../../../contexts/services_context.mock'; - -import { nextTick } from '@kbn/test-jest-helpers'; -import { - ServicesContextProvider, - EditorContextProvider, - RequestContextProvider, - ContextValue, -} from '../../../../contexts'; - -// Mocked functions -import { sendRequest } from '../../../../hooks/use_send_current_request/send_request'; -import { getEndpointFromPosition } from '../../../../../lib/autocomplete/get_endpoint_from_position'; -import type { DevToolsSettings } from '../../../../../services'; -import * as consoleMenuActions from '../console_menu_actions'; -import { Editor } from './editor'; -import * as utils from '../../../../../lib/utils'; - -describe('Legacy (Ace) Console Editor Component Smoke Test', () => { - let mockedAppContextValue: ContextValue; - const sandbox = sinon.createSandbox(); - - const doMount = () => - mount( - - - - - {}} /> - - - - - ); - - beforeEach(() => { - document.queryCommandSupported = sinon.fake(() => true); - mockedAppContextValue = serviceContextMock.create(); - (utils.replaceVariables as jest.Mock).mockReturnValue(['test']); - }); - - afterEach(() => { - jest.clearAllMocks(); - sandbox.restore(); - }); - - it('calls send current request', async () => { - (getEndpointFromPosition as jest.Mock).mockReturnValue({ patterns: [] }); - (sendRequest as jest.Mock).mockRejectedValue({}); - const editor = doMount(); - act(() => { - editor.find('button[data-test-subj~="sendRequestButton"]').simulate('click'); - }); - await nextTick(); - expect(sendRequest).toBeCalledTimes(1); - }); - - it('opens docs', () => { - const stub = sandbox.stub(consoleMenuActions, 'getDocumentation'); - const editor = doMount(); - const consoleMenuToggle = editor.find('[data-test-subj~="toggleConsoleMenu"]').last(); - consoleMenuToggle.simulate('click'); - - const docsButton = editor.find('[data-test-subj~="consoleMenuOpenDocs"]').last(); - docsButton.simulate('click'); - - expect(stub.callCount).toBe(1); - }); - - it('prompts auto-indent', () => { - const stub = sandbox.stub(consoleMenuActions, 'autoIndent'); - const editor = doMount(); - const consoleMenuToggle = editor.find('[data-test-subj~="toggleConsoleMenu"]').last(); - consoleMenuToggle.simulate('click'); - - const autoIndentButton = editor.find('[data-test-subj~="consoleMenuAutoIndent"]').last(); - autoIndentButton.simulate('click'); - - expect(stub.callCount).toBe(1); - }); -}); diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx deleted file mode 100644 index a0119ac2ec8fa..0000000000000 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ /dev/null @@ -1,343 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLink, - EuiScreenReaderOnly, - EuiToolTip, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { debounce } from 'lodash'; -import { decompressFromEncodedURIComponent } from 'lz-string'; -import { parse } from 'query-string'; -import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react'; -import { ace } from '@kbn/es-ui-shared-plugin/public'; -import { ConsoleMenu } from '../../../../components'; -import { useEditorReadContext, useServicesContext } from '../../../../contexts'; -import { - useSaveCurrentTextObject, - useSendCurrentRequest, - useSetInputEditor, -} from '../../../../hooks'; -import * as senseEditor from '../../../../models/sense_editor'; -import { autoIndent, getDocumentation } from '../console_menu_actions'; -import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; -import { applyCurrentSettings } from './apply_editor_settings'; -import { registerCommands } from './keyboard_shortcuts'; -import type { SenseEditor } from '../../../../models/sense_editor'; -import { StorageKeys } from '../../../../../services'; -import { DEFAULT_INPUT_VALUE } from '../../../../../../common/constants'; - -const { useUIAceKeyboardMode } = ace; - -export interface EditorProps { - initialTextValue: string; - setEditorInstance: (instance: SenseEditor) => void; -} - -interface QueryParams { - load_from: string; -} - -const abs: CSSProperties = { - position: 'absolute', - top: '0', - left: '0', - bottom: '0', - right: '0', -}; - -const inputId = 'ConAppInputTextarea'; - -function EditorUI({ initialTextValue, setEditorInstance }: EditorProps) { - const { - services: { - history, - notifications, - settings: settingsService, - esHostService, - http, - autocompleteInfo, - storage, - }, - docLinkVersion, - ...startServices - } = useServicesContext(); - - const { settings } = useEditorReadContext(); - const setInputEditor = useSetInputEditor(); - const sendCurrentRequest = useSendCurrentRequest(); - const saveCurrentTextObject = useSaveCurrentTextObject(); - - const editorRef = useRef(null); - const editorInstanceRef = useRef(null); - - const [textArea, setTextArea] = useState(null); - useUIAceKeyboardMode(textArea, startServices, settings.isAccessibilityOverlayEnabled); - - const openDocumentation = useCallback(async () => { - const documentation = await getDocumentation(editorInstanceRef.current!, docLinkVersion); - if (!documentation) { - return; - } - window.open(documentation, '_blank'); - }, [docLinkVersion]); - - useEffect(() => { - editorInstanceRef.current = senseEditor.create(editorRef.current!); - const editor = editorInstanceRef.current; - const textareaElement = editorRef.current!.querySelector('textarea'); - - if (textareaElement) { - textareaElement.setAttribute('id', inputId); - textareaElement.setAttribute('data-test-subj', 'console-textarea'); - } - - const readQueryParams = () => { - const [, queryString] = (window.location.hash || window.location.search || '').split('?'); - - return parse(queryString || '', { sort: false }) as Required; - }; - - const loadBufferFromRemote = (url: string) => { - const coreEditor = editor.getCoreEditor(); - // Normalize and encode the URL to avoid issues with spaces and other special characters. - const encodedUrl = new URL(url).toString(); - if (/^https?:\/\//.test(encodedUrl)) { - const loadFrom: Record = { - url, - // Having dataType here is required as it doesn't allow jQuery to `eval` content - // coming from the external source thereby preventing XSS attack. - dataType: 'text', - kbnXsrfToken: false, - }; - - if (/https?:\/\/api\.github\.com/.test(url)) { - loadFrom.headers = { Accept: 'application/vnd.github.v3.raw' }; - } - - // Fire and forget. - $.ajax(loadFrom).done(async (data) => { - // when we load data from another Api we also must pass history - await editor.update(`${initialTextValue}\n ${data}`, true); - editor.moveToNextRequestEdge(false); - coreEditor.clearSelection(); - editor.highlightCurrentRequestsAndUpdateActionBar(); - coreEditor.getContainer().focus(); - }); - } - - // If we have a data URI instead of HTTP, LZ-decode it. This enables - // opening requests in Console from anywhere in Kibana. - if (/^data:/.test(url)) { - const data = decompressFromEncodedURIComponent(url.replace(/^data:text\/plain,/, '')); - - // Show a toast if we have a failure - if (data === null || data === '') { - notifications.toasts.addWarning( - i18n.translate('console.loadFromDataUriErrorMessage', { - defaultMessage: 'Unable to load data from the load_from query parameter in the URL', - }) - ); - return; - } - - editor.update(data, true); - editor.moveToNextRequestEdge(false); - coreEditor.clearSelection(); - editor.highlightCurrentRequestsAndUpdateActionBar(); - coreEditor.getContainer().focus(); - } - }; - - // Support for loading a console snippet from a remote source, like support docs. - const onHashChange = debounce(() => { - const { load_from: url } = readQueryParams(); - if (!url) { - return; - } - loadBufferFromRemote(url); - }, 200); - window.addEventListener('hashchange', onHashChange); - - const initialQueryParams = readQueryParams(); - - if (initialQueryParams.load_from) { - loadBufferFromRemote(initialQueryParams.load_from); - } else { - editor.update(initialTextValue || DEFAULT_INPUT_VALUE); - } - - function setupAutosave() { - let timer: number; - const saveDelay = 500; - - editor.getCoreEditor().on('change', () => { - if (timer) { - clearTimeout(timer); - } - timer = window.setTimeout(saveCurrentState, saveDelay); - }); - } - - function saveCurrentState() { - try { - const content = editor.getCoreEditor().getValue(); - saveCurrentTextObject(content); - } catch (e) { - // Ignoring saving error - } - } - - function restoreFolds() { - if (editor) { - const foldRanges = storage.get(StorageKeys.FOLDS, []); - editor.getCoreEditor().addFoldsAtRanges(foldRanges); - } - } - - restoreFolds(); - - function saveFoldsOnChange() { - if (editor) { - editor.getCoreEditor().on('changeFold', () => { - const foldRanges = editor.getCoreEditor().getAllFoldRanges(); - storage.set(StorageKeys.FOLDS, foldRanges); - }); - } - } - - saveFoldsOnChange(); - - setInputEditor(editor); - setTextArea(editorRef.current!.querySelector('textarea')); - - autocompleteInfo.retrieve(settingsService, settingsService.getAutocomplete()); - - const unsubscribeResizer = subscribeResizeChecker(editorRef.current!, editor); - if (!initialQueryParams.load_from) { - // Don't setup autosaving editor content when we pre-load content - // This prevents losing the user's current console content when - // `loadFrom` query param is used for a console session - setupAutosave(); - } - - return () => { - unsubscribeResizer(); - autocompleteInfo.clearSubscriptions(); - window.removeEventListener('hashchange', onHashChange); - if (editorInstanceRef.current) { - // Close autocomplete popup on unmount - editorInstanceRef.current?.getCoreEditor().detachCompleter(); - editorInstanceRef.current.getCoreEditor().destroy(); - } - }; - }, [ - notifications.toasts, - saveCurrentTextObject, - initialTextValue, - history, - setInputEditor, - settingsService, - http, - autocompleteInfo, - storage, - ]); - - useEffect(() => { - const { current: editor } = editorInstanceRef; - applyCurrentSettings(editor!.getCoreEditor(), settings); - // Preserve legacy focus behavior after settings have updated. - editor!.getCoreEditor().getContainer().focus(); - }, [settings]); - - useEffect(() => { - const { isKeyboardShortcutsEnabled } = settings; - if (isKeyboardShortcutsEnabled) { - registerCommands({ - senseEditor: editorInstanceRef.current!, - sendCurrentRequest, - openDocumentation, - }); - } - }, [openDocumentation, settings, sendCurrentRequest]); - - useEffect(() => { - const { current: editor } = editorInstanceRef; - if (editor) { - setEditorInstance(editor); - } - }, [setEditorInstance]); - - return ( -
-
-
    - - - - - - - - - - { - return editorInstanceRef.current!.getRequestsAsCURL(esHostService.getHost()); - }} - getDocumentation={() => { - return getDocumentation(editorInstanceRef.current!, docLinkVersion); - }} - autoIndent={(event) => { - autoIndent(editorInstanceRef.current!, event); - }} - notifications={notifications} - /> - - - - - - -
    -
    -
- ); -} - -export const Editor = React.memo(EditorUI); diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx deleted file mode 100644 index 09cdf02cbab98..0000000000000 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor_output.tsx +++ /dev/null @@ -1,125 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { VectorTile } from '@mapbox/vector-tile'; -import Protobuf from 'pbf'; -import { EuiScreenReaderOnly } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useEffect, useRef } from 'react'; -import { convertMapboxVectorTileToJson } from './mapbox_vector_tile'; -import { Mode } from '../../../../models/legacy_core_editor/mode/output'; - -// Ensure the modes we might switch to dynamically are available -import 'brace/mode/text'; -import 'brace/mode/hjson'; -import 'brace/mode/yaml'; - -import { - useEditorReadContext, - useRequestReadContext, - useServicesContext, -} from '../../../../contexts'; -import { createReadOnlyAceEditor, CustomAceEditor } from '../../../../models/legacy_core_editor'; -import { subscribeResizeChecker } from '../subscribe_console_resize_checker'; -import { applyCurrentSettings } from './apply_editor_settings'; -import { isJSONContentType, isMapboxVectorTile, safeExpandLiteralStrings } from '../../utilities'; - -function modeForContentType(contentType?: string) { - if (!contentType) { - return 'ace/mode/text'; - } - if (isJSONContentType(contentType) || isMapboxVectorTile(contentType)) { - // Using hjson will allow us to use comments in editor output and solves the problem with error markers - return 'ace/mode/hjson'; - } else if (contentType.indexOf('application/yaml') >= 0) { - return 'ace/mode/yaml'; - } - return 'ace/mode/text'; -} - -function EditorOutputUI() { - const editorRef = useRef(null); - const editorInstanceRef = useRef(null); - const { services } = useServicesContext(); - const { settings: readOnlySettings } = useEditorReadContext(); - const { - lastResult: { data, error }, - } = useRequestReadContext(); - const inputId = 'ConAppOutputTextarea'; - - useEffect(() => { - editorInstanceRef.current = createReadOnlyAceEditor(editorRef.current!); - const unsubscribe = subscribeResizeChecker(editorRef.current!, editorInstanceRef.current); - const textarea = editorRef.current!.querySelector('textarea')!; - textarea.setAttribute('id', inputId); - textarea.setAttribute('readonly', 'true'); - - return () => { - unsubscribe(); - editorInstanceRef.current!.destroy(); - }; - }, [services.settings]); - - useEffect(() => { - const editor = editorInstanceRef.current!; - if (data) { - const isMultipleRequest = data.length > 1; - const mode = isMultipleRequest - ? new Mode() - : modeForContentType(data[0].response.contentType); - editor.update( - data - .map((result) => { - const { value, contentType } = result.response; - - let editorOutput; - if (readOnlySettings.tripleQuotes && isJSONContentType(contentType)) { - editorOutput = safeExpandLiteralStrings(value as string); - } else if (isMapboxVectorTile(contentType)) { - const vectorTile = new VectorTile(new Protobuf(value as ArrayBuffer)); - const vectorTileJson = convertMapboxVectorTileToJson(vectorTile); - editorOutput = safeExpandLiteralStrings(vectorTileJson as string); - } else { - editorOutput = value; - } - - return editorOutput; - }) - .join('\n'), - mode - ); - } else if (error) { - const mode = modeForContentType(error.response.contentType); - editor.update(error.response.value as string, mode); - } else { - editor.update(''); - } - }, [readOnlySettings, data, error]); - - useEffect(() => { - applyCurrentSettings(editorInstanceRef.current!, readOnlySettings); - }, [readOnlySettings]); - - return ( - <> - - - -
-
-
- - ); -} - -export const EditorOutput = React.memo(EditorOutputUI); diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/keyboard_shortcuts.ts b/src/plugins/console/public/application/containers/editor/legacy/console_editor/keyboard_shortcuts.ts deleted file mode 100644 index daad4bbdb7dbd..0000000000000 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/keyboard_shortcuts.ts +++ /dev/null @@ -1,92 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { throttle } from 'lodash'; -import { SenseEditor } from '../../../../models/sense_editor'; - -interface Actions { - senseEditor: SenseEditor; - sendCurrentRequest: () => void; - openDocumentation: () => void; -} - -const COMMANDS = { - SEND_TO_ELASTICSEARCH: 'send to Elasticsearch', - OPEN_DOCUMENTATION: 'open documentation', - AUTO_INDENT_REQUEST: 'auto indent request', - MOVE_TO_PREVIOUS_REQUEST: 'move to previous request start or end', - MOVE_TO_NEXT_REQUEST: 'move to next request start or end', - GO_TO_LINE: 'gotoline', -}; - -export function registerCommands({ senseEditor, sendCurrentRequest, openDocumentation }: Actions) { - const throttledAutoIndent = throttle(() => senseEditor.autoIndent(), 500, { - leading: true, - trailing: true, - }); - const coreEditor = senseEditor.getCoreEditor(); - - coreEditor.registerKeyboardShortcut({ - keys: { win: 'Ctrl-Enter', mac: 'Command-Enter' }, - name: COMMANDS.SEND_TO_ELASTICSEARCH, - fn: () => { - sendCurrentRequest(); - }, - }); - - coreEditor.registerKeyboardShortcut({ - name: COMMANDS.OPEN_DOCUMENTATION, - keys: { win: 'Ctrl-/', mac: 'Command-/' }, - fn: () => { - openDocumentation(); - }, - }); - - coreEditor.registerKeyboardShortcut({ - name: COMMANDS.AUTO_INDENT_REQUEST, - keys: { win: 'Ctrl-I', mac: 'Command-I' }, - fn: () => { - throttledAutoIndent(); - }, - }); - - coreEditor.registerKeyboardShortcut({ - name: COMMANDS.MOVE_TO_PREVIOUS_REQUEST, - keys: { win: 'Ctrl-Up', mac: 'Command-Up' }, - fn: () => { - senseEditor.moveToPreviousRequestEdge(); - }, - }); - - coreEditor.registerKeyboardShortcut({ - name: COMMANDS.MOVE_TO_NEXT_REQUEST, - keys: { win: 'Ctrl-Down', mac: 'Command-Down' }, - fn: () => { - senseEditor.moveToNextRequestEdge(false); - }, - }); - - coreEditor.registerKeyboardShortcut({ - name: COMMANDS.GO_TO_LINE, - keys: { win: 'Ctrl-L', mac: 'Command-L' }, - fn: (editor) => { - const line = parseInt(prompt('Enter line number') ?? '', 10); - if (!isNaN(line)) { - editor.gotoLine(line); - } - }, - }); -} - -export function unregisterCommands(senseEditor: SenseEditor) { - const coreEditor = senseEditor.getCoreEditor(); - Object.values(COMMANDS).forEach((command) => { - coreEditor.unregisterKeyboardShortcut(command); - }); -} diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_menu_actions.ts b/src/plugins/console/public/application/containers/editor/legacy/console_menu_actions.ts deleted file mode 100644 index c65efbc0d82f5..0000000000000 --- a/src/plugins/console/public/application/containers/editor/legacy/console_menu_actions.ts +++ /dev/null @@ -1,39 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { getEndpointFromPosition } from '../../../../lib/autocomplete/get_endpoint_from_position'; -import { SenseEditor } from '../../../models/sense_editor'; - -export async function autoIndent(editor: SenseEditor, event: React.MouseEvent) { - event.preventDefault(); - await editor.autoIndent(); - editor.getCoreEditor().getContainer().focus(); -} - -export function getDocumentation( - editor: SenseEditor, - docLinkVersion: string -): Promise { - return editor.getRequestsInRange().then((requests) => { - if (!requests || requests.length === 0) { - return null; - } - const position = requests[0].range.end; - position.column = position.column - 1; - const endpoint = getEndpointFromPosition(editor.getCoreEditor(), position, editor.parser); - if (endpoint && endpoint.documentation && endpoint.documentation.indexOf('http') !== -1) { - return endpoint.documentation - .replace('/master/', `/${docLinkVersion}/`) - .replace('/current/', `/${docLinkVersion}/`) - .replace('/{branch}/', `/${docLinkVersion}/`); - } else { - return null; - } - }); -} diff --git a/src/plugins/console/public/application/containers/editor/legacy/subscribe_console_resize_checker.ts b/src/plugins/console/public/application/containers/editor/legacy/subscribe_console_resize_checker.ts deleted file mode 100644 index 6511d7ad3cc3b..0000000000000 --- a/src/plugins/console/public/application/containers/editor/legacy/subscribe_console_resize_checker.ts +++ /dev/null @@ -1,28 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { ResizeChecker } from '@kbn/kibana-utils-plugin/public'; - -export function subscribeResizeChecker(el: HTMLElement, ...editors: any[]) { - const checker = new ResizeChecker(el); - checker.on('resize', () => - editors.forEach((e) => { - if (e.getCoreEditor) { - e.getCoreEditor().resize(); - } else { - e.resize(); - } - - if (e.updateActionsBar) { - e.updateActionsBar(); - } - }) - ); - return () => checker.destroy(); -} diff --git a/src/plugins/console/public/application/containers/editor/monaco/index.ts b/src/plugins/console/public/application/containers/editor/monaco/index.ts deleted file mode 100644 index b7b8576bbdf65..0000000000000 --- a/src/plugins/console/public/application/containers/editor/monaco/index.ts +++ /dev/null @@ -1,11 +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 - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { MonacoEditor } from './monaco_editor'; -export { MonacoEditorOutput } from './monaco_editor_output'; diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx b/src/plugins/console/public/application/containers/editor/monaco_editor.tsx similarity index 79% rename from src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx rename to src/plugins/console/public/application/containers/editor/monaco_editor.tsx index ca6e66a8ba66f..bc174b772bb1c 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor.tsx +++ b/src/plugins/console/public/application/containers/editor/monaco_editor.tsx @@ -8,18 +8,19 @@ */ import React, { CSSProperties, useCallback, useMemo, useRef, useState, useEffect } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { css } from '@emotion/react'; import { CodeEditor } from '@kbn/code-editor'; import { CONSOLE_LANG_ID, CONSOLE_THEME_ID, monaco } from '@kbn/monaco'; import { i18n } from '@kbn/i18n'; -import { useSetInputEditor } from '../../../hooks'; +import { useSetInputEditor } from '../../hooks'; import { ContextMenu } from './components'; import { useServicesContext, useEditorReadContext, useRequestActionContext, -} from '../../../contexts'; + useEditorActionContext, +} from '../../contexts'; import { useSetInitialValue, useSetupAutocompletePolling, @@ -32,10 +33,12 @@ import { MonacoEditorActionsProvider } from './monaco_editor_actions_provider'; import { getSuggestionProvider } from './monaco_editor_suggestion_provider'; export interface EditorProps { - initialTextValue: string; + localStorageValue: string | undefined; + value: string; + setValue: (value: string) => void; } -export const MonacoEditor = ({ initialTextValue }: EditorProps) => { +export const MonacoEditor = ({ localStorageValue, value, setValue }: EditorProps) => { const context = useServicesContext(); const { services: { notifications, settings: settingsService, autocompleteInfo }, @@ -43,7 +46,11 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { config: { isDevMode }, } = context; const { toasts } = notifications; - const { settings } = useEditorReadContext(); + const { + settings, + restoreRequestFromHistory: requestToRestoreFromHistory, + fileToImport, + } = useEditorReadContext(); const [editorInstance, setEditorInstace] = useState< monaco.editor.IStandaloneCodeEditor | undefined >(); @@ -53,6 +60,7 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { const { registerKeyboardCommands, unregisterKeyboardCommands } = useKeyboardCommandsUtils(); const dispatch = useRequestActionContext(); + const editorDispatch = useEditorActionContext(); const actionsProvider = useRef(null); const [editorActionsCss, setEditorActionsCss] = useState({}); @@ -117,18 +125,40 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => { const suggestionProvider = useMemo(() => { return getSuggestionProvider(actionsProvider); }, []); - const [value, setValue] = useState(initialTextValue); - useSetInitialValue({ initialTextValue, setValue, toasts }); + useSetInitialValue({ localStorageValue, setValue, toasts }); useSetupAutocompletePolling({ autocompleteInfo, settingsService }); useSetupAutosave({ value }); + // Restore the request from history if there is one + const updateEditor = useCallback(async () => { + if (requestToRestoreFromHistory) { + editorDispatch({ type: 'clearRequestToRestore' }); + await actionsProvider.current?.appendRequestToEditor( + requestToRestoreFromHistory, + dispatch, + context + ); + } + + // Import a request file if one is provided + if (fileToImport) { + editorDispatch({ type: 'setFileToImport', payload: null }); + await actionsProvider.current?.importRequestsToEditor(fileToImport); + } + }, [fileToImport, requestToRestoreFromHistory, dispatch, context, editorDispatch]); + + useEffect(() => { + updateEditor(); + }, [updateEditor]); + return (
{ - + - - - + iconSize={'s'} + /> - + { }; }); -jest.mock('../../../../services', () => { +jest.mock('../../../services', () => { return { getStorage: () => ({ get: () => [], @@ -40,7 +40,7 @@ jest.mock('../../../../services', () => { }; }); -jest.mock('../../../../lib/autocomplete/engine', () => { +jest.mock('../../../lib/autocomplete/engine', () => { return { populateContext: (...args: any) => { mockPopulateContext(args); diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts b/src/plugins/console/public/application/containers/editor/monaco_editor_actions_provider.ts similarity index 86% rename from src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts rename to src/plugins/console/public/application/containers/editor/monaco_editor_actions_provider.ts index 14be049c8ab26..8c66d31b2b57e 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_actions_provider.ts +++ b/src/plugins/console/public/application/containers/editor/monaco_editor_actions_provider.ts @@ -13,11 +13,11 @@ import { ConsoleParsedRequestsProvider, getParsedRequestsProvider, monaco } from import { i18n } from '@kbn/i18n'; import { toMountPoint } from '@kbn/react-kibana-mount'; import { XJson } from '@kbn/es-ui-shared-plugin/public'; -import { isQuotaExceededError } from '../../../../services/history'; -import { DEFAULT_VARIABLES } from '../../../../../common/constants'; -import { getStorage, StorageKeys } from '../../../../services'; -import { sendRequest } from '../../../hooks'; -import { Actions } from '../../../stores/request'; +import { isQuotaExceededError } from '../../../services/history'; +import { DEFAULT_VARIABLES } from '../../../../common/constants'; +import { getStorage, StorageKeys } from '../../../services'; +import { sendRequest } from '../../hooks'; +import { Actions } from '../../stores/request'; import { AutocompleteType, @@ -40,8 +40,9 @@ import { } from './utils'; import type { AdjustedParsedRequest } from './types'; -import { StorageQuotaError } from '../../../components/storage_quota_error'; -import { ContextValue } from '../../../contexts'; +import { type RequestToRestore, RestoreMethod } from '../../../types'; +import { StorageQuotaError } from '../../components/storage_quota_error'; +import { ContextValue } from '../../contexts'; import { containsComments, indentData } from './utils/requests_utils'; const AUTO_INDENTATION_ACTION_LABEL = 'Apply indentations'; @@ -120,7 +121,8 @@ export class MonacoEditorActionsProvider { const offset = this.editor.getTopForLineNumber(lineNumber) - this.editor.getScrollTop(); this.setEditorActionsCss({ visibility: 'visible', - top: offset, + // Move position down by 1 px so that the action buttons panel doesn't cover the top border of the selected block + top: offset + 1, }); } } @@ -147,7 +149,7 @@ export class MonacoEditorActionsProvider { range: selectedRange, options: { isWholeLine: true, - className: SELECTED_REQUESTS_CLASSNAME, + blockClassName: SELECTED_REQUESTS_CLASSNAME, }, }, ]); @@ -160,6 +162,11 @@ export class MonacoEditorActionsProvider { private async getSelectedParsedRequests(): Promise { const model = this.editor.getModel(); + + if (!model) { + return []; + } + const selection = this.editor.getSelection(); if (!model || !selection) { return Promise.resolve([]); @@ -173,6 +180,9 @@ export class MonacoEditorActionsProvider { startLineNumber: number, endLineNumber: number ): Promise { + if (!model) { + return []; + } const parsedRequests = await this.parsedRequestsProvider.getRequests(); const selectedRequests: AdjustedParsedRequest[] = []; for (const [index, parsedRequest] of parsedRequests.entries()) { @@ -243,9 +253,17 @@ export class MonacoEditorActionsProvider { const { toasts } = notifications; try { const allRequests = await this.getRequests(); - // if any request doesnt have a method then we gonna treat it as a non-valid - // request - const requests = allRequests.filter((request) => request.method); + const selectedRequests = await this.getSelectedParsedRequests(); + + const requests = allRequests + // if any request doesnt have a method then we gonna treat it as a non-valid + // request + .filter((request) => request.method) + // map the requests to the original line number + .map((request, index) => ({ + ...request, + lineNumber: selectedRequests[index].startLineNumber, + })); // If we do have requests but none have methods we are not sending the request if (allRequests.length > 0 && !requests.length) { @@ -479,9 +497,6 @@ export class MonacoEditorActionsProvider { return this.getSuggestions(model, position, context); } - /* - * This function inserts a request from the history into the editor - */ public async restoreRequestFromHistory(request: string) { const model = this.editor.getModel(); if (!model) { @@ -679,4 +694,82 @@ export class MonacoEditorActionsProvider { this.editor.trigger(TRIGGER_SUGGESTIONS_ACTION_LABEL, TRIGGER_SUGGESTIONS_HANDLER_ID, {}); } } + + /* + * This function cleares out the editor content and replaces it with the provided requests + */ + public async importRequestsToEditor(requestsToImport: string) { + const model = this.editor.getModel(); + + if (!model) { + return; + } + + const edit: monaco.editor.IIdentifiedSingleEditOperation = { + range: model.getFullModelRange(), + text: requestsToImport, + forceMoveMarkers: true, + }; + + this.editor.executeEdits('restoreFromHistory', [edit]); + } + + /* + * This function inserts a request after the last request in the editor + */ + public async appendRequestToEditor( + req: RequestToRestore, + dispatch: Dispatch, + context: ContextValue + ) { + const model = this.editor.getModel(); + + if (!model) { + return; + } + + // 1 - Create an edit operation to insert the request after the last request + const lastLineNumber = model.getLineCount(); + const column = model.getLineMaxColumn(lastLineNumber); + const edit: monaco.editor.IIdentifiedSingleEditOperation = { + range: { + startLineNumber: lastLineNumber, + startColumn: column, + endLineNumber: lastLineNumber, + endColumn: column, + }, + text: `\n\n${req.request}`, + forceMoveMarkers: true, + }; + this.editor.executeEdits('restoreFromHistory', [edit]); + + // 2 - Since we add two new lines, the cursor should be at the beginning of the new request + const beginningOfNewReq = lastLineNumber + 2; + const selectedRequests = await this.getRequestsBetweenLines( + model, + beginningOfNewReq, + beginningOfNewReq + ); + // We can assume that there is only one request given that we only add one + // request at a time. + const restoredRequest = selectedRequests[0]; + + // 3 - Set the cursor to the beginning of the new request, + this.editor.setSelection({ + startLineNumber: restoredRequest.startLineNumber, + startColumn: 1, + endLineNumber: restoredRequest.startLineNumber, + endColumn: 1, + }); + + // 4 - Scroll to the beginning of the new request + this.editor.setScrollPosition({ + scrollTop: this.editor.getTopForLineNumber(restoredRequest.startLineNumber), + }); + + // 5 - Optionally send the request + if (req.restoreMethod === RestoreMethod.RESTORE_AND_EXECUTE) { + this.sendRequests(dispatch, context); + } + } } diff --git a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_output.tsx b/src/plugins/console/public/application/containers/editor/monaco_editor_output.tsx similarity index 60% rename from src/plugins/console/public/application/containers/editor/monaco/monaco_editor_output.tsx rename to src/plugins/console/public/application/containers/editor/monaco_editor_output.tsx index 9de6748b62b6c..b9e3f3e6f9885 100644 --- a/src/plugins/console/public/application/containers/editor/monaco/monaco_editor_output.tsx +++ b/src/plugins/console/public/application/containers/editor/monaco_editor_output.tsx @@ -7,26 +7,44 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'; +import React, { + CSSProperties, + FunctionComponent, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { CodeEditor } from '@kbn/code-editor'; import { css } from '@emotion/react'; import { VectorTile } from '@mapbox/vector-tile'; import Protobuf from 'pbf'; import { i18n } from '@kbn/i18n'; -import { EuiScreenReaderOnly } from '@elastic/eui'; +import { + EuiScreenReaderOnly, + EuiFlexGroup, + EuiFlexItem, + EuiButtonIcon, + EuiToolTip, +} from '@elastic/eui'; import { CONSOLE_THEME_ID, CONSOLE_OUTPUT_LANG_ID, monaco } from '@kbn/monaco'; -import { getStatusCodeDecorations } from './utils'; -import { useEditorReadContext, useRequestReadContext } from '../../../contexts'; -import { convertMapboxVectorTileToJson } from '../legacy/console_editor/mapbox_vector_tile'; import { + getStatusCodeDecorations, isJSONContentType, isMapboxVectorTile, safeExpandLiteralStrings, languageForContentType, -} from '../utilities'; + convertMapboxVectorTileToJson, +} from './utils'; +import { useEditorReadContext, useRequestReadContext, useServicesContext } from '../../contexts'; +import { MonacoEditorOutputActionsProvider } from './monaco_editor_output_actions_provider'; import { useResizeCheckerUtils } from './hooks'; export const MonacoEditorOutput: FunctionComponent = () => { + const context = useServicesContext(); + const { + services: { notifications }, + } = context; const { settings: readOnlySettings } = useEditorReadContext(); const { lastResult: { data }, @@ -37,8 +55,14 @@ export const MonacoEditorOutput: FunctionComponent = () => { const { setupResizeChecker, destroyResizeChecker } = useResizeCheckerUtils(); const lineDecorations = useRef(null); + const actionsProvider = useRef(null); + const [editorActionsCss, setEditorActionsCss] = useState({}); + const editorDidMountCallback = useCallback( (editor: monaco.editor.IStandaloneCodeEditor) => { + const provider = new MonacoEditorOutputActionsProvider(editor, setEditorActionsCss); + actionsProvider.current = provider; + setupResizeChecker(divRef.current!, editor); lineDecorations.current = editor.createDecorationsCollection(); }, @@ -83,19 +107,71 @@ export const MonacoEditorOutput: FunctionComponent = () => { // If there are multiple responses, add decorations for their status codes const decorations = getStatusCodeDecorations(data); lineDecorations.current?.set(decorations); + // Highlight first line of the output editor + actionsProvider.current?.selectFirstLine(); } } else { setValue(''); } }, [readOnlySettings, data, value]); + const copyOutputCallback = useCallback(async () => { + const selectedText = (await actionsProvider.current?.getParsedOutput()) as string; + + try { + if (!window.navigator?.clipboard) { + throw new Error('Could not copy to clipboard!'); + } + + await window.navigator.clipboard.writeText(selectedText); + + notifications.toasts.addSuccess({ + title: i18n.translate('console.outputPanel.copyOutputToast', { + defaultMessage: 'Selected output copied to clipboard', + }), + }); + } catch (e) { + notifications.toasts.addDanger({ + title: i18n.translate('console.outputPanel.copyOutputToastFailedMessage', { + defaultMessage: 'Could not copy selected output to clipboard', + }), + }); + } + }, [notifications.toasts]); + return (
+ + + + + + +