mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Console] UX Improvements for phase 2 (#190698)
This commit is contained in:
parent
ebe4686e6c
commit
b3a1e5fb8f
187 changed files with 5210 additions and 5986 deletions
|
@ -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
|
||||
|
|
|
@ -39,6 +39,7 @@ export {
|
|||
CONSOLE_THEME_ID,
|
||||
getParsedRequestsProvider,
|
||||
ConsoleParsedRequestsProvider,
|
||||
createOutputParser,
|
||||
} from './src/console';
|
||||
|
||||
export type { ParsedRequest } from './src/console';
|
||||
|
|
|
@ -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';
|
||||
|
|
401
packages/kbn-monaco/src/console/output_parser.js
Normal file
401
packages/kbn-monaco/src/console/output_parser.js
Normal file
|
@ -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;
|
||||
};
|
||||
}
|
40
packages/kbn-monaco/src/console/output_parser.test.ts
Normal file
40
packages/kbn-monaco/src/console/output_parser.test.ts
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -21,6 +21,16 @@ export interface ConsoleParserResult {
|
|||
requests: ParsedRequest[];
|
||||
}
|
||||
|
||||
export interface ConsoleOutputParsedResponse {
|
||||
startOffset: number;
|
||||
endOffset?: number;
|
||||
data?: Array<Record<string, unknown>>;
|
||||
}
|
||||
export interface ConsoleOutputParserResult {
|
||||
errors: ErrorAnnotation[];
|
||||
responses: ConsoleOutputParsedResponse[];
|
||||
}
|
||||
|
||||
export interface ConsoleWorkerDefinition {
|
||||
getParserResult: (modelUri: string) => ConsoleParserResult | undefined;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<Props, State> {
|
|||
|
||||
render() {
|
||||
const button = (
|
||||
<EuiLink
|
||||
<EuiButtonIcon
|
||||
onClick={this.onButtonClick}
|
||||
data-test-subj="toggleConsoleMenu"
|
||||
aria-label={i18n.translate('console.requestOptionsButtonAriaLabel', {
|
||||
defaultMessage: 'Request options',
|
||||
})}
|
||||
>
|
||||
<EuiIcon type="boxesVertical" />
|
||||
</EuiLink>
|
||||
iconType="boxesVertical"
|
||||
iconSize="s"
|
||||
/>
|
||||
);
|
||||
|
||||
const items = [
|
||||
|
|
|
@ -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 (
|
||||
<EuiTourStep
|
||||
step={step}
|
||||
stepsTotal={stepsTotal}
|
||||
isStepOpen={isStepOpen}
|
||||
title={title}
|
||||
content={content}
|
||||
onFinish={onFinish}
|
||||
footerAction={footerAction}
|
||||
data-test-subj={dataTestSubj}
|
||||
anchorPosition={anchorPosition as PopoverAnchorPosition}
|
||||
maxWidth={maxWidth}
|
||||
css={css}
|
||||
>
|
||||
{children}
|
||||
</EuiTourStep>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<EuiPageSection className="conApp__editor__spinner">
|
||||
<EuiSkeletonText lines={10} />
|
||||
<EuiPageSection alignment="center" grow={true} className="conApp__editor__spinner">
|
||||
<EuiLoadingSpinner size="xxl" />
|
||||
</EuiPageSection>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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<HTMLDivElement | null>(null);
|
||||
const editorRef = useRef<CustomAceEditor>();
|
||||
|
||||
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 (
|
||||
<>
|
||||
<EuiScreenReaderOnly>
|
||||
<label htmlFor={inputId}>
|
||||
{i18n.translate('console.exampleOutputTextarea', {
|
||||
defaultMessage: 'Dev Tools Console editor example',
|
||||
})}
|
||||
</label>
|
||||
</EuiScreenReaderOnly>
|
||||
<div ref={wrapperDivRef} className="conApp_example" css={wrapperDivStyle} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default withEuiTheme(EditorExample);
|
|
@ -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 (
|
||||
<EuiFlyout onClose={props.onClose} data-test-subj="helpFlyout" size="s">
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage id="console.helpPage.pageTitle" defaultMessage="Help" />
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiText>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
defaultMessage="Request format"
|
||||
id="console.helpPage.requestFormatTitle"
|
||||
/>
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.requestFormatDescription"
|
||||
defaultMessage="You can type one or more requests in the editor. Console understands requests in a compact format."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.learnAboutConsoleAndQueryDslText"
|
||||
defaultMessage="Learn about {console} and {queryDsl}"
|
||||
values={{
|
||||
console: (
|
||||
<EuiLink href={docLinks.console.guide} target="_blank" external>
|
||||
Console
|
||||
</EuiLink>
|
||||
),
|
||||
queryDsl: (
|
||||
<EuiLink href={docLinks.query.queryDsl} target="_blank" external>
|
||||
Query DSL
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<EditorExample panel="help" />
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.keyboardCommandsTitle"
|
||||
defaultMessage="Keyboard commands"
|
||||
/>
|
||||
</h3>
|
||||
<EuiSpacer />
|
||||
<dl>
|
||||
<dt>Ctrl/Cmd + I</dt>
|
||||
<dd>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.keyboardCommands.autoIndentDescription"
|
||||
defaultMessage="Auto indent current request"
|
||||
/>
|
||||
</dd>
|
||||
<dt>Ctrl/Cmd + /</dt>
|
||||
<dd>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.keyboardCommands.openDocumentationDescription"
|
||||
defaultMessage="Open documentation for current request"
|
||||
/>
|
||||
</dd>
|
||||
<dt>Ctrl + Space</dt>
|
||||
<dd>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.keyboardCommands.openAutoCompleteDescription"
|
||||
defaultMessage="Open Auto complete (even if not typing)"
|
||||
/>
|
||||
</dd>
|
||||
<dt>Ctrl/Cmd + Enter</dt>
|
||||
<dd>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.keyboardCommands.submitRequestDescription"
|
||||
defaultMessage="Submit request"
|
||||
/>
|
||||
</dd>
|
||||
<dt>Ctrl/Cmd + Up/Down</dt>
|
||||
<dd>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.keyboardCommands.jumpToPreviousNextRequestDescription"
|
||||
defaultMessage="Jump to the previous/next request start or end."
|
||||
/>
|
||||
</dd>
|
||||
<dt>Ctrl/Cmd + Alt + L</dt>
|
||||
<dd>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.keyboardCommands.collapseExpandCurrentScopeDescription"
|
||||
defaultMessage="Collapse/expand current scope."
|
||||
/>
|
||||
</dd>
|
||||
<dt>Ctrl/Cmd + Option + 0</dt>
|
||||
<dd>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.keyboardCommands.collapseAllScopesDescription"
|
||||
defaultMessage="Collapse all scopes but the current one. Expand by adding a shift."
|
||||
/>
|
||||
</dd>
|
||||
<dt>Down arrow</dt>
|
||||
<dd>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.keyboardCommands.switchFocusToAutoCompleteMenuDescription"
|
||||
defaultMessage="Switch focus to auto-complete menu. Use arrows to further select a term"
|
||||
/>
|
||||
</dd>
|
||||
<dt>Enter/Tab</dt>
|
||||
<dd>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.keyboardCommands.selectCurrentlySelectedInAutoCompleteMenuDescription"
|
||||
defaultMessage="Select the currently selected or the top most term in auto-complete menu"
|
||||
/>
|
||||
</dd>
|
||||
<dt>Ctrl/Cmd + L</dt>
|
||||
<dd>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.keyboardCommands.goToLineNumberDescription"
|
||||
defaultMessage="Go to line number"
|
||||
/>
|
||||
</dd>
|
||||
<dt>Esc</dt>
|
||||
<dd>
|
||||
<FormattedMessage
|
||||
id="console.helpPage.keyboardCommands.closeAutoCompleteMenuDescription"
|
||||
defaultMessage="Close auto-complete menu"
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
</EuiText>
|
||||
</EuiFlyoutBody>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="downRight"
|
||||
buffer={4}
|
||||
ownFocus={false}
|
||||
data-test-subj="consoleHelpPopover"
|
||||
>
|
||||
<EuiTitle size="xs">
|
||||
<h4>
|
||||
{i18n.translate('console.helpPopover.title', {
|
||||
defaultMessage: 'Elastic Console',
|
||||
})}
|
||||
</h4>
|
||||
</EuiTitle>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiText style={{ width: 300 }} color="subdued" size="s">
|
||||
<p>
|
||||
{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.',
|
||||
})}
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiFlexGroup gutterSize="m" direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<p>
|
||||
{i18n.translate('console.helpPopover.aboutConsoleLabel', {
|
||||
defaultMessage: 'About Console',
|
||||
})}
|
||||
</p>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="popout"
|
||||
href={docLinks.console.guide}
|
||||
target="_blank"
|
||||
color="text"
|
||||
aria-label={i18n.translate('console.helpPopover.aboutConsoleButtonAriaLabel', {
|
||||
defaultMessage: 'About Console link',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<p>
|
||||
{i18n.translate('console.helpPopover.aboutQueryDSLLabel', {
|
||||
defaultMessage: 'About Query DSL',
|
||||
})}
|
||||
</p>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="popout"
|
||||
href={docLinks.query.queryDsl}
|
||||
target="_blank"
|
||||
color="text"
|
||||
aria-label={i18n.translate('console.helpPopover.aboutQueryDSLButtonAriaLabel', {
|
||||
defaultMessage: 'About QueryDSL link',
|
||||
})}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<p>
|
||||
{i18n.translate('console.helpPopover.rerunTourLabel', {
|
||||
defaultMessage: 'Re-run feature tour',
|
||||
})}
|
||||
</p>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
iconType="refresh"
|
||||
onClick={resetTour}
|
||||
color="text"
|
||||
aria-label={i18n.translate('console.helpPopover.rerunTourButtonAriaLabel', {
|
||||
defaultMessage: 'Re-run feature tour button',
|
||||
})}
|
||||
data-test-subj="consoleRerunTourButton"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -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';
|
||||
|
|
|
@ -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 (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="console.outputEmptyState.title"
|
||||
defaultMessage="Enter a new request"
|
||||
/>
|
||||
</h2>
|
||||
}
|
||||
body={
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.outputEmptyState.description"
|
||||
defaultMessage="When you run a request in the input panel, you will see the output response here."
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<EuiTitle size="xxs">
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="console.outputEmptyState.learnMore"
|
||||
defaultMessage="Want to learn more?"
|
||||
/>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
<EuiLink href={docLinks.console.guide} target="_blank">
|
||||
<FormattedMessage
|
||||
id="console.outputEmptyState.docsLink"
|
||||
defaultMessage="Read the Console docs"
|
||||
/>
|
||||
</EuiLink>
|
||||
</>
|
||||
}
|
||||
data-test-subj="consoleOutputPanelEmptyState"
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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);
|
|
@ -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 (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
<FormattedMessage id="console.settingsPage.pageTitle" defaultMessage="Console settings" />
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.settingsPage.pageDescription"
|
||||
defaultMessage="Customize Console to suit your workflow."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
|
||||
{/* GENERAL SETTINGS */}
|
||||
<SettingsGroup
|
||||
title={i18n.translate('console.settingsPage.generalSettingsLabel', {
|
||||
defaultMessage: 'General settings',
|
||||
})}
|
||||
/>
|
||||
<SettingsFormRow
|
||||
label={i18n.translate('console.settingsPage.saveRequestsToHistoryLabel', {
|
||||
defaultMessage: 'Save requests to history',
|
||||
})}
|
||||
>
|
||||
<EuiSwitch
|
||||
checked={isHistoryEnabled}
|
||||
label={isHistoryEnabled ? ON_LABEL : OFF_LABEL}
|
||||
onChange={(e) => toggleSavingToHistory(e.target.checked)}
|
||||
/>
|
||||
</SettingsFormRow>
|
||||
<SettingsFormRow
|
||||
label={i18n.translate('console.settingsPage.enableKeyboardShortcutsLabel', {
|
||||
defaultMessage: 'Keyboard shortcuts',
|
||||
})}
|
||||
>
|
||||
<EuiSwitch
|
||||
data-test-subj="enableKeyboardShortcuts"
|
||||
label={isKeyboardShortcutsEnabled ? ON_LABEL : OFF_LABEL}
|
||||
checked={isKeyboardShortcutsEnabled}
|
||||
onChange={(e) => toggleKeyboardShortcuts(e.target.checked)}
|
||||
/>
|
||||
</SettingsFormRow>
|
||||
<SettingsFormRow
|
||||
label={i18n.translate('console.settingsPage.enableAccessibilityOverlayLabel', {
|
||||
defaultMessage: 'Accessibility overlay',
|
||||
})}
|
||||
>
|
||||
<EuiSwitch
|
||||
data-test-subj="enableA11yOverlay"
|
||||
label={isAccessibilityOverlayEnabled ? ON_LABEL : OFF_LABEL}
|
||||
checked={isAccessibilityOverlayEnabled}
|
||||
onChange={(e) => toggleAccessibilityOverlay(e.target.checked)}
|
||||
/>
|
||||
</SettingsFormRow>
|
||||
|
||||
{/* DISPLAY SETTINGS */}
|
||||
<SettingsGroup
|
||||
title={i18n.translate('console.settingsPage.displaySettingsLabel', {
|
||||
defaultMessage: 'Display',
|
||||
})}
|
||||
/>
|
||||
<SettingsFormRow
|
||||
label={i18n.translate('console.settingsPage.fontSizeLabel', {
|
||||
defaultMessage: 'Font size',
|
||||
})}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
css={{ minWidth: '220px' }}
|
||||
compressed
|
||||
data-test-subj="setting-font-size-input"
|
||||
value={fontSize}
|
||||
min={6}
|
||||
max={50}
|
||||
onChange={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!val) return;
|
||||
setFontSize(val);
|
||||
}}
|
||||
/>
|
||||
</SettingsFormRow>
|
||||
<SettingsFormRow
|
||||
label={i18n.translate('console.settingsPage.wrapLongLinesLabel', {
|
||||
defaultMessage: 'Wrap long lines',
|
||||
})}
|
||||
>
|
||||
<EuiSwitch
|
||||
data-test-subj="settingsWrapLines"
|
||||
label={wrapMode ? ON_LABEL : OFF_LABEL}
|
||||
checked={wrapMode}
|
||||
onChange={(e) => setWrapMode(e.target.checked)}
|
||||
id="wrapLines"
|
||||
/>
|
||||
</SettingsFormRow>
|
||||
<SettingsFormRow
|
||||
label={i18n.translate('console.settingsPage.tripleQuotesMessage', {
|
||||
defaultMessage: 'Triple quotes in output',
|
||||
})}
|
||||
>
|
||||
<EuiSwitch
|
||||
data-test-subj="tripleQuotes"
|
||||
label={tripleQuotes ? ON_LABEL : OFF_LABEL}
|
||||
checked={tripleQuotes}
|
||||
onChange={(e) => setTripleQuotes(e.target.checked)}
|
||||
id="tripleQuotes"
|
||||
/>
|
||||
</SettingsFormRow>
|
||||
|
||||
{/* AUTOCOMPLETE SETTINGS */}
|
||||
<SettingsGroup
|
||||
title={i18n.translate('console.settingsPage.autocompleteSettingsLabel', {
|
||||
defaultMessage: 'Autocomplete',
|
||||
})}
|
||||
/>
|
||||
{autoCompleteCheckboxes.map((opts) => (
|
||||
<SettingsFormRow key={opts.id} label={opts.label}>
|
||||
<EuiSwitch
|
||||
data-test-subj={`autocomplete-settings-${opts.id}`}
|
||||
label={opts.checked ? ON_LABEL : OFF_LABEL}
|
||||
checked={opts.checked}
|
||||
onChange={(e) => opts.stateSetter(e.target.checked)}
|
||||
/>
|
||||
</SettingsFormRow>
|
||||
))}
|
||||
|
||||
{/* AUTOCOMPLETE REFRESH SETTINGS */}
|
||||
{(fields || indices || templates || dataStreams) && (
|
||||
<>
|
||||
<SettingsGroup
|
||||
title={i18n.translate('console.settingsPage.autocompleteRefreshSettingsLabel', {
|
||||
defaultMessage: 'Autocomplete refresh',
|
||||
})}
|
||||
description={i18n.translate(
|
||||
'console.settingsPage.autocompleteRefreshSettingsDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Console refreshes autocomplete suggestions by querying Elasticsearch. Use less frequent refreshes to reduce bandwidth costs.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<SettingsFormRow
|
||||
label={i18n.translate('console.settingsPage.refreshingDataLabel', {
|
||||
defaultMessage: 'Refresh frequency',
|
||||
})}
|
||||
>
|
||||
<EuiSuperSelect
|
||||
css={{ minWidth: '220px' }}
|
||||
compressed
|
||||
options={intervalOptions}
|
||||
valueOfSelected={pollInterval.toString()}
|
||||
onChange={onPollingIntervalChange}
|
||||
/>
|
||||
</SettingsFormRow>
|
||||
|
||||
<SettingsFormRow
|
||||
label={i18n.translate('console.settingsPage.manualRefreshLabel', {
|
||||
defaultMessage: 'Manually refresh autocomplete suggestions',
|
||||
})}
|
||||
>
|
||||
<EuiButton
|
||||
iconType="refresh"
|
||||
size="s"
|
||||
data-test-subj="autocompletePolling"
|
||||
id="autocompletePolling"
|
||||
onClick={() => {
|
||||
// Only refresh the currently selected settings.
|
||||
props.refreshAutocompleteSettings({
|
||||
fields,
|
||||
indices,
|
||||
templates,
|
||||
dataStreams,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Refresh"
|
||||
id="console.settingsPage.refreshButtonLabel"
|
||||
/>
|
||||
</EuiButton>
|
||||
</SettingsFormRow>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiFlexGroup alignItems="center" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>{label}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiTitle size="xs">
|
||||
<h2>{title}</h2>
|
||||
</EuiTitle>
|
||||
{description && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued">
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
<EuiHorizontalRule margin="s" />
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -7,5 +7,4 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { Panel } from './panel';
|
||||
export { PanelsContainer } from './panel_container';
|
||||
export type AutocompleteOptions = 'fields' | 'indices' | 'templates';
|
|
@ -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 ? (
|
||||
<Fragment>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="console.settingsPage.refreshingDataLabel"
|
||||
defaultMessage="Refresh frequency"
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="console.settingsPage.refreshingDataDescription"
|
||||
defaultMessage="Console refreshes autocomplete suggestions by querying Elasticsearch.
|
||||
Use less frequent refreshes to reduce bandwidth costs."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiSuperSelect
|
||||
options={intervalOptions}
|
||||
valueOfSelected={pollInterval.toString()}
|
||||
onChange={onPollingIntervalChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiButton
|
||||
data-test-subj="autocompletePolling"
|
||||
id="autocompletePolling"
|
||||
onClick={() => {
|
||||
// Only refresh the currently selected settings.
|
||||
props.refreshAutocompleteSettings({
|
||||
fields,
|
||||
indices,
|
||||
templates,
|
||||
dataStreams,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Refresh autocomplete suggestions"
|
||||
id="console.settingsPage.refreshButtonLabel"
|
||||
/>
|
||||
</EuiButton>
|
||||
</Fragment>
|
||||
) : undefined;
|
||||
|
||||
return (
|
||||
<EuiModal
|
||||
data-test-subj="devToolsSettingsModal"
|
||||
className="conApp__settingsModal"
|
||||
onClose={props.onClose}
|
||||
>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>
|
||||
<FormattedMessage id="console.settingsPage.pageTitle" defaultMessage="Console settings" />
|
||||
</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage id="console.settingsPage.fontSizeLabel" defaultMessage="Font size" />
|
||||
}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
autoFocus
|
||||
data-test-subj="setting-font-size-input"
|
||||
value={fontSize}
|
||||
min={6}
|
||||
max={50}
|
||||
onChange={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!val) return;
|
||||
setFontSize(val);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
checked={wrapMode}
|
||||
data-test-subj="settingsWrapLines"
|
||||
id="wrapLines"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Wrap long lines"
|
||||
id="console.settingsPage.wrapLongLinesLabelText"
|
||||
/>
|
||||
}
|
||||
onChange={(e) => setWrapMode(e.target.checked)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="console.settingsPage.jsonSyntaxLabel"
|
||||
defaultMessage="JSON syntax"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiSwitch
|
||||
checked={tripleQuotes}
|
||||
data-test-subj="tripleQuotes"
|
||||
id="tripleQuotes"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Use triple quotes in output"
|
||||
id="console.settingsPage.tripleQuotesMessage"
|
||||
/>
|
||||
}
|
||||
onChange={(e) => setTripleQuotes(e.target.checked)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage id="console.settingsPage.historyLabel" defaultMessage="History" />
|
||||
}
|
||||
>
|
||||
<EuiSwitch
|
||||
checked={isHistoryEnabled}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Save requests to history"
|
||||
id="console.settingsPage.saveRequestsToHistoryLabel"
|
||||
/>
|
||||
}
|
||||
onChange={(e) => toggleSavingToHistory(e.target.checked)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="console.settingsPage.keyboardShortcutsLabel"
|
||||
defaultMessage="Keyboard shortcuts"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiSwitch
|
||||
checked={isKeyboardShortcutsEnabled}
|
||||
data-test-subj="enableKeyboardShortcuts"
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Enable keyboard shortcuts"
|
||||
id="console.settingsPage.enableKeyboardShortcutsLabel"
|
||||
/>
|
||||
}
|
||||
onChange={(e) => toggleKeyboardShortcuts(e.target.checked)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="console.settingsPage.accessibilityOverlayLabel"
|
||||
defaultMessage="Accessibility overlay"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiSwitch
|
||||
data-test-subj="enableA11yOverlay"
|
||||
checked={isAccessibilityOverlayEnabled}
|
||||
label={
|
||||
<FormattedMessage
|
||||
defaultMessage="Enable accessibility overlay"
|
||||
id="console.settingsPage.enableAccessibilityOverlayLabel"
|
||||
/>
|
||||
}
|
||||
onChange={(e) => toggleAccessibilityOverlay(e.target.checked)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiFormRow
|
||||
labelType="legend"
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="console.settingsPage.autocompleteLabel"
|
||||
defaultMessage="Autocomplete"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiCheckboxGroup
|
||||
options={autoCompleteCheckboxes.map((opts) => {
|
||||
const { stateSetter, ...rest } = opts;
|
||||
return rest;
|
||||
})}
|
||||
idToSelectedMap={checkboxIdToSelectedMap}
|
||||
onChange={(e: unknown) => {
|
||||
onAutocompleteChange(e as AutocompleteOptions);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{pollingFields}
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButtonEmpty data-test-subj="settingsCancelButton" onClick={props.onClose}>
|
||||
<FormattedMessage id="console.settingsPage.cancelButtonLabel" defaultMessage="Cancel" />
|
||||
</EuiButtonEmpty>
|
||||
|
||||
<EuiButton fill data-test-subj="settings-save-button" onClick={saveSettings}>
|
||||
<FormattedMessage id="console.settingsPage.saveButtonLabel" defaultMessage="Save" />
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</EuiModal>
|
||||
);
|
||||
};
|
|
@ -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';
|
|
@ -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: (
|
||||
<EuiIcon
|
||||
type={'sortUp'}
|
||||
title={i18n.translate('console.shortcutKeys.keyUpArrow', {
|
||||
defaultMessage: 'Up arrow',
|
||||
})}
|
||||
size="s"
|
||||
/>
|
||||
),
|
||||
keyDown: (
|
||||
<EuiIcon
|
||||
type={'sortDown'}
|
||||
title={i18n.translate('console.shortcutKeys.keyDownArrow', {
|
||||
defaultMessage: 'Down arrow',
|
||||
})}
|
||||
size="s"
|
||||
/>
|
||||
),
|
||||
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',
|
||||
}),
|
||||
};
|
|
@ -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) => (
|
||||
<span key={index}>
|
||||
{index > 0 && ' + '}
|
||||
<EuiCode>{key}</EuiCode>
|
||||
</span>
|
||||
));
|
||||
};
|
||||
|
||||
export const ShortcutLineFlexItem = ({
|
||||
id,
|
||||
description,
|
||||
keys,
|
||||
alternativeKeys,
|
||||
}: ShortcutLineFlexItemProps) => {
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
{i18n.translate('console.shortcutDescription.' + id, {
|
||||
defaultMessage: description,
|
||||
})}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="xs">
|
||||
{renderKeys(keys)}
|
||||
{alternativeKeys && (
|
||||
<>
|
||||
<strong>
|
||||
{' '}
|
||||
{i18n.translate('console.shortcuts.alternativeKeysOrDivider', {
|
||||
defaultMessage: 'or',
|
||||
})}{' '}
|
||||
</strong>
|
||||
{renderKeys(alternativeKeys)}
|
||||
</>
|
||||
)}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={isOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="downRight"
|
||||
data-test-subj="consoleShortcutsPopover"
|
||||
>
|
||||
<EuiTitle size="xxs">
|
||||
<h5>
|
||||
{i18n.translate('console.shortcuts.navigationShortcutsSubtitle', {
|
||||
defaultMessage: 'Navigation shortcuts',
|
||||
})}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFlexGroup gutterSize="s" direction="column">
|
||||
<ShortcutLineFlexItem
|
||||
id="goToLineNumber"
|
||||
description="Go to line number"
|
||||
keys={[KEYS.keyCtrlCmd, KEYS.keyL]}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiTitle size="xxs">
|
||||
<h5>
|
||||
{i18n.translate('console.shortcuts.requestShortcutsSubtitle', {
|
||||
defaultMessage: 'Request shortcuts',
|
||||
})}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFlexGroup gutterSize="s" direction="column">
|
||||
<ShortcutLineFlexItem
|
||||
id="autoindentCurrentRequest"
|
||||
description="Auto-indent current request"
|
||||
keys={[KEYS.keyCtrlCmd, KEYS.keyI]}
|
||||
/>
|
||||
<ShortcutLineFlexItem
|
||||
id="jumpToNextRequestEnd"
|
||||
description="Jump to next request end"
|
||||
keys={[KEYS.keyCtrlCmd, KEYS.keyDown]}
|
||||
/>
|
||||
<ShortcutLineFlexItem
|
||||
id="jumpToPreviousRequestEnd"
|
||||
description="Jump to previous request end"
|
||||
keys={[KEYS.keyCtrlCmd, KEYS.keyUp]}
|
||||
/>
|
||||
<ShortcutLineFlexItem
|
||||
id="openDocumentation"
|
||||
description="Open documentation for current request"
|
||||
keys={[KEYS.keyCtrlCmd, KEYS.keySlash]}
|
||||
/>
|
||||
<ShortcutLineFlexItem
|
||||
id="runRequest"
|
||||
description="Run current request"
|
||||
keys={[KEYS.keyCtrlCmd, KEYS.keyEnter]}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
|
||||
<EuiTitle size="xxs">
|
||||
<h5>
|
||||
{i18n.translate('console.shortcuts.autocompleteShortcutsSubtitle', {
|
||||
defaultMessage: 'Autocomplete menu shortcuts',
|
||||
})}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiHorizontalRule margin="s" />
|
||||
<EuiFlexGroup gutterSize="s" direction="column">
|
||||
<ShortcutLineFlexItem
|
||||
id="applyCurrentAutocompleteSuggestion"
|
||||
description="Apply current or topmost term in menu"
|
||||
keys={[KEYS.keyEnter]}
|
||||
alternativeKeys={[KEYS.keyTab]}
|
||||
/>
|
||||
<ShortcutLineFlexItem
|
||||
id="closeAutocompleteMenu"
|
||||
description="Close autocomplete menu"
|
||||
keys={[KEYS.keyEsc]}
|
||||
/>
|
||||
<ShortcutLineFlexItem
|
||||
id="navigateAutocompleteMenu"
|
||||
description="Navigate items in autocomplete menu"
|
||||
keys={[KEYS.keyDown]}
|
||||
alternativeKeys={[KEYS.keyUp]}
|
||||
/>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -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<Props> = ({ items, disabled }) => {
|
||||
export const TopNavMenu: FunctionComponent<Props> = ({ items, disabled, tourStepProps }) => {
|
||||
return (
|
||||
<EuiTabs size="s">
|
||||
<EuiTabs size="s" bottomBorder={false}>
|
||||
{items.map((item, idx) => {
|
||||
return (
|
||||
const tab = (
|
||||
<EuiTab
|
||||
key={idx}
|
||||
disabled={disabled}
|
||||
onClick={item.onClick}
|
||||
title={item.label}
|
||||
data-test-subj={item.testId}
|
||||
isSelected={item.isSelected}
|
||||
>
|
||||
{item.label}
|
||||
</EuiTab>
|
||||
);
|
||||
|
||||
if (item.tourStep) {
|
||||
return (
|
||||
<ConsoleTourStep tourStepProps={tourStepProps[item.tourStep - 1]} key={idx}>
|
||||
{tab}
|
||||
</ConsoleTourStep>
|
||||
);
|
||||
}
|
||||
|
||||
return tab;
|
||||
})}
|
||||
</EuiTabs>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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<string | null>(null);
|
||||
const [variables, setVariables] = useState<DevToolsVariable[]>(props.variables);
|
||||
const deleteModalTitleId = useGeneratedHtmlId();
|
||||
|
||||
// Use a ref to persist the BehaviorSubject across renders
|
||||
const itemIdToExpandedRowMap$ = useRef(new BehaviorSubject<Record<string, React.ReactNode>>({}));
|
||||
// Subscribe to the BehaviorSubject and update local state on change
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<
|
||||
Record<string, React.ReactNode>
|
||||
>({});
|
||||
// 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] = (
|
||||
<VariableEditorForm
|
||||
title={i18n.translate('console.variablesPage.editVariableForm.title', {
|
||||
defaultMessage: 'Edit variable',
|
||||
})}
|
||||
onSubmit={(data: DevToolsVariable) => {
|
||||
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<EuiBasicTableColumn<DevToolsVariable>> = [
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate('console.variablesPage.variablesTable.columns.variableHeader', {
|
||||
defaultMessage: 'Variable name',
|
||||
}),
|
||||
'data-test-subj': 'variableNameCell',
|
||||
render: (name: string) => {
|
||||
return <EuiCode>{`\$\{${name}\}`}</EuiCode>;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: i18n.translate('console.variablesPage.variablesTable.columns.valueHeader', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
'data-test-subj': 'variableValueCell',
|
||||
render: (value: string) => <EuiCode>{value}</EuiCode>,
|
||||
},
|
||||
{
|
||||
field: 'id',
|
||||
name: '',
|
||||
width: '40px',
|
||||
isExpander: true,
|
||||
render: (id: string, variable: DevToolsVariable) => {
|
||||
const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap };
|
||||
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
iconType={itemIdToExpandedRowMapValues[id] ? 'arrowUp' : 'pencil'}
|
||||
aria-label={i18n.translate('console.variablesPage.variablesTable.columns.editButton', {
|
||||
defaultMessage: 'Edit {variable}',
|
||||
values: { variable: variable.name },
|
||||
})}
|
||||
color="primary"
|
||||
onClick={() => toggleDetails(id)}
|
||||
data-test-subj="variableEditButton"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'id',
|
||||
name: '',
|
||||
width: '40px',
|
||||
render: (id: string, variable: DevToolsVariable) => (
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
aria-label={i18n.translate('console.variablesPage.variablesTable.columns.deleteButton', {
|
||||
defaultMessage: 'Delete {variable}',
|
||||
values: { variable: variable.name },
|
||||
})}
|
||||
color="danger"
|
||||
onClick={() => setDeleteModalForVariable(id)}
|
||||
data-test-subj="variablesRemoveButton"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
<FormattedMessage id="console.variablesPage.pageTitle" defaultMessage="Variables" />
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.variablesPage.pageDescription"
|
||||
defaultMessage="Define reusable placeholders for dynamic values in your queries."
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiBasicTable
|
||||
items={variables}
|
||||
columns={columns}
|
||||
itemId="id"
|
||||
responsiveBreakpoint={false}
|
||||
className="conVariablesTable"
|
||||
data-test-subj="variablesTable"
|
||||
itemIdToExpandedRowMap={itemIdToExpandedRowMap}
|
||||
noItemsMessage={i18n.translate('console.variablesPage.table.noItemsMessage', {
|
||||
defaultMessage: 'No variables have been added yet',
|
||||
})}
|
||||
/>
|
||||
|
||||
{isAddingVariable && (
|
||||
<VariableEditorForm onSubmit={onAddVariable} onCancel={() => setIsAddingVariable(false)} />
|
||||
)}
|
||||
|
||||
<EuiSpacer size="m" />
|
||||
|
||||
<div>
|
||||
<EuiButton
|
||||
data-test-subj="variablesAddButton"
|
||||
iconType="plusInCircle"
|
||||
onClick={() => {
|
||||
setIsAddingVariable(true);
|
||||
collapseExpandedRows();
|
||||
}}
|
||||
disabled={isAddingVariable}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="console.variablesPage.addButtonLabel"
|
||||
defaultMessage="Add variable"
|
||||
/>
|
||||
</EuiButton>
|
||||
</div>
|
||||
|
||||
{deleteModalForVariable && (
|
||||
<EuiConfirmModal
|
||||
aria-labelledby={deleteModalTitleId}
|
||||
title={i18n.translate('console.variablesPage.deleteModal.title', {
|
||||
defaultMessage: 'Are you sure?',
|
||||
})}
|
||||
onCancel={() => 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"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.variablesPage.deleteModal.description"
|
||||
defaultMessage="Deleting a variable is irreversible."
|
||||
/>
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<string, FieldConfig> = {
|
||||
variableName: {
|
||||
label: i18n.translate('console.variablesPage.form.variableNameFieldLabel', {
|
||||
defaultMessage: 'Variable name',
|
||||
}),
|
||||
validations: [
|
||||
{
|
||||
validator: ({ value }: ValidationFuncArg<any, any>) => {
|
||||
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 (
|
||||
<>
|
||||
<EuiPanel
|
||||
paddingSize="l"
|
||||
hasShadow={false}
|
||||
borderRadius="none"
|
||||
grow={false}
|
||||
css={{ width: '100%' }}
|
||||
>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{props.title ?? (
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a new variable"
|
||||
id="console.variablesPage.addNewVariableTitle"
|
||||
/>
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<Form form={form}>
|
||||
<UseField
|
||||
config={fieldsConfig.variableName}
|
||||
path="name"
|
||||
component={TextField}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'nameField',
|
||||
placeholder: i18n.translate('console.variablesPage.form.namePlaceholderLabel', {
|
||||
defaultMessage: 'exampleName',
|
||||
}),
|
||||
prepend: '${',
|
||||
append: '}',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<UseField
|
||||
config={fieldsConfig.value}
|
||||
path="value"
|
||||
component={TextField}
|
||||
componentProps={{
|
||||
euiFieldProps: {
|
||||
'data-test-subj': 'valueField',
|
||||
placeholder: i18n.translate('console.variablesPage.form.valuePlaceholderLabel', {
|
||||
defaultMessage: 'exampleValue',
|
||||
}),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<EuiSpacer size="l" />
|
||||
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={() => props.onCancel()}>
|
||||
<FormattedMessage
|
||||
id="console.variablesPage.addNew.cancelButton"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="save"
|
||||
onClick={form.submit}
|
||||
data-test-subj="addNewVariableButton"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="console.variablesPage.addNew.submitButton"
|
||||
defaultMessage="Save changes"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Form>
|
||||
</EuiPanel>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<DevToolsVariable[]>(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<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
props.onSaveVariables(variables.filter(({ name, value }) => name.trim() && value));
|
||||
},
|
||||
[props, variables]
|
||||
);
|
||||
|
||||
const onChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>, id: string) => {
|
||||
const { name, value } = event.target;
|
||||
const editedVariables = utils.editVariable(name, value, id, variables);
|
||||
setVariables(editedVariables);
|
||||
},
|
||||
[variables]
|
||||
);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<DevToolsVariable>> = [
|
||||
{
|
||||
field: 'name',
|
||||
name: i18n.translate('console.variablesPage.variablesTable.columns.variableHeader', {
|
||||
defaultMessage: 'Variable name',
|
||||
}),
|
||||
render: (name, { id }) => {
|
||||
const isInvalid = !utils.isValidVariableName(name);
|
||||
return (
|
||||
<EuiFormRow
|
||||
isInvalid={isInvalid}
|
||||
error={[
|
||||
<FormattedMessage
|
||||
id="console.variablesPage.variablesTable.variableInputError.validCharactersText"
|
||||
defaultMessage="Only letters, numbers and underscores are allowed"
|
||||
/>,
|
||||
]}
|
||||
fullWidth={true}
|
||||
css={{ flexGrow: 1 }}
|
||||
>
|
||||
<EuiFieldText
|
||||
data-test-subj="variablesNameInput"
|
||||
name="name"
|
||||
value={name}
|
||||
onChange={(e) => onChange(e, id)}
|
||||
isInvalid={isInvalid}
|
||||
fullWidth={true}
|
||||
aria-label={i18n.translate(
|
||||
'console.variablesPage.variablesTable.variableInput.ariaLabel',
|
||||
{
|
||||
defaultMessage: 'Variable name',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'value',
|
||||
name: i18n.translate('console.variablesPage.variablesTable.columns.valueHeader', {
|
||||
defaultMessage: 'Value',
|
||||
}),
|
||||
render: (value, { id }) => (
|
||||
<EuiFieldText
|
||||
data-test-subj="variablesValueInput"
|
||||
name="value"
|
||||
onChange={(e) => 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) => (
|
||||
<EuiButtonIcon
|
||||
iconType="trash"
|
||||
aria-label="Delete"
|
||||
color="danger"
|
||||
onClick={() => deleteVariable(id)}
|
||||
data-test-subj="variablesRemoveButton"
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={props.onClose}>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
<FormattedMessage id="console.variablesPage.pageTitle" defaultMessage="Variables" />
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.variablesPage.descriptionText"
|
||||
defaultMessage="Define variables and use them in your requests in the form of {variable}."
|
||||
values={{
|
||||
variable: (
|
||||
<code>
|
||||
<FormattedMessage
|
||||
id="console.variablesPage.descriptionText.variableNameText"
|
||||
defaultMessage="{variableName}"
|
||||
values={{
|
||||
variableName: '${variableName}',
|
||||
}}
|
||||
/>
|
||||
</code>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiForm id={formId} component="form" onSubmit={onSubmit}>
|
||||
<EuiBasicTable items={variables} columns={columns} />
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="variablesAddButton"
|
||||
iconType="plus"
|
||||
onClick={addNewVariable}
|
||||
>
|
||||
<FormattedMessage id="console.variablesPage.addButtonLabel" defaultMessage="Add" />
|
||||
</EuiButtonEmpty>
|
||||
</EuiForm>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty data-test-subj="variablesCancelButton" onClick={props.onClose}>
|
||||
<FormattedMessage
|
||||
id="console.variablesPage.cancelButtonLabel"
|
||||
defaultMessage="Cancel"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill data-test-subj="variablesSaveButton" type="submit" form={formId}>
|
||||
<FormattedMessage id="console.variablesPage.saveButtonLabel" defaultMessage="Save" />
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
};
|
|
@ -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 (
|
||||
<EuiFlyout
|
||||
onClose={props.onDismiss}
|
||||
data-test-subj="welcomePanel"
|
||||
size="m"
|
||||
maxWidth={0}
|
||||
ownFocus={false}
|
||||
>
|
||||
<EuiFlyoutHeader hasBorder>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="console.welcomePage.pageTitle"
|
||||
defaultMessage="Send requests with Console"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody>
|
||||
<EuiText>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.welcomePage.quickIntroDescription"
|
||||
defaultMessage="Console understands commands in a cURL-like syntax. Here is a request to the Elasticsearch _search API."
|
||||
/>
|
||||
</p>
|
||||
<EditorExample panel="welcome-example-1" />
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.welcomePage.kibanaAPIsDescription"
|
||||
defaultMessage="To send a request to a Kibana API, prefix the path with {kibanaApiPrefix}."
|
||||
values={{
|
||||
kibanaApiPrefix: <EuiCode>kbn:</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<EditorExample
|
||||
panel="welcome-example-2"
|
||||
example={examples.kibanaApiExample}
|
||||
linesOfExampleCode={2}
|
||||
/>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="console.welcomePage.sendMultipleRequestsTitle"
|
||||
defaultMessage="Send multiple requests"
|
||||
/>
|
||||
</h4>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.welcomePage.sendMultipleRequestsDescription"
|
||||
defaultMessage="Select multiple requests and send them together. You'll get responses to all your requests, regardless of whether they succeed or fail."
|
||||
/>
|
||||
</p>
|
||||
<EditorExample
|
||||
panel="welcome-example-3"
|
||||
example={examples.multipleRequestsExample}
|
||||
linesOfExampleCode={22}
|
||||
mode="output"
|
||||
/>
|
||||
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="console.welcomePage.addCommentsTitle"
|
||||
defaultMessage="Add comments in request bodies"
|
||||
/>
|
||||
</h4>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.welcomePage.addCommentsDescription"
|
||||
defaultMessage="To add a single-line comment, use {hash} or {doubleSlash}. For a multiline comment, mark the beginning with {slashAsterisk} and the end with {asteriskSlash}."
|
||||
values={{
|
||||
hash: <EuiCode>#</EuiCode>,
|
||||
doubleSlash: <EuiCode>//</EuiCode>,
|
||||
slashAsterisk: <EuiCode>/*</EuiCode>,
|
||||
asteriskSlash: <EuiCode>*/</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<EditorExample
|
||||
panel="welcome-example-4"
|
||||
example={examples.commentsExample}
|
||||
linesOfExampleCode={14}
|
||||
/>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
id="console.welcomePage.useVariablesTitle"
|
||||
defaultMessage="Reuse values with variables"
|
||||
/>
|
||||
</h4>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.welcomePage.useVariablesDescription"
|
||||
defaultMessage="Define variables in Console, and then use them in your requests in the form of {variableName}."
|
||||
values={{
|
||||
// use html tags to render the curly braces
|
||||
variableName: <EuiCode>${variableName}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<ol>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="console.welcomePage.useVariables.step1"
|
||||
defaultMessage="Click {variableText}, and then enter the variable name and value."
|
||||
values={{
|
||||
variableText: <strong>Variables</strong>,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<FormattedMessage
|
||||
id="console.welcomePage.useVariables.step2"
|
||||
defaultMessage="Refer to variables in the paths and bodies of your requests as many times as you like."
|
||||
/>
|
||||
</li>
|
||||
</ol>
|
||||
<EditorExample
|
||||
panel="welcome-example-5"
|
||||
example={examples.variablesExample}
|
||||
linesOfExampleCode={9}
|
||||
/>
|
||||
</EuiText>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiButton
|
||||
fill={true}
|
||||
fullWidth={false}
|
||||
data-test-subj="help-close-button"
|
||||
onClick={props.onDismiss}
|
||||
>
|
||||
<FormattedMessage id="console.welcomePage.closeButtonLabel" defaultMessage="Dismiss" />
|
||||
</EuiButton>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<EuiPanel
|
||||
color="subdued"
|
||||
paddingSize="l"
|
||||
hasShadow={false}
|
||||
borderRadius="none"
|
||||
css={{ height: '100%' }}
|
||||
data-test-subj="consoleConfigPanel"
|
||||
>
|
||||
<EuiFlexGroup
|
||||
gutterSize="xl"
|
||||
direction={isVerticalLayout ? 'column' : 'row'}
|
||||
// Turn off default responsiveness
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<Settings />
|
||||
<EuiSpacer size="m" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<Variables />
|
||||
<EuiSpacer size="m" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -7,4 +7,4 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export * from './output_data';
|
||||
export { Config } from './config';
|
|
@ -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 (
|
||||
<DevToolsSettingsModal
|
||||
onClose={onClose}
|
||||
<SettingsEditor
|
||||
onSaveSettings={onSaveSettings}
|
||||
refreshAutocompleteSettings={(selectedSettings) =>
|
||||
refreshAutocompleteSettings(settings, selectedSettings)
|
||||
}
|
||||
settings={settings.toJSON()}
|
||||
editorInstance={editorInstance}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<DevToolsVariablesFlyout
|
||||
onClose={onClose}
|
||||
<VariablesEditor
|
||||
onSaveVariables={onSaveVariables}
|
||||
variables={storage.get(StorageKeys.VARIABLES, DEFAULT_VARIABLES)}
|
||||
/>
|
|
@ -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<any[]>(history.getHistory());
|
||||
|
||||
const clearHistory = useCallback(() => {
|
||||
history.clearHistory();
|
||||
setPastRequests(history.getHistory());
|
||||
}, [history]);
|
||||
|
||||
const listRef = useRef<HTMLUListElement | null>(null);
|
||||
|
||||
const [viewingReq, setViewingReq] = useState<any>(null);
|
||||
const [selectedIndex, setSelectedIndex] = useState<number>(0);
|
||||
const selectedReq = useRef<any>(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 (
|
||||
<>
|
||||
<div className="conHistory">
|
||||
<EuiTitle size="s">
|
||||
<h2>{i18n.translate('console.historyPage.pageTitle', { defaultMessage: 'History' })}</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<div className="conHistory__body">
|
||||
<ul
|
||||
ref={listRef}
|
||||
onKeyDown={(ev: React.KeyboardEvent) => {
|
||||
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
|
||||
<li
|
||||
key={idx}
|
||||
id={`${CHILD_ELEMENT_PREFIX}${idx}`}
|
||||
className={`list-group-item conHistory__req ${
|
||||
isSelected ? 'conHistory__req-selected' : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
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}
|
||||
<span className="conHistory__reqIcon">
|
||||
<EuiIcon type="arrowRight" />
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<div className="conHistory__body__spacer" />
|
||||
|
||||
{isMonacoEnabled ? (
|
||||
<HistoryViewerMonaco settings={readOnlySettings} req={viewingReq} />
|
||||
) : (
|
||||
<HistoryViewer settings={readOnlySettings} req={viewingReq} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="consoleClearHistoryButton"
|
||||
color="danger"
|
||||
onClick={() => clear()}
|
||||
>
|
||||
{i18n.translate('console.historyPage.clearHistoryButtonLabel', {
|
||||
defaultMessage: 'Clear',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="consoleHistoryCloseButton"
|
||||
color="primary"
|
||||
onClick={() => close()}
|
||||
>
|
||||
{i18n.translate('console.historyPage.closehistoryButtonLabel', {
|
||||
defaultMessage: 'Close',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="consoleHistoryApplyButton"
|
||||
color="primary"
|
||||
disabled={!selectedReq}
|
||||
onClick={() => restoreRequestFromHistory(selectedReq.current)}
|
||||
>
|
||||
{i18n.translate('console.historyPage.applyHistoryButtonLabel', {
|
||||
defaultMessage: 'Apply',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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<HTMLDivElement | null>(null);
|
||||
const viewerRef = useRef<editor.CustomAceEditor | null>(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 <div className="conHistory__viewer" ref={divRef} />;
|
||||
}
|
|
@ -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<EditorRequest[]>;
|
||||
|
@ -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 = (
|
||||
<EuiLink
|
||||
<EuiButtonIcon
|
||||
onClick={() => setIsPopoverOpen((prev) => !prev)}
|
||||
data-test-subj="toggleConsoleMenu"
|
||||
aria-label={i18n.translate('console.requestOptionsButtonAriaLabel', {
|
||||
defaultMessage: 'Request options',
|
||||
})}
|
||||
>
|
||||
<EuiIcon type="boxesVertical" />
|
||||
</EuiLink>
|
||||
iconType="boxesVertical"
|
||||
iconSize="s"
|
||||
/>
|
||||
);
|
||||
|
||||
const items = [
|
||||
|
@ -187,10 +202,11 @@ export const ContextMenu = ({
|
|||
onCopyAsSubmit();
|
||||
}}
|
||||
icon="copyClipboard"
|
||||
css={styles.button}
|
||||
>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center">
|
||||
<EuiFlexGroup gutterSize="xs" alignItems="center" className="languageSelector">
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
tagName="span"
|
|
@ -24,7 +24,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { AVAILABLE_LANGUAGES } from '../../../../../../../common/constants';
|
||||
import { AVAILABLE_LANGUAGES } from '../../../../../../common/constants';
|
||||
|
||||
interface Props {
|
||||
closeModal: () => void;
|
|
@ -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();
|
||||
export const Editor = memo(
|
||||
({ loading, isVerticalLayout, inputEditorValue, setInputEditorValue }: Props) => {
|
||||
const {
|
||||
services: { storage, objectStorageClient },
|
||||
} = useServicesContext();
|
||||
|
||||
const { currentTextObject } = useEditorReadContext();
|
||||
const { requestInFlight } = useRequestReadContext();
|
||||
const editorValueRef = useRef<TextObject | null>(null);
|
||||
const { currentTextObject } = useEditorReadContext();
|
||||
const {
|
||||
requestInFlight,
|
||||
lastResult: { data: requestData, error: requestError },
|
||||
} = useRequestReadContext();
|
||||
|
||||
const [fetchingMappings, setFetchingMappings] = useState(false);
|
||||
const dispatch = useRequestActionContext();
|
||||
const editorDispatch = useEditorActionContext();
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = getAutocompleteInfo().mapping.isLoading$.subscribe(setFetchingMappings);
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
const [fetchingAutocompleteEntities, setFetchingAutocompleteEntities] = useState(false);
|
||||
|
||||
const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [
|
||||
INITIAL_PANEL_WIDTH,
|
||||
INITIAL_PANEL_WIDTH,
|
||||
]);
|
||||
useEffect(() => {
|
||||
const debouncedSetFechingAutocompleteEntities = debounce(
|
||||
setFetchingAutocompleteEntities,
|
||||
DEBOUNCE_DELAY
|
||||
);
|
||||
const subscription = getAutocompleteInfo().isLoading$.subscribe(
|
||||
debouncedSetFechingAutocompleteEntities
|
||||
);
|
||||
|
||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
||||
const onPanelWidthChange = useCallback(
|
||||
debounce((widths: number[]) => {
|
||||
storage.set(StorageKeys.WIDTH, widths);
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
debouncedSetFechingAutocompleteEntities.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!currentTextObject) return null;
|
||||
const [firstPanelSize, secondPanelSize] = storage.get(StorageKeys.SIZE, [
|
||||
INITIAL_PANEL_SIZE,
|
||||
INITIAL_PANEL_SIZE,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{requestInFlight || fetchingMappings ? (
|
||||
<div className="conApp__requestProgressBarContainer">
|
||||
<EuiProgress size="xs" color="accent" position="absolute" />
|
||||
</div>
|
||||
) : null}
|
||||
<PanelsContainer onPanelWidthChange={onPanelWidthChange} resizerClassName="conApp__resizer">
|
||||
<Panel
|
||||
style={{ height: '100%', position: 'relative', minWidth: PANEL_MIN_WIDTH }}
|
||||
initialWidth={firstPanelWidth}
|
||||
/* 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 ? (
|
||||
<div className="conApp__requestProgressBarContainer">
|
||||
<EuiProgress size="xs" color="accent" position="absolute" />
|
||||
</div>
|
||||
) : null}
|
||||
<EuiResizableContainer
|
||||
style={{ height: '100%' }}
|
||||
direction={isVerticalLayout ? 'vertical' : 'horizontal'}
|
||||
onPanelWidthChange={(sizes) => onPanelSizeChange(sizes)}
|
||||
data-test-subj="consoleEditorContainer"
|
||||
>
|
||||
{loading ? (
|
||||
<EditorContentSpinner />
|
||||
) : isMonacoEnabled ? (
|
||||
<MonacoEditor initialTextValue={currentTextObject.text} />
|
||||
) : (
|
||||
<EditorUI
|
||||
initialTextValue={currentTextObject.text}
|
||||
setEditorInstance={setEditorInstance}
|
||||
/>
|
||||
{(EuiResizablePanel, EuiResizableButton) => (
|
||||
<>
|
||||
<EuiResizablePanel
|
||||
initialSize={firstPanelSize}
|
||||
minSize={PANEL_MIN_SIZE}
|
||||
tabIndex={0}
|
||||
paddingSize="none"
|
||||
>
|
||||
<EuiSplitPanel.Outer
|
||||
grow={true}
|
||||
borderRadius="none"
|
||||
hasShadow={false}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<EuiSplitPanel.Inner
|
||||
paddingSize="none"
|
||||
grow={true}
|
||||
className="consoleEditorPanel"
|
||||
style={{ top: 0, height: 'calc(100% - 40px)' }}
|
||||
>
|
||||
{loading ? (
|
||||
<EditorContentSpinner />
|
||||
) : (
|
||||
<MonacoEditor
|
||||
localStorageValue={currentTextObject.text}
|
||||
value={inputEditorValue}
|
||||
setValue={setInputEditorValue}
|
||||
/>
|
||||
)}
|
||||
</EuiSplitPanel.Inner>
|
||||
|
||||
{!loading && (
|
||||
<EuiSplitPanel.Inner
|
||||
grow={false}
|
||||
paddingSize="s"
|
||||
css={{
|
||||
backgroundColor: euiThemeVars.euiFormBackgroundColor,
|
||||
}}
|
||||
className="consoleEditorPanel"
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
color="primary"
|
||||
data-test-subj="clearConsoleInput"
|
||||
onClick={() => setInputEditorValue('')}
|
||||
>
|
||||
{i18n.translate('console.editor.clearConsoleInputButton', {
|
||||
defaultMessage: 'Clear this input',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiSplitPanel.Inner>
|
||||
)}
|
||||
</EuiSplitPanel.Outer>
|
||||
</EuiResizablePanel>
|
||||
|
||||
<EuiResizableButton
|
||||
className="conApp__resizerButton"
|
||||
aria-label={i18n.translate('console.editor.adjustPanelSizeAriaLabel', {
|
||||
defaultMessage: "Press left/right to adjust panels' sizes",
|
||||
})}
|
||||
/>
|
||||
|
||||
<EuiResizablePanel
|
||||
initialSize={secondPanelSize}
|
||||
minSize={PANEL_MIN_SIZE}
|
||||
tabIndex={0}
|
||||
paddingSize="none"
|
||||
>
|
||||
<EuiSplitPanel.Outer
|
||||
borderRadius="none"
|
||||
hasShadow={false}
|
||||
style={{ height: '100%' }}
|
||||
>
|
||||
<EuiSplitPanel.Inner
|
||||
paddingSize="none"
|
||||
css={{ alignContent: 'center', top: 0 }}
|
||||
className="consoleEditorPanel"
|
||||
>
|
||||
{data ? (
|
||||
<MonacoEditorOutput />
|
||||
) : isLoading ? (
|
||||
<EditorContentSpinner />
|
||||
) : (
|
||||
<OutputPanelEmptyState />
|
||||
)}
|
||||
</EuiSplitPanel.Inner>
|
||||
|
||||
{(data || isLoading) && (
|
||||
<EuiSplitPanel.Inner
|
||||
grow={false}
|
||||
paddingSize="s"
|
||||
css={{
|
||||
backgroundColor: euiThemeVars.euiFormBackgroundColor,
|
||||
}}
|
||||
className="consoleEditorPanel"
|
||||
>
|
||||
<EuiFlexGroup gutterSize="none" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
color="primary"
|
||||
data-test-subj="clearConsoleOutput"
|
||||
onClick={() => dispatch({ type: 'cleanRequest', payload: undefined })}
|
||||
>
|
||||
{i18n.translate('console.editor.clearConsoleOutputButton', {
|
||||
defaultMessage: 'Clear this output',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
<NetworkRequestStatusBar
|
||||
requestInProgress={requestInFlight}
|
||||
requestResult={
|
||||
data
|
||||
? {
|
||||
method: data.request.method.toUpperCase(),
|
||||
endpoint: data.request.path,
|
||||
statusCode: data.response.statusCode,
|
||||
statusText: data.response.statusText,
|
||||
timeElapsedMs: data.response.timeMs,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
)}
|
||||
</EuiSplitPanel.Outer>
|
||||
</EuiResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</Panel>
|
||||
<Panel
|
||||
style={{ height: '100%', position: 'relative', minWidth: PANEL_MIN_WIDTH }}
|
||||
initialWidth={secondPanelWidth}
|
||||
>
|
||||
{loading ? (
|
||||
<EditorContentSpinner />
|
||||
) : isMonacoEnabled ? (
|
||||
<MonacoEditorOutput />
|
||||
) : (
|
||||
<EditorOutput />
|
||||
)}
|
||||
</Panel>
|
||||
</PanelsContainer>
|
||||
</>
|
||||
);
|
||||
});
|
||||
</EuiResizableContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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]);
|
||||
};
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { AutocompleteInfo, Settings } from '../../../../../services';
|
||||
import { AutocompleteInfo, Settings } from '../../../../services';
|
||||
|
||||
interface SetupAutocompletePollingParams {
|
||||
/** The Console autocomplete service. */
|
|
@ -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 {
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
}));
|
|
@ -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(
|
||||
<I18nProvider>
|
||||
<ServicesContextProvider value={mockedAppContextValue}>
|
||||
<RequestContextProvider>
|
||||
<EditorContextProvider settings={{} as unknown as DevToolsSettings}>
|
||||
<Editor initialTextValue="" setEditorInstance={() => {}} />
|
||||
</EditorContextProvider>
|
||||
</RequestContextProvider>
|
||||
</ServicesContextProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -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<HTMLDivElement | null>(null);
|
||||
const editorInstanceRef = useRef<senseEditor.SenseEditor | null>(null);
|
||||
|
||||
const [textArea, setTextArea] = useState<HTMLTextAreaElement | null>(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<QueryParams>;
|
||||
};
|
||||
|
||||
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<string, any> = {
|
||||
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 (
|
||||
<div style={abs} data-test-subj="console-application" className="conApp">
|
||||
<div className="conApp__editor">
|
||||
<ul className="conApp__autoComplete" id="autocomplete" />
|
||||
<EuiFlexGroup
|
||||
className="conApp__editorActions"
|
||||
id="ConAppEditorActions"
|
||||
gutterSize="none"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('console.sendRequestButtonTooltipContent', {
|
||||
defaultMessage: 'Click to send request',
|
||||
})}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
onClick={sendCurrentRequest}
|
||||
data-test-subj="sendRequestButton"
|
||||
aria-label={i18n.translate('console.sendRequestButtonTooltipAriaLabel', {
|
||||
defaultMessage: 'Click to send request',
|
||||
})}
|
||||
>
|
||||
<EuiIcon type="play" />
|
||||
</EuiLink>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ConsoleMenu
|
||||
getCurl={() => {
|
||||
return editorInstanceRef.current!.getRequestsAsCURL(esHostService.getHost());
|
||||
}}
|
||||
getDocumentation={() => {
|
||||
return getDocumentation(editorInstanceRef.current!, docLinkVersion);
|
||||
}}
|
||||
autoIndent={(event) => {
|
||||
autoIndent(editorInstanceRef.current!, event);
|
||||
}}
|
||||
notifications={notifications}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiScreenReaderOnly>
|
||||
<label htmlFor={inputId}>
|
||||
{i18n.translate('console.inputTextarea', {
|
||||
defaultMessage: 'Dev Tools Console',
|
||||
})}
|
||||
</label>
|
||||
</EuiScreenReaderOnly>
|
||||
<div
|
||||
ref={editorRef}
|
||||
id="ConAppEditor"
|
||||
className="conApp__editorContent"
|
||||
data-test-subj="request-editor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Editor = React.memo(EditorUI);
|
|
@ -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 | HTMLDivElement>(null);
|
||||
const editorInstanceRef = useRef<null | CustomAceEditor>(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 (
|
||||
<>
|
||||
<EuiScreenReaderOnly>
|
||||
<label htmlFor={inputId}>
|
||||
{i18n.translate('console.outputTextarea', {
|
||||
defaultMessage: 'Dev Tools Console output',
|
||||
})}
|
||||
</label>
|
||||
</EuiScreenReaderOnly>
|
||||
<div ref={editorRef} className="conApp__output" data-test-subj="response-editor">
|
||||
<div className="conApp__outputContent" id="ConAppOutput" />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const EditorOutput = React.memo(EditorOutputUI);
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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<string | null> {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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 { EditorOutput, Editor } from './console_editor';
|
||||
export { getDocumentation, autoIndent } from './console_menu_actions';
|
|
@ -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();
|
||||
}
|
|
@ -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';
|
|
@ -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<MonacoEditorActionsProvider | null>(null);
|
||||
const [editorActionsCss, setEditorActionsCss] = useState<CSSProperties>({});
|
||||
|
||||
|
@ -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 (
|
||||
<div
|
||||
css={css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`}
|
||||
ref={divRef}
|
||||
data-test-subj="consoleMonacoEditorContainer"
|
||||
|
@ -136,29 +166,30 @@ export const MonacoEditor = ({ initialTextValue }: EditorProps) => {
|
|||
<EuiFlexGroup
|
||||
className="conApp__editorActions"
|
||||
id="ConAppEditorActions"
|
||||
gutterSize="none"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
style={editorActionsCss}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('console.monaco.sendRequestButtonTooltipContent', {
|
||||
defaultMessage: 'Click to send request',
|
||||
})}
|
||||
>
|
||||
<EuiLink
|
||||
color="primary"
|
||||
<EuiButtonIcon
|
||||
iconType="playFilled"
|
||||
onClick={sendRequestsCallback}
|
||||
data-test-subj="sendRequestButton"
|
||||
aria-label={i18n.translate('console.monaco.sendRequestButtonTooltipAriaLabel', {
|
||||
defaultMessage: 'Click to send request',
|
||||
})}
|
||||
>
|
||||
<EuiIcon type="play" />
|
||||
</EuiLink>
|
||||
iconSize={'s'}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ContextMenu
|
||||
getRequests={getRequestsCallback}
|
||||
getDocumentation={getDocumenationLink}
|
|
@ -29,7 +29,7 @@ jest.mock('@kbn/monaco', () => {
|
|||
};
|
||||
});
|
||||
|
||||
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);
|
|
@ -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<AdjustedParsedRequest[]> {
|
||||
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<AdjustedParsedRequest[]> {
|
||||
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<Actions>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 { 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 {
|
||||
EuiScreenReaderOnly,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonIcon,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { CONSOLE_THEME_ID, CONSOLE_OUTPUT_LANG_ID, monaco } from '@kbn/monaco';
|
||||
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<monaco.editor.IEditorDecorationsCollection | null>(null);
|
||||
|
||||
const actionsProvider = useRef<MonacoEditorOutputActionsProvider | null>(null);
|
||||
const [editorActionsCss, setEditorActionsCss] = useState<CSSProperties>({});
|
||||
|
||||
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 (
|
||||
<div
|
||||
css={css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`}
|
||||
ref={divRef}
|
||||
>
|
||||
<EuiFlexGroup
|
||||
className="conApp__outputActions"
|
||||
responsive={false}
|
||||
style={editorActionsCss}
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiToolTip
|
||||
content={i18n.translate('console.outputPanel.copyOutputButtonTooltipContent', {
|
||||
defaultMessage: 'Click to copy to clipboard',
|
||||
})}
|
||||
>
|
||||
<EuiButtonIcon
|
||||
iconType="copyClipboard"
|
||||
onClick={copyOutputCallback}
|
||||
data-test-subj="copyOutputButton"
|
||||
aria-label={i18n.translate('console.outputPanel.copyOutputButtonTooltipAriaLabel', {
|
||||
defaultMessage: 'Click to copy to clipboard',
|
||||
})}
|
||||
iconSize={'s'}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiScreenReaderOnly>
|
||||
<label htmlFor={'ConAppOutputTextarea'}>
|
||||
{i18n.translate('console.monaco.outputTextarea', {
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* 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 { CSSProperties } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import { createOutputParser } from '@kbn/monaco/src/console/output_parser';
|
||||
|
||||
import {
|
||||
getRequestEndLineNumber,
|
||||
getRequestStartLineNumber,
|
||||
SELECTED_REQUESTS_CLASSNAME,
|
||||
} from './utils';
|
||||
|
||||
import type { AdjustedParsedRequest } from './types';
|
||||
|
||||
const DEBOUNCE_HIGHLIGHT_WAIT_MS = 200;
|
||||
const OFFSET_EDITOR_ACTIONS = 1;
|
||||
|
||||
export class MonacoEditorOutputActionsProvider {
|
||||
private highlightedLines: monaco.editor.IEditorDecorationsCollection;
|
||||
constructor(
|
||||
private editor: monaco.editor.IStandaloneCodeEditor,
|
||||
private setEditorActionsCss: (css: CSSProperties) => void
|
||||
) {
|
||||
this.highlightedLines = this.editor.createDecorationsCollection();
|
||||
this.editor.focus();
|
||||
|
||||
const debouncedHighlightRequests = debounce(
|
||||
() => this.highlightRequests(),
|
||||
DEBOUNCE_HIGHLIGHT_WAIT_MS,
|
||||
{
|
||||
leading: true,
|
||||
}
|
||||
);
|
||||
debouncedHighlightRequests();
|
||||
|
||||
// init all listeners
|
||||
editor.onDidChangeCursorPosition(async (event) => {
|
||||
await debouncedHighlightRequests();
|
||||
});
|
||||
editor.onDidScrollChange(async (event) => {
|
||||
await debouncedHighlightRequests();
|
||||
});
|
||||
editor.onDidChangeCursorSelection(async (event) => {
|
||||
await debouncedHighlightRequests();
|
||||
});
|
||||
editor.onDidContentSizeChange(async (event) => {
|
||||
await debouncedHighlightRequests();
|
||||
});
|
||||
}
|
||||
|
||||
private updateEditorActions(lineNumber?: number) {
|
||||
// if no request is currently selected, hide the actions buttons
|
||||
if (!lineNumber) {
|
||||
this.setEditorActionsCss({
|
||||
visibility: 'hidden',
|
||||
});
|
||||
} else {
|
||||
// if a request is selected, the actions buttons are placed at lineNumberOffset - scrollOffset
|
||||
const offset = this.editor.getTopForLineNumber(lineNumber) - this.editor.getScrollTop();
|
||||
this.setEditorActionsCss({
|
||||
visibility: 'visible',
|
||||
// Add a little bit of padding to the top of the actions buttons so that
|
||||
// it doesnt overlap with the selected request delimiter
|
||||
top: offset + OFFSET_EDITOR_ACTIONS,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async highlightRequests(): Promise<void> {
|
||||
// get the requests in the selected range
|
||||
const parsedRequests = await this.getSelectedParsedOutput();
|
||||
// if any requests are selected, highlight the lines and update the position of actions buttons
|
||||
if (parsedRequests.length > 0) {
|
||||
// display the actions buttons on the 1st line of the 1st selected request
|
||||
const selectionStartLineNumber = parsedRequests[0].startLineNumber;
|
||||
this.updateEditorActions(selectionStartLineNumber); // highlight the lines from the 1st line of the first selected request
|
||||
// to the last line of the last selected request
|
||||
const selectionEndLineNumber = parsedRequests[parsedRequests.length - 1].endLineNumber;
|
||||
const selectedRange = new monaco.Range(
|
||||
selectionStartLineNumber,
|
||||
1,
|
||||
selectionEndLineNumber,
|
||||
this.editor.getModel()?.getLineMaxColumn(selectionEndLineNumber) ?? 1
|
||||
);
|
||||
this.highlightedLines.set([
|
||||
{
|
||||
range: selectedRange,
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
blockClassName: SELECTED_REQUESTS_CLASSNAME,
|
||||
},
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
// if no requests are selected, hide actions buttons and remove highlighted lines
|
||||
this.updateEditorActions();
|
||||
this.highlightedLines.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private async getSelectedParsedOutput(): Promise<AdjustedParsedRequest[]> {
|
||||
const model = this.editor.getModel();
|
||||
const selection = this.editor.getSelection();
|
||||
|
||||
if (!model || !selection) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const { startLineNumber, endLineNumber } = selection;
|
||||
return this.getRequestsBetweenLines(model, startLineNumber, endLineNumber);
|
||||
}
|
||||
|
||||
private async getRequestsBetweenLines(
|
||||
model: monaco.editor.ITextModel,
|
||||
startLineNumber: number,
|
||||
endLineNumber: number
|
||||
): Promise<AdjustedParsedRequest[]> {
|
||||
const parser = createOutputParser();
|
||||
const parsedRequests = await parser(model.getValue(), undefined).responses;
|
||||
|
||||
const selectedRequests: AdjustedParsedRequest[] = [];
|
||||
for (const [index, parsedRequest] of parsedRequests.entries()) {
|
||||
const requestStartLineNumber = getRequestStartLineNumber(parsedRequest, model);
|
||||
const requestEndLineNumber = getRequestEndLineNumber({
|
||||
parsedRequest,
|
||||
nextRequest: parsedRequests.at(index + 1),
|
||||
model,
|
||||
startLineNumber,
|
||||
});
|
||||
if (requestStartLineNumber > endLineNumber) {
|
||||
// request is past the selection, no need to check further requests
|
||||
break;
|
||||
}
|
||||
if (requestEndLineNumber < startLineNumber) {
|
||||
// request is before the selection, do nothing
|
||||
} else {
|
||||
// request is selected
|
||||
selectedRequests.push({
|
||||
...parsedRequest,
|
||||
startLineNumber: requestStartLineNumber,
|
||||
endLineNumber: requestEndLineNumber,
|
||||
});
|
||||
}
|
||||
}
|
||||
return selectedRequests;
|
||||
}
|
||||
|
||||
// Set the cursor to the first line of the editor
|
||||
public selectFirstLine() {
|
||||
this.editor.setSelection(new monaco.Selection(0, 0, 0, 0));
|
||||
}
|
||||
|
||||
public async getParsedOutput(): Promise<string> {
|
||||
const model = this.editor.getModel();
|
||||
|
||||
if (!model) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let selectedRequestsString = '';
|
||||
const selectedRequests = await this.getSelectedParsedOutput();
|
||||
|
||||
for (const request of selectedRequests) {
|
||||
const dataString = model
|
||||
.getValueInRange({
|
||||
startLineNumber: request.startLineNumber,
|
||||
startColumn: 1,
|
||||
endLineNumber: request.endLineNumber,
|
||||
endColumn: model.getLineMaxColumn(request.endLineNumber),
|
||||
})
|
||||
.trim();
|
||||
|
||||
selectedRequestsString += dataString + '\n';
|
||||
}
|
||||
|
||||
return selectedRequestsString;
|
||||
}
|
||||
}
|
|
@ -14,14 +14,14 @@ import { monaco } from '@kbn/monaco';
|
|||
|
||||
const mockPopulateContext = jest.fn();
|
||||
|
||||
jest.mock('../../../../../lib/autocomplete/engine', () => {
|
||||
jest.mock('../../../../lib/autocomplete/engine', () => {
|
||||
return {
|
||||
populateContext: (...args: any) => {
|
||||
mockPopulateContext(args);
|
||||
},
|
||||
};
|
||||
});
|
||||
import { AutoCompleteContext } from '../../../../../lib/autocomplete/types';
|
||||
import { AutoCompleteContext } from '../../../../lib/autocomplete/types';
|
||||
import {
|
||||
getDocumentationLinkFromAutocomplete,
|
||||
getUrlPathCompletionItems,
|
|
@ -14,13 +14,13 @@ import {
|
|||
getGlobalAutocompleteComponents,
|
||||
getTopLevelUrlCompleteComponents,
|
||||
getUnmatchedEndpointComponents,
|
||||
} from '../../../../../lib/kb';
|
||||
} from '../../../../lib/kb';
|
||||
import {
|
||||
AutoCompleteContext,
|
||||
type DataAutoCompleteRulesOneOf,
|
||||
ResultTerm,
|
||||
} from '../../../../../lib/autocomplete/types';
|
||||
import { populateContext } from '../../../../../lib/autocomplete/engine';
|
||||
} from '../../../../lib/autocomplete/types';
|
||||
import { populateContext } from '../../../../lib/autocomplete/engine';
|
||||
import type { EditorRequest } from '../types';
|
||||
import { parseBody, parseLine, parseUrl } from './tokens_utils';
|
||||
import {
|
|
@ -35,3 +35,10 @@ export {
|
|||
} from './autocomplete_utils';
|
||||
export { getLineTokens, containsUrlParams } from './tokens_utils';
|
||||
export { getStatusCodeDecorations } from './status_code_decoration_utils';
|
||||
export {
|
||||
isMapboxVectorTile,
|
||||
languageForContentType,
|
||||
safeExpandLiteralStrings,
|
||||
isJSONContentType,
|
||||
} from './output_data';
|
||||
export { convertMapboxVectorTileToJson } from './mapbox_vector_tile';
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { monaco, ParsedRequest } from '@kbn/monaco';
|
||||
import type { MetricsTracker } from '../../../../../types';
|
||||
import type { MetricsTracker } from '../../../../types';
|
||||
import {
|
||||
getAutoIndentedRequests,
|
||||
getCurlRequest,
|
|
@ -9,9 +9,9 @@
|
|||
|
||||
import { monaco, ParsedRequest } from '@kbn/monaco';
|
||||
import { parse } from 'hjson';
|
||||
import { constructUrl } from '../../../../../lib/es';
|
||||
import type { MetricsTracker } from '../../../../../types';
|
||||
import type { DevToolsVariable } from '../../../../components';
|
||||
import { constructUrl } from '../../../../lib/es';
|
||||
import type { MetricsTracker } from '../../../../types';
|
||||
import type { DevToolsVariable } from '../../../components';
|
||||
import type { EditorRequest, AdjustedParsedRequest } from '../types';
|
||||
import {
|
||||
urlVariableTemplateRegex,
|
|
@ -13,7 +13,7 @@ import {
|
|||
WARNING_STATUS_BADGE_CLASSNAME,
|
||||
DANGER_STATUS_BADGE_CLASSNAME,
|
||||
} from './constants';
|
||||
import { RequestResult } from '../../../../hooks/use_send_current_request/send_request';
|
||||
import { RequestResult } from '../../../hooks/use_send_current_request/send_request';
|
||||
|
||||
describe('getStatusCodeDecorations', () => {
|
||||
it('correctly returns all decorations on full data', () => {
|
|
@ -8,7 +8,7 @@
|
|||
*/
|
||||
|
||||
import { monaco } from '@kbn/monaco';
|
||||
import { RequestResult } from '../../../../hooks/use_send_current_request/send_request';
|
||||
import { RequestResult } from '../../../hooks/use_send_current_request/send_request';
|
||||
import {
|
||||
DEFAULT_STATUS_BADGE_CLASSNAME,
|
||||
SUCCESS_STATUS_BADGE_CLASSNAME,
|
|
@ -118,7 +118,7 @@ interface ConsoleWrapperProps
|
|||
|
||||
export const ConsoleWrapper = (props: ConsoleWrapperProps) => {
|
||||
const [dependencies, setDependencies] = useState<ConsoleDependencies | null>(null);
|
||||
const { core, usageCollection, onKeyDown, isMonacoEnabled, isDevMode, isOpen } = props;
|
||||
const { core, usageCollection, onKeyDown, isDevMode, isOpen } = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (dependencies === null && isOpen) {
|
||||
|
@ -169,7 +169,6 @@ export const ConsoleWrapper = (props: ConsoleWrapperProps) => {
|
|||
autocompleteInfo,
|
||||
},
|
||||
config: {
|
||||
isMonacoEnabled,
|
||||
isDevMode,
|
||||
},
|
||||
}}
|
||||
|
@ -179,7 +178,7 @@ export const ConsoleWrapper = (props: ConsoleWrapperProps) => {
|
|||
{isOpen ? (
|
||||
<div className="embeddableConsole__content" data-test-subj="consoleEmbeddedBody">
|
||||
<EuiWindowEvent event="keydown" handler={onKeyDown} />
|
||||
<Main hideWelcome />
|
||||
<Main isEmbeddable={true} />
|
||||
</div>
|
||||
) : (
|
||||
<span />
|
||||
|
|
|
@ -70,7 +70,6 @@ export const EmbeddableConsole = ({
|
|||
usageCollection,
|
||||
setDispatch,
|
||||
alternateView,
|
||||
isMonacoEnabled,
|
||||
isDevMode,
|
||||
getConsoleHeight,
|
||||
setConsoleHeight,
|
||||
|
@ -199,7 +198,6 @@ export const EmbeddableConsole = ({
|
|||
core={core}
|
||||
usageCollection={usageCollection}
|
||||
onKeyDown={onKeyDown}
|
||||
isMonacoEnabled={isMonacoEnabled}
|
||||
isDevMode={isDevMode}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
@ -0,0 +1,306 @@
|
|||
/*
|
||||
* 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, useState } from 'react';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
EuiText,
|
||||
EuiPanel,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButton,
|
||||
useEuiTheme,
|
||||
EuiAutoSizer,
|
||||
EuiSplitPanel,
|
||||
EuiButtonEmpty,
|
||||
EuiFormFieldset,
|
||||
EuiCheckableCard,
|
||||
EuiResizableContainer,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
import { SHELL_TAB_ID } from '../main';
|
||||
import { HistoryEmptyPrompt } from './history_empty';
|
||||
import { useServicesContext } from '../../contexts';
|
||||
import { useEditorActionContext } from '../../contexts/editor_context';
|
||||
import { HistoryViewer } from './history_viewer_monaco';
|
||||
import { useEditorReadContext } from '../../contexts/editor_context';
|
||||
import { getFormattedRequest } from '../../lib';
|
||||
import { ESRequest, RestoreMethod } from '../../../types';
|
||||
|
||||
const CHILD_ELEMENT_PREFIX = 'historyReq';
|
||||
|
||||
interface HistoryProps {
|
||||
data: string;
|
||||
endpoint: string;
|
||||
method: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
interface RowProps {
|
||||
index: number;
|
||||
style: React.CSSProperties;
|
||||
data: HistoryProps[];
|
||||
}
|
||||
|
||||
const CheckeableCardLabel = ({ historyItem }: { historyItem: HistoryProps }) => {
|
||||
const date = moment(historyItem.time);
|
||||
|
||||
let formattedDate = date.format('MMM D');
|
||||
if (date.diff(moment(), 'days') > -7) {
|
||||
formattedDate = date.fromNow();
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<b>{historyItem.endpoint}</b>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s" color="subdued">
|
||||
{formattedDate}
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
isVerticalLayout: boolean;
|
||||
}
|
||||
|
||||
export function History({ isVerticalLayout }: Props) {
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const {
|
||||
services: { history, routeHistory },
|
||||
} = useServicesContext();
|
||||
const dispatch = useEditorActionContext();
|
||||
|
||||
const { settings: readOnlySettings } = useEditorReadContext();
|
||||
|
||||
const [requests, setPastRequests] = useState<HistoryProps[]>(history.getHistory());
|
||||
|
||||
const clearHistory = useCallback(() => {
|
||||
history.clearHistory();
|
||||
setPastRequests(history.getHistory());
|
||||
}, [history]);
|
||||
|
||||
const [viewingReq, setViewingReq] = useState<any>(null);
|
||||
|
||||
const initialize = useCallback(() => {
|
||||
const nextSelectedIndex = 0;
|
||||
setViewingReq(requests[nextSelectedIndex]);
|
||||
}, [requests]);
|
||||
|
||||
const clear = () => {
|
||||
clearHistory();
|
||||
initialize();
|
||||
};
|
||||
|
||||
const restoreRequestFromHistory = useCallback(
|
||||
(restoreMethod: RestoreMethod) => {
|
||||
routeHistory?.push(`/console/${SHELL_TAB_ID}`);
|
||||
|
||||
const formattedRequest = getFormattedRequest(viewingReq as ESRequest);
|
||||
|
||||
dispatch({
|
||||
type: 'setRequestToRestore',
|
||||
payload: {
|
||||
request: formattedRequest,
|
||||
restoreMethod,
|
||||
},
|
||||
});
|
||||
},
|
||||
[viewingReq, dispatch, routeHistory]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
initialize();
|
||||
}, [initialize]);
|
||||
|
||||
const Row = useCallback(
|
||||
({ data, index, style }: RowProps) => (
|
||||
<EuiFormFieldset key={index} data-test-subj="historyItemFieldset" style={style}>
|
||||
<EuiCheckableCard
|
||||
id={`${CHILD_ELEMENT_PREFIX}${index}`}
|
||||
label={<CheckeableCardLabel historyItem={data[index]} />}
|
||||
data-test-subj={`historyItem-${index}`}
|
||||
checkableType="radio"
|
||||
checked={viewingReq === data[index]}
|
||||
onChange={() => {
|
||||
setViewingReq(data[index]);
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</EuiFormFieldset>
|
||||
),
|
||||
[viewingReq, setViewingReq]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel
|
||||
color="subdued"
|
||||
paddingSize="none"
|
||||
hasShadow={false}
|
||||
borderRadius="none"
|
||||
css={{ height: '100%' }}
|
||||
data-test-subj="consoleHistoryPanel"
|
||||
>
|
||||
<EuiResizableContainer
|
||||
style={{ height: '100%' }}
|
||||
direction={isVerticalLayout ? 'vertical' : 'horizontal'}
|
||||
>
|
||||
{(EuiResizablePanel, EuiResizableButton) => (
|
||||
<>
|
||||
<EuiResizablePanel
|
||||
initialSize={50}
|
||||
minSize="30%"
|
||||
tabIndex={0}
|
||||
borderRadius="none"
|
||||
hasShadow={false}
|
||||
paddingSize="none"
|
||||
>
|
||||
<EuiSplitPanel.Outer
|
||||
grow
|
||||
color="subdued"
|
||||
css={{
|
||||
height: '100%',
|
||||
paddingRight: euiTheme.size.s,
|
||||
}}
|
||||
>
|
||||
<EuiSplitPanel.Inner paddingSize="m">
|
||||
<EuiFlexGroup direction="column" gutterSize="none" css={{ height: '100%' }}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="console.historyPage.pageTitle"
|
||||
defaultMessage="History"
|
||||
/>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiText color="subdued">
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="console.historyPage.pageDescription"
|
||||
defaultMessage="Revisit and reuse your past queries"
|
||||
/>
|
||||
</p>
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={true} css={{ height: '100%' }}>
|
||||
{requests.length === 0 && <HistoryEmptyPrompt />}
|
||||
|
||||
{requests.length > 0 && (
|
||||
<EuiAutoSizer>
|
||||
{({ height, width }) => (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
itemCount={requests.length}
|
||||
itemSize={62}
|
||||
itemData={requests}
|
||||
width={width}
|
||||
>
|
||||
{Row}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</EuiAutoSizer>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner
|
||||
grow={false}
|
||||
color="subdued"
|
||||
paddingSize="s"
|
||||
css={{ paddingTop: euiTheme.size.l }}
|
||||
>
|
||||
<EuiText>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
color="primary"
|
||||
data-test-subj="consoleClearHistoryButton"
|
||||
onClick={clear}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="console.historyPage.clearHistoryButtonLabel"
|
||||
defaultMessage="Clear all history"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiText>
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
</EuiResizablePanel>
|
||||
|
||||
<EuiResizableButton className="conApp__resizerButton" />
|
||||
|
||||
<EuiResizablePanel initialSize={50} minSize="15%" tabIndex={0} paddingSize="none">
|
||||
<EuiSplitPanel.Outer
|
||||
color="subdued"
|
||||
css={{ height: '100%' }}
|
||||
borderRadius="none"
|
||||
hasShadow={false}
|
||||
>
|
||||
<EuiSplitPanel.Inner
|
||||
paddingSize="none"
|
||||
css={{ top: 0 }}
|
||||
className="consoleEditorPanel"
|
||||
>
|
||||
<HistoryViewer settings={readOnlySettings} req={viewingReq} />
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiSplitPanel.Inner grow={false} className="consoleEditorPanel">
|
||||
<EuiFlexGroup justifyContent="flexEnd" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="consoleHistoryAddAndRunButton"
|
||||
color="primary"
|
||||
iconType="play"
|
||||
disabled={!viewingReq}
|
||||
onClick={() => restoreRequestFromHistory(RestoreMethod.RESTORE_AND_EXECUTE)}
|
||||
>
|
||||
{i18n.translate('console.historyPage.addAndRunButtonLabel', {
|
||||
defaultMessage: 'Add and run',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="consoleHistoryApplyButton"
|
||||
fill
|
||||
color="primary"
|
||||
iconType="plusInCircle"
|
||||
disabled={!viewingReq}
|
||||
onClick={() => restoreRequestFromHistory(RestoreMethod.RESTORE)}
|
||||
>
|
||||
{i18n.translate('console.historyPage.applyHistoryButtonLabel', {
|
||||
defaultMessage: 'Add',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
</EuiResizablePanel>
|
||||
</>
|
||||
)}
|
||||
</EuiResizableContainer>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 { EuiTitle, EuiFlexGroup, EuiFlexItem, EuiLink, EuiEmptyPrompt } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { useServicesContext } from '../../contexts';
|
||||
|
||||
export function HistoryEmptyPrompt() {
|
||||
const { docLinks } = useServicesContext();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
<h2>
|
||||
{i18n.translate('console.historyPage.emptyPromptTitle', {
|
||||
defaultMessage: 'No queries yet',
|
||||
})}
|
||||
</h2>
|
||||
}
|
||||
titleSize="xs"
|
||||
body={
|
||||
<p>
|
||||
{i18n.translate('console.historyPage.emptyPromptBody', {
|
||||
defaultMessage:
|
||||
'This history panel will display any past queries you’ve run for review and reuse.',
|
||||
})}
|
||||
</p>
|
||||
}
|
||||
footer={
|
||||
<EuiTitle size="xxs">
|
||||
<div>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="console.historyPage.emptyPromptFooterLabel"
|
||||
defaultMessage="Want to learn more?"
|
||||
/>
|
||||
</h3>
|
||||
<EuiLink href={docLinks.console.guide} target="_blank">
|
||||
<FormattedMessage
|
||||
id="console.historyPage.emptyPromptFooterLink"
|
||||
defaultMessage="Read Console documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
</div>
|
||||
</EuiTitle>
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -14,7 +14,7 @@ import { CodeEditor } from '@kbn/code-editor';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { formatRequestBodyDoc } from '../../../lib/utils';
|
||||
import { DevToolsSettings } from '../../../services';
|
||||
import { useResizeCheckerUtils } from '../editor/monaco/hooks';
|
||||
import { useResizeCheckerUtils } from '../editor/hooks';
|
||||
|
||||
export const HistoryViewer = ({
|
||||
settings,
|
||||
|
@ -43,13 +43,14 @@ export const HistoryViewer = ({
|
|||
renderedHistoryRequest = req.method + ' ' + req.endpoint + '\n' + formattedData;
|
||||
} else {
|
||||
renderedHistoryRequest = i18n.translate('console.historyPage.monaco.noHistoryTextMessage', {
|
||||
defaultMessage: 'No history available',
|
||||
defaultMessage: '# No history available to display',
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div
|
||||
css={css`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`}
|
||||
ref={divRef}
|
||||
>
|
|
@ -7,4 +7,4 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export { ConsoleHistory } from './console_history';
|
||||
export { History } from './history';
|
|
@ -8,4 +8,3 @@
|
|||
*/
|
||||
|
||||
export { Main } from './main';
|
||||
export { Panel, PanelsContainer } from './split_panel';
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 const SHELL_TAB_ID = 'shell';
|
||||
export const HISTORY_TAB_ID = 'history';
|
||||
export const CONFIG_TAB_ID = 'config';
|
||||
|
||||
export const SHELL_TOUR_STEP = 1;
|
||||
export const EDITOR_TOUR_STEP = 2;
|
||||
export const HISTORY_TOUR_STEP = 3;
|
||||
export const CONFIG_TOUR_STEP = 4;
|
||||
export const FILES_TOUR_STEP = 5;
|
||||
|
||||
// Key used for storing tour state in local storage
|
||||
export const TOUR_STORAGE_KEY = 'consoleTour';
|
||||
|
||||
export const INITIAL_TOUR_CONFIG = {
|
||||
currentTourStep: 1,
|
||||
isTourActive: true,
|
||||
tourPopoverWidth: 360,
|
||||
tourSubtitle: 'Console onboarding', // Used for state in local storage
|
||||
};
|
||||
|
||||
export const EXPORT_FILE_NAME = 'console_export';
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiTourStepProps,
|
||||
EuiTourActions,
|
||||
EuiTourState,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { ConsoleTourStepProps } from '../../components';
|
||||
|
||||
export const getConsoleTourStepProps = (
|
||||
stateTourStepProps: EuiTourStepProps[],
|
||||
actions: EuiTourActions,
|
||||
tourState: EuiTourState
|
||||
): ConsoleTourStepProps[] => {
|
||||
return stateTourStepProps.map((step: EuiTourStepProps) => {
|
||||
const nextTourStep = () => {
|
||||
if (tourState.currentTourStep < 5) {
|
||||
actions.incrementStep();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
step: step.step,
|
||||
stepsTotal: step.stepsTotal,
|
||||
isStepOpen: step.step === tourState.currentTourStep && tourState.isTourActive,
|
||||
title: step.title,
|
||||
content: step.content,
|
||||
anchorPosition: step.anchorPosition,
|
||||
dataTestSubj: step['data-test-subj'],
|
||||
maxWidth: step.maxWidth,
|
||||
css: step.css,
|
||||
onFinish: () => actions.finishTour(false),
|
||||
footerAction:
|
||||
step.step === step.stepsTotal ? (
|
||||
<EuiButton
|
||||
color="success"
|
||||
size="s"
|
||||
onClick={() => actions.finishTour()}
|
||||
data-test-subj="consoleCompleteTourButton"
|
||||
>
|
||||
{i18n.translate('console.tour.completeTourButton', {
|
||||
defaultMessage: 'Complete',
|
||||
})}
|
||||
</EuiButton>
|
||||
) : (
|
||||
[
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
color="text"
|
||||
onClick={() => actions.finishTour()}
|
||||
data-test-subj="consoleSkipTourButton"
|
||||
>
|
||||
{i18n.translate('console.tour.skipTourButton', {
|
||||
defaultMessage: 'Skip tour',
|
||||
})}
|
||||
</EuiButtonEmpty>,
|
||||
<EuiButton
|
||||
color="success"
|
||||
size="s"
|
||||
onClick={nextTourStep}
|
||||
data-test-subj="consoleNextTourStepButton"
|
||||
>
|
||||
{i18n.translate('console.tour.nextStepButton', {
|
||||
defaultMessage: 'Next',
|
||||
})}
|
||||
</EuiButton>,
|
||||
]
|
||||
),
|
||||
} as ConsoleTourStepProps;
|
||||
});
|
||||
};
|
|
@ -8,23 +8,39 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
SHELL_TAB_ID,
|
||||
HISTORY_TAB_ID,
|
||||
CONFIG_TAB_ID,
|
||||
SHELL_TOUR_STEP,
|
||||
CONFIG_TOUR_STEP,
|
||||
HISTORY_TOUR_STEP,
|
||||
} from './constants';
|
||||
|
||||
interface Props {
|
||||
onClickHistory: () => void;
|
||||
onClickSettings: () => void;
|
||||
onClickHelp: () => void;
|
||||
onClickVariables: () => void;
|
||||
selectedTab: string;
|
||||
setSelectedTab: (id: string) => void;
|
||||
}
|
||||
|
||||
export function getTopNavConfig({
|
||||
onClickHistory,
|
||||
onClickSettings,
|
||||
onClickHelp,
|
||||
onClickVariables,
|
||||
}: Props) {
|
||||
export function getTopNavConfig({ selectedTab, setSelectedTab }: Props) {
|
||||
return [
|
||||
{
|
||||
id: 'history',
|
||||
id: SHELL_TAB_ID,
|
||||
label: i18n.translate('console.topNav.shellTabLabel', {
|
||||
defaultMessage: 'Shell',
|
||||
}),
|
||||
description: i18n.translate('console.topNav.shellTabDescription', {
|
||||
defaultMessage: 'Shell',
|
||||
}),
|
||||
onClick: () => {
|
||||
setSelectedTab(SHELL_TAB_ID);
|
||||
},
|
||||
testId: 'consoleShellButton',
|
||||
isSelected: selectedTab === SHELL_TAB_ID,
|
||||
tourStep: SHELL_TOUR_STEP,
|
||||
},
|
||||
{
|
||||
id: HISTORY_TAB_ID,
|
||||
label: i18n.translate('console.topNav.historyTabLabel', {
|
||||
defaultMessage: 'History',
|
||||
}),
|
||||
|
@ -32,48 +48,26 @@ export function getTopNavConfig({
|
|||
defaultMessage: 'History',
|
||||
}),
|
||||
onClick: () => {
|
||||
onClickHistory();
|
||||
setSelectedTab(HISTORY_TAB_ID);
|
||||
},
|
||||
testId: 'consoleHistoryButton',
|
||||
isSelected: selectedTab === HISTORY_TAB_ID,
|
||||
tourStep: HISTORY_TOUR_STEP,
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: i18n.translate('console.topNav.settingsTabLabel', {
|
||||
defaultMessage: 'Settings',
|
||||
id: CONFIG_TAB_ID,
|
||||
label: i18n.translate('console.topNav.configTabLabel', {
|
||||
defaultMessage: 'Config',
|
||||
}),
|
||||
description: i18n.translate('console.topNav.settingsTabDescription', {
|
||||
defaultMessage: 'Settings',
|
||||
description: i18n.translate('console.topNav.configTabDescription', {
|
||||
defaultMessage: 'Config',
|
||||
}),
|
||||
onClick: () => {
|
||||
onClickSettings();
|
||||
setSelectedTab(CONFIG_TAB_ID);
|
||||
},
|
||||
testId: 'consoleSettingsButton',
|
||||
},
|
||||
{
|
||||
id: 'variables',
|
||||
label: i18n.translate('console.topNav.variablesTabLabel', {
|
||||
defaultMessage: 'Variables',
|
||||
}),
|
||||
description: i18n.translate('console.topNav.variablesTabDescription', {
|
||||
defaultMessage: 'Variables',
|
||||
}),
|
||||
onClick: () => {
|
||||
onClickVariables();
|
||||
},
|
||||
testId: 'consoleVariablesButton',
|
||||
},
|
||||
{
|
||||
id: 'help',
|
||||
label: i18n.translate('console.topNav.helpTabLabel', {
|
||||
defaultMessage: 'Help',
|
||||
}),
|
||||
description: i18n.translate('console.topNav.helpTabDescription', {
|
||||
defaultMessage: 'Help',
|
||||
}),
|
||||
onClick: () => {
|
||||
onClickHelp();
|
||||
},
|
||||
testId: 'consoleHelpButton',
|
||||
testId: 'consoleConfigButton',
|
||||
isSelected: selectedTab === CONFIG_TAB_ID,
|
||||
tourStep: CONFIG_TOUR_STEP,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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 { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiStatelessTourSteps, EuiLink, EuiText } from '@elastic/eui';
|
||||
import { DocLinksStart } from '@kbn/core-doc-links-browser';
|
||||
|
||||
const commonProps = {
|
||||
stepsTotal: 5,
|
||||
maxWidth: 400,
|
||||
};
|
||||
|
||||
export function getTourSteps(docLinks: DocLinksStart['links']) {
|
||||
return [
|
||||
{
|
||||
step: 1,
|
||||
title: i18n.translate('console.tour.shellStepTitle', {
|
||||
defaultMessage: 'Welcome to the Console',
|
||||
}),
|
||||
content: (
|
||||
<EuiText>
|
||||
{i18n.translate('console.tour.shellStepContent', {
|
||||
defaultMessage:
|
||||
'Console is an interactive UI for calling Elasticsearch and Kibana APIs and viewing their responses. Use Query DSL syntax to search your data, manage settings, and more.',
|
||||
})}
|
||||
</EuiText>
|
||||
),
|
||||
anchorPosition: 'downLeft',
|
||||
'data-test-subj': 'shellTourStep',
|
||||
...commonProps,
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: i18n.translate('console.tour.editorStepTitle', {
|
||||
defaultMessage: 'Get started querying',
|
||||
}),
|
||||
content: (
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="console.tour.editorStepContent"
|
||||
defaultMessage="Enter a request in this input pane, and view the response in the adjacent output pane. For more details, visit {queryDslDocs}."
|
||||
values={{
|
||||
queryDslDocs: (
|
||||
<EuiLink href={docLinks.query.queryDsl} target="_blank">
|
||||
QueryDSL documentation
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
),
|
||||
anchorPosition: 'rightUp',
|
||||
'data-test-subj': 'editorTourStep',
|
||||
css: {
|
||||
position: 'absolute',
|
||||
top: '40%',
|
||||
left: '15%',
|
||||
transform: 'translateY(10px)',
|
||||
},
|
||||
...commonProps,
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: i18n.translate('console.tour.historyStepTitle', {
|
||||
defaultMessage: 'Revisit past queries',
|
||||
}),
|
||||
content: (
|
||||
<EuiText>
|
||||
{i18n.translate('console.tour.historyStepContent', {
|
||||
defaultMessage:
|
||||
'The history panel keeps track of your past queries, making it easy to review and rerun them.',
|
||||
})}
|
||||
</EuiText>
|
||||
),
|
||||
anchorPosition: 'rightUp',
|
||||
'data-test-subj': 'historyTourStep',
|
||||
...commonProps,
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: i18n.translate('console.tour.configStepTitle', {
|
||||
defaultMessage: 'Customize your toolbox',
|
||||
}),
|
||||
content: (
|
||||
<EuiText>
|
||||
{i18n.translate('console.tour.configStepContent', {
|
||||
defaultMessage:
|
||||
'Fine-tune your Console’s settings and create variables to personalize your workflow.',
|
||||
})}
|
||||
</EuiText>
|
||||
),
|
||||
anchorPosition: 'rightUp',
|
||||
'data-test-subj': 'configTourStep',
|
||||
...commonProps,
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
title: i18n.translate('console.tour.filesStepTitle', {
|
||||
defaultMessage: 'Manage Console files',
|
||||
}),
|
||||
content: (
|
||||
<EuiText>
|
||||
{i18n.translate('console.tour.filesStepContent', {
|
||||
defaultMessage:
|
||||
'Easily export your Console requests to a file, or import ones you’ve saved previously.',
|
||||
})}
|
||||
</EuiText>
|
||||
),
|
||||
anchorPosition: 'downRight',
|
||||
'data-test-subj': 'filesTourStep',
|
||||
...commonProps,
|
||||
},
|
||||
] as EuiStatelessTourSteps;
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const MAIN_PANEL_LABELS = {
|
||||
consolePageHeading: i18n.translate('console.pageHeading', {
|
||||
defaultMessage: 'Console',
|
||||
}),
|
||||
importButton: i18n.translate('console.importButtonLabel', {
|
||||
defaultMessage: 'Import requests',
|
||||
}),
|
||||
importButtonTooltip: i18n.translate('console.importButtonTooltipLabel', {
|
||||
defaultMessage: 'Import requests from a file into the editor',
|
||||
}),
|
||||
exportButton: i18n.translate('console.exportButton', {
|
||||
defaultMessage: 'Export requests',
|
||||
}),
|
||||
exportButtonTooltip: i18n.translate('console.exportButtonTooltipLabel', {
|
||||
defaultMessage: 'Export all console requests to a TXT file',
|
||||
}),
|
||||
helpButton: i18n.translate('console.helpButtonTooltipContent', {
|
||||
defaultMessage: 'Help',
|
||||
}),
|
||||
shortcutsButton: i18n.translate('console.shortcutsButtonAriaLabel', {
|
||||
defaultMessage: 'Keyboard shortcuts',
|
||||
}),
|
||||
variablesButton: i18n.translate('console.variablesButton', {
|
||||
defaultMessage: 'Variables',
|
||||
}),
|
||||
openFullscrenButton: i18n.translate('console.openFullscreenButton', {
|
||||
defaultMessage: 'Open this console as a full page experience',
|
||||
}),
|
||||
closeFullscrenButton: i18n.translate('console.closeFullscreenButton', {
|
||||
defaultMessage: 'Close full page experience',
|
||||
}),
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiConfirmModal } from '@elastic/eui';
|
||||
import { useEditorActionContext } from '../../contexts';
|
||||
import { useServicesContext } from '../../contexts';
|
||||
|
||||
interface ImportConfirmModalProps {
|
||||
onClose: () => void;
|
||||
fileContent: string;
|
||||
}
|
||||
|
||||
export const ImportConfirmModal = ({ onClose, fileContent }: ImportConfirmModalProps) => {
|
||||
const dispatch = useEditorActionContext();
|
||||
const {
|
||||
services: { notifications },
|
||||
} = useServicesContext();
|
||||
|
||||
const onConfirmImport = useCallback(() => {
|
||||
// Import the file content
|
||||
dispatch({
|
||||
type: 'setFileToImport',
|
||||
payload: fileContent as string,
|
||||
});
|
||||
|
||||
notifications.toasts.addSuccess(
|
||||
i18n.translate('console.notification.fileImportedSuccessfully', {
|
||||
defaultMessage: `The file you selected has been imported successfully.`,
|
||||
})
|
||||
);
|
||||
|
||||
onClose();
|
||||
}, [fileContent, onClose, dispatch, notifications.toasts]);
|
||||
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
data-test-subj="importConfirmModal"
|
||||
title={i18n.translate('console.importConfirmModal.title', {
|
||||
defaultMessage: 'Import and replace requests?',
|
||||
})}
|
||||
onCancel={onClose}
|
||||
onConfirm={onConfirmImport}
|
||||
cancelButtonText={i18n.translate('console.importConfirmModal.cancelButton', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
confirmButtonText={i18n.translate('console.importConfirmModal.confirmButton', {
|
||||
defaultMessage: 'Import and replace',
|
||||
})}
|
||||
buttonColor="primary"
|
||||
defaultFocusedButton="confirm"
|
||||
>
|
||||
<p>
|
||||
{i18n.translate('console.importConfirmModal.body', {
|
||||
defaultMessage: `Importing this file will replace all current requests in the editor.`,
|
||||
})}
|
||||
</p>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
};
|
|
@ -8,3 +8,4 @@
|
|||
*/
|
||||
|
||||
export { Main } from './main';
|
||||
export * from './constants';
|
||||
|
|
|
@ -7,61 +7,177 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPageTemplate,
|
||||
EuiSplitPanel,
|
||||
EuiToolTip,
|
||||
useEuiTour,
|
||||
EuiButtonEmpty,
|
||||
EuiHorizontalRule,
|
||||
EuiScreenReaderOnly,
|
||||
useIsWithinBreakpoints,
|
||||
useEuiOverflowScroll,
|
||||
useEuiTheme,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiPageTemplate } from '@elastic/eui';
|
||||
import { ConsoleHistory } from '../console_history';
|
||||
import { downloadFileAs } from '@kbn/share-plugin/public';
|
||||
import { getConsoleTourStepProps } from './get_console_tour_step_props';
|
||||
import { useServicesContext } from '../../contexts';
|
||||
import { MAIN_PANEL_LABELS } from './i18n';
|
||||
import { NavIconButton } from './nav_icon_button';
|
||||
import { Editor } from '../editor';
|
||||
import { Settings } from '../settings';
|
||||
import { Variables } from '../variables';
|
||||
|
||||
import { Config } from '../config';
|
||||
import {
|
||||
useEditorReadContext,
|
||||
useEditorActionContext,
|
||||
useRequestActionContext,
|
||||
} from '../../contexts';
|
||||
import {
|
||||
TopNavMenu,
|
||||
WelcomePanel,
|
||||
HelpPanel,
|
||||
SomethingWentWrongCallout,
|
||||
NetworkRequestStatusBar,
|
||||
HelpPopover,
|
||||
ShortcutsPopover,
|
||||
ConsoleTourStep,
|
||||
ConsoleTourStepProps,
|
||||
} from '../../components';
|
||||
|
||||
import { useServicesContext, useEditorReadContext, useRequestReadContext } from '../../contexts';
|
||||
import { History } from '../history';
|
||||
import { useDataInit } from '../../hooks';
|
||||
|
||||
import { getTopNavConfig } from './get_top_nav';
|
||||
import type { SenseEditor } from '../../models/sense_editor';
|
||||
import { getResponseWithMostSevereStatusCode } from '../../../lib/utils';
|
||||
import { getTourSteps } from './get_tour_steps';
|
||||
import { ImportConfirmModal } from './import_confirm_modal';
|
||||
import {
|
||||
SHELL_TAB_ID,
|
||||
HISTORY_TAB_ID,
|
||||
CONFIG_TAB_ID,
|
||||
EDITOR_TOUR_STEP,
|
||||
TOUR_STORAGE_KEY,
|
||||
INITIAL_TOUR_CONFIG,
|
||||
FILES_TOUR_STEP,
|
||||
EXPORT_FILE_NAME,
|
||||
} from './constants';
|
||||
|
||||
export interface MainProps {
|
||||
hideWelcome?: boolean;
|
||||
interface MainProps {
|
||||
currentTabProp?: string;
|
||||
isEmbeddable?: boolean;
|
||||
}
|
||||
|
||||
export function Main({ hideWelcome = false }: MainProps) {
|
||||
// 2MB limit (2 * 1024 * 1024 bytes)
|
||||
const MAX_FILE_UPLOAD_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
export function Main({ currentTabProp, isEmbeddable = false }: MainProps) {
|
||||
const dispatch = useEditorActionContext();
|
||||
const requestDispatch = useRequestActionContext();
|
||||
const { currentView } = useEditorReadContext();
|
||||
const currentTab = currentTabProp ?? currentView;
|
||||
const [isShortcutsOpen, setIsShortcutsOpen] = useState(false);
|
||||
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||
const [isFullscreenOpen, setIsFullScreen] = useState(false);
|
||||
const [isConfirmImportOpen, setIsConfirmImportOpen] = useState<string | null>(null);
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const {
|
||||
services: { storage },
|
||||
docLinks,
|
||||
services: { notifications, routeHistory },
|
||||
} = useServicesContext();
|
||||
|
||||
const { ready: editorsReady } = useEditorReadContext();
|
||||
const isVerticalLayout = useIsWithinBreakpoints(['xs', 's', 'm']);
|
||||
|
||||
const {
|
||||
requestInFlight: requestInProgress,
|
||||
lastResult: { data: requestData, error: requestError },
|
||||
} = useRequestReadContext();
|
||||
const storageTourState = localStorage.getItem(TOUR_STORAGE_KEY);
|
||||
const initialTourState = storageTourState ? JSON.parse(storageTourState) : INITIAL_TOUR_CONFIG;
|
||||
const [tourStepProps, actions, tourState] = useEuiTour(getTourSteps(docLinks), initialTourState);
|
||||
|
||||
const [showWelcome, setShowWelcomePanel] = useState(
|
||||
() => storage.get('version_welcome_shown') !== '@@SENSE_REVISION' && !hideWelcome
|
||||
useEffect(() => {
|
||||
localStorage.setItem(TOUR_STORAGE_KEY, JSON.stringify(tourState));
|
||||
}, [tourState]);
|
||||
|
||||
// Clean up request output when switching tabs
|
||||
useEffect(() => {
|
||||
requestDispatch({ type: 'cleanRequest', payload: undefined });
|
||||
}, [currentView, requestDispatch]);
|
||||
|
||||
const consoleTourStepProps: ConsoleTourStepProps[] = getConsoleTourStepProps(
|
||||
tourStepProps,
|
||||
actions,
|
||||
tourState
|
||||
);
|
||||
|
||||
const [showingHistory, setShowHistory] = useState(false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [showHelp, setShowHelp] = useState(false);
|
||||
const [showVariables, setShowVariables] = useState(false);
|
||||
|
||||
const [editorInstance, setEditorInstance] = useState<SenseEditor | null>(null);
|
||||
|
||||
const renderConsoleHistory = () => {
|
||||
return editorsReady ? <ConsoleHistory close={() => setShowHistory(false)} /> : null;
|
||||
};
|
||||
const { done, error, retry } = useDataInit();
|
||||
|
||||
const { currentTextObject } = useEditorReadContext();
|
||||
const [inputEditorValue, setInputEditorValue] = useState<string>(currentTextObject?.text ?? '');
|
||||
|
||||
const updateTab = (tab: string) => {
|
||||
if (routeHistory) {
|
||||
routeHistory?.push(`/console/${tab}`);
|
||||
} else {
|
||||
dispatch({ type: 'setCurrentView', payload: tab });
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
const isEnabled = !isFullscreenOpen;
|
||||
|
||||
setIsFullScreen(isEnabled);
|
||||
|
||||
if (isEnabled) {
|
||||
document.querySelector('#consoleRoot')?.requestFullscreen();
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
const onFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
const file = files && files[0];
|
||||
// Clear the input value so that a file can be imported again
|
||||
event.target.value = '';
|
||||
|
||||
if (file) {
|
||||
if (file.size > MAX_FILE_UPLOAD_SIZE) {
|
||||
notifications.toasts.addWarning(
|
||||
i18n.translate('console.notification.error.fileTooBigMessage', {
|
||||
defaultMessage: `File size exceeds the 2MB limit.`,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onerror = () => {
|
||||
notifications.toasts.addWarning(
|
||||
i18n.translate('console.notification.error.failedToReadFile', {
|
||||
defaultMessage: `Failed to read the file you selected.`,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
reader.onload = (e) => {
|
||||
const fileContent = e?.target?.result;
|
||||
|
||||
if (fileContent) {
|
||||
setIsConfirmImportOpen(fileContent as string);
|
||||
} else {
|
||||
notifications.toasts.addWarning(
|
||||
i18n.translate('console.notification.error.fileImportNoContent', {
|
||||
defaultMessage: `The file you selected doesn't appear to have any content. Please select a different file.`,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
const scrollablePanelStyle = css`
|
||||
${useEuiOverflowScroll('y', false)}
|
||||
`;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<EuiPageTemplate.EmptyPrompt color="danger">
|
||||
|
@ -70,76 +186,171 @@ export function Main({ hideWelcome = false }: MainProps) {
|
|||
);
|
||||
}
|
||||
|
||||
const data = getResponseWithMostSevereStatusCode(requestData) ?? requestError;
|
||||
const shortcutsButton = (
|
||||
<NavIconButton
|
||||
iconType="keyboard"
|
||||
onClick={() => setIsShortcutsOpen(!isShortcutsOpen)}
|
||||
ariaLabel={MAIN_PANEL_LABELS.shortcutsButton}
|
||||
dataTestSubj="consoleShortcutsButton"
|
||||
toolTipContent={MAIN_PANEL_LABELS.shortcutsButton}
|
||||
/>
|
||||
);
|
||||
|
||||
const helpButton = (
|
||||
<NavIconButton
|
||||
iconType="questionInCircle"
|
||||
onClick={() => setIsHelpOpen(!isHelpOpen)}
|
||||
ariaLabel={MAIN_PANEL_LABELS.helpButton}
|
||||
dataTestSubj="consoleHelpButton"
|
||||
toolTipContent={MAIN_PANEL_LABELS.helpButton}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div id="consoleRoot">
|
||||
<EuiFlexGroup
|
||||
className="consoleContainer"
|
||||
gutterSize="none"
|
||||
direction="column"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle className="euiScreenReaderOnly">
|
||||
<h1>
|
||||
{i18n.translate('console.pageHeading', {
|
||||
defaultMessage: 'Console',
|
||||
})}
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
<EuiFlexGroup gutterSize="none">
|
||||
<div id="consoleRoot" className={`consoleContainer${isEmbeddable ? '--embeddable' : ''}`}>
|
||||
<EuiScreenReaderOnly>
|
||||
<h1>{MAIN_PANEL_LABELS.consolePageHeading}</h1>
|
||||
</EuiScreenReaderOnly>
|
||||
<EuiSplitPanel.Outer grow={true} borderRadius={isEmbeddable ? 'none' : 'm'}>
|
||||
<EuiSplitPanel.Inner grow={false} className="consoleTabs">
|
||||
<EuiFlexGroup direction="row" alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem>
|
||||
<TopNavMenu
|
||||
disabled={!done}
|
||||
items={getTopNavConfig({
|
||||
onClickHistory: () => setShowHistory(!showingHistory),
|
||||
onClickSettings: () => setShowSettings(true),
|
||||
onClickHelp: () => setShowHelp(!showHelp),
|
||||
onClickVariables: () => setShowVariables(!showVariables),
|
||||
selectedTab: currentTab,
|
||||
setSelectedTab: (tab) => updateTab(tab),
|
||||
})}
|
||||
tourStepProps={consoleTourStepProps}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false} className="conApp__tabsExtension">
|
||||
<NetworkRequestStatusBar
|
||||
requestInProgress={requestInProgress}
|
||||
requestResult={
|
||||
data
|
||||
? {
|
||||
method: data.request.method.toUpperCase(),
|
||||
endpoint: data.request.path,
|
||||
statusCode: data.response.statusCode,
|
||||
statusText: data.response.statusText,
|
||||
timeElapsedMs: data.response.timeMs,
|
||||
<EuiFlexItem grow={false}>
|
||||
<ConsoleTourStep tourStepProps={consoleTourStepProps[FILES_TOUR_STEP - 1]}>
|
||||
<>
|
||||
<EuiToolTip content={MAIN_PANEL_LABELS.exportButtonTooltip}>
|
||||
<EuiButtonEmpty
|
||||
iconType="exportAction"
|
||||
onClick={() =>
|
||||
downloadFileAs(EXPORT_FILE_NAME, {
|
||||
content: inputEditorValue,
|
||||
type: 'text/plain',
|
||||
})
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
size="xs"
|
||||
data-test-subj="consoleExportButton"
|
||||
>
|
||||
{MAIN_PANEL_LABELS.exportButton}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
<>
|
||||
<EuiToolTip content={MAIN_PANEL_LABELS.importButtonTooltip}>
|
||||
<EuiButtonEmpty
|
||||
iconType="importAction"
|
||||
onClick={() => document.getElementById('importConsoleFile')?.click()}
|
||||
size="xs"
|
||||
data-test-subj="consoleImportButton"
|
||||
>
|
||||
{MAIN_PANEL_LABELS.importButton}
|
||||
</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
{/* This input is hidden by CSS in the UI, but the NavIcon button activates it */}
|
||||
<input
|
||||
type="file"
|
||||
accept="text/*"
|
||||
multiple={false}
|
||||
name="consoleSnippets"
|
||||
id="importConsoleFile"
|
||||
onChange={onFileChange}
|
||||
/>
|
||||
</>
|
||||
</>
|
||||
</ConsoleTourStep>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ShortcutsPopover
|
||||
button={shortcutsButton}
|
||||
isOpen={isShortcutsOpen}
|
||||
closePopover={() => setIsShortcutsOpen(false)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<HelpPopover
|
||||
button={helpButton}
|
||||
isOpen={isHelpOpen}
|
||||
closePopover={() => setIsHelpOpen(false)}
|
||||
resetTour={() => {
|
||||
setIsHelpOpen(false);
|
||||
updateTab(SHELL_TAB_ID);
|
||||
actions.resetTour();
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{isEmbeddable && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<NavIconButton
|
||||
iconType={isFullscreenOpen ? 'fullScreenExit' : 'fullScreen'}
|
||||
onClick={toggleFullscreen}
|
||||
ariaLabel={
|
||||
isFullscreenOpen
|
||||
? MAIN_PANEL_LABELS.closeFullscrenButton
|
||||
: MAIN_PANEL_LABELS.openFullscrenButton
|
||||
}
|
||||
dataTestSubj="consoleToggleFullscreenButton"
|
||||
toolTipContent={
|
||||
isFullscreenOpen
|
||||
? MAIN_PANEL_LABELS.closeFullscrenButton
|
||||
: MAIN_PANEL_LABELS.openFullscrenButton
|
||||
}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{showingHistory ? <EuiFlexItem grow={false}>{renderConsoleHistory()}</EuiFlexItem> : null}
|
||||
<EuiFlexItem>
|
||||
<Editor loading={!done} setEditorInstance={setEditorInstance} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
<EuiSplitPanel.Inner
|
||||
paddingSize="none"
|
||||
css={[scrollablePanelStyle, { backgroundColor: euiTheme.colors.body }]}
|
||||
>
|
||||
{currentTab === SHELL_TAB_ID && (
|
||||
<Editor
|
||||
loading={!done}
|
||||
isVerticalLayout={isVerticalLayout}
|
||||
inputEditorValue={inputEditorValue}
|
||||
setInputEditorValue={setInputEditorValue}
|
||||
/>
|
||||
)}
|
||||
{currentTab === HISTORY_TAB_ID && <History isVerticalLayout={isVerticalLayout} />}
|
||||
{currentTab === CONFIG_TAB_ID && <Config isVerticalLayout={isVerticalLayout} />}
|
||||
</EuiSplitPanel.Inner>
|
||||
<EuiHorizontalRule margin="none" className="consoleVariablesBottomBar" />
|
||||
<EuiSplitPanel.Inner
|
||||
paddingSize="xs"
|
||||
grow={false}
|
||||
className="consoleVariablesBottomBar"
|
||||
color="plain"
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => updateTab(CONFIG_TAB_ID)}
|
||||
iconType="editorCodeBlock"
|
||||
size="xs"
|
||||
color="text"
|
||||
>
|
||||
{MAIN_PANEL_LABELS.variablesButton}
|
||||
</EuiButtonEmpty>
|
||||
</EuiSplitPanel.Inner>
|
||||
</EuiSplitPanel.Outer>
|
||||
|
||||
{done && showWelcome ? (
|
||||
<WelcomePanel
|
||||
onDismiss={() => {
|
||||
storage.set('version_welcome_shown', '@@SENSE_REVISION');
|
||||
setShowWelcomePanel(false);
|
||||
}}
|
||||
{/* Empty container for Editor Tour Step */}
|
||||
<ConsoleTourStep tourStepProps={consoleTourStepProps[EDITOR_TOUR_STEP - 1]}>
|
||||
<div />
|
||||
</ConsoleTourStep>
|
||||
|
||||
{isConfirmImportOpen && (
|
||||
<ImportConfirmModal
|
||||
onClose={() => setIsConfirmImportOpen(null)}
|
||||
fileContent={isConfirmImportOpen}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{showSettings ? (
|
||||
<Settings onClose={() => setShowSettings(false)} editorInstance={editorInstance} />
|
||||
) : null}
|
||||
|
||||
{showVariables ? <Variables onClose={() => setShowVariables(false)} /> : null}
|
||||
|
||||
{showHelp ? <HelpPanel onClose={() => setShowHelp(false)} /> : null}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
|
||||
interface NavIconButtonProps {
|
||||
iconType: string;
|
||||
onClick: () => void;
|
||||
ariaLabel: string;
|
||||
dataTestSubj: string;
|
||||
toolTipContent: string;
|
||||
}
|
||||
|
||||
export const NavIconButton = ({
|
||||
iconType,
|
||||
onClick,
|
||||
ariaLabel,
|
||||
dataTestSubj,
|
||||
toolTipContent,
|
||||
}: NavIconButtonProps) => {
|
||||
return (
|
||||
<EuiToolTip position="top" content={toolTipContent}>
|
||||
<EuiButtonIcon
|
||||
iconType={iconType}
|
||||
onClick={onClick}
|
||||
aria-label={ariaLabel}
|
||||
data-test-subj={dataTestSubj}
|
||||
/>
|
||||
</EuiToolTip>
|
||||
);
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue