Console to NP ready (#43346)

*  General structure of Public w/ legacy brace + autocomplete 🤔
 Refactor Resizer functionality (panel component)
 Refactor Play Button
 Refactor Auto-completion
 Refactor Docs opener
 Refactor Storage

* First refactor of kbn ace keyboard mode to TS+React

* clean up unused props

* console_menu.js -> console_menu.tsx

* Remove unused file from quarantine and added fixed ui ace keyboard mode react hook

* - Refactored history and storage to app-wide services
- Pre-emptive changes to tests
- sense-history -> HistoryList
- removed unused kbn top nav v2 component

* A lot of cleanup, re-introduced editor resize checker, re-introduced history viewer as TS+React. `history` still needs refactoring.

* First iteration of tap nav menu, with history toggle working

* Lots of fixes
Also moved over and integrated remaining three react components

* Moved a lot of files around again, tidied up NP set up

* Replace angular directive

* Remove used code

* Re-order imports and move all ace dependencies to same location

* Remove more unused code

* Revise quarantined setup mocks

* Don't suggest 'undefined' or other null-like values in autocomplete

* Clean up api_server folder

* Re-add missing style

* Updated karma spec mock

* Fix editors cutting of at bottom of screen

* Refactor console editors into single components
Refactor a lot of business logic to main.tsx container
Minor renaming of variables for better readability

* Updated use of contexts with better error message
Fixed broken render sync cycles (using useCallback)
Fixed Main container render cycle (added missing deps to useEffect)
Fixed default input and removed auto indent from being called on init for already formatted text

* Updated test mocks

* Update to be more in line with NP conventions https://github.com/elastic/kibana/blob/master/src/core/CONVENTIONS.md

* Update console history when making new requests
Fixed spacing between editor and console history
Moved registration of keyboard commands to TS
Fixed setup_mocks.js after renaming app to application

* Clean up git merge conflict artifact

* Use updated NP interfaces

* More typings fixed after updating local project dependencies

* Removing some dependencies on KUI and font awesome from legacy editor

* Fix clear history not re-rendering
Refactor prop name to be more descriptive

* Simplify split_panel and add tests

* Fix accessibility tabbing behaviour for ace editor

* Refactor ConsoleEditor into two separate components
Remove unused changeCursor code
Remove unused textArea ref
Use default lodash debounce (remove unnecessary arg)

* Major a11y fix when tabbing
Major fix for ace in IE11 and Edge browsers

* Update comment
This commit is contained in:
Jean-Louis Leysens 2019-09-12 13:28:29 +02:00 committed by GitHub
parent 19837fea23
commit 5c2d0cae9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
292 changed files with 2484 additions and 1263 deletions

View file

@ -39,7 +39,7 @@ class MyPlugin {
id: 'my-app',
title: 'My Application',
async mount(context, params) {
const { renderApp } = await import('./applcation');
const { renderApp } = await import('./application');
return renderApp(context, params);
}
});
@ -267,7 +267,7 @@ export class MyPlugin {
application.register({
id: 'my-app',
async mount(context, params) {
const { renderApp } = await import('./applcation');
const { renderApp } = await import('./application');
return renderApp(context, params);
}
});

View file

@ -23,18 +23,17 @@ import { resolve, join, sep } from 'path';
import url from 'url';
import { has, isEmpty, head, pick } from 'lodash';
import { resolveApi } from './api_server/server';
import { addExtensionSpecFilePath } from './api_server/spec';
// @ts-ignore
import { resolveApi } from './server/api_server/server';
// @ts-ignore
import { addExtensionSpecFilePath } from './server/api_server/spec';
// @ts-ignore
import { setHeaders } from './server/set_headers';
// @ts-ignore
import { ProxyConfigCollection, getElasticsearchProxyConfig, createProxyRoute } from './server';
import {
ProxyConfigCollection,
getElasticsearchProxyConfig,
createProxyRoute
} from './server';
function filterHeaders(originalHeaders, headersToKeep) {
const normalizeHeader = function (header) {
function filterHeaders(originalHeaders: any, headersToKeep: any) {
const normalizeHeader = function(header: any) {
if (!header) {
return '';
}
@ -48,51 +47,64 @@ function filterHeaders(originalHeaders, headersToKeep) {
return pick(originalHeaders, headersToKeepNormalized);
}
export default function (kibana) {
// eslint-disable-next-line
export default function(kibana: any) {
const modules = resolve(__dirname, 'public/webpackShims/');
const src = resolve(__dirname, 'public/src/');
const quarantinedSrc = resolve(__dirname, 'public/quarantined/src/');
const npSrc = resolve(__dirname, 'np_ready/public');
let defaultVars;
const apps = [];
let defaultVars: any;
const apps: any[] = [];
return new kibana.Plugin({
id: 'console',
require: ['elasticsearch'],
config: function (Joi) {
config(Joi: any) {
return Joi.object({
enabled: Joi.boolean().default(true),
proxyFilter: Joi.array().items(Joi.string()).single().default(['.*']),
proxyFilter: Joi.array()
.items(Joi.string())
.single()
.default(['.*']),
ssl: Joi.object({
verify: Joi.boolean(),
}).default(),
proxyConfig: Joi.array().items(
Joi.object().keys({
match: Joi.object().keys({
protocol: Joi.string().default('*'),
host: Joi.string().default('*'),
port: Joi.string().default('*'),
path: Joi.string().default('*')
}),
proxyConfig: Joi.array()
.items(
Joi.object().keys({
match: Joi.object().keys({
protocol: Joi.string().default('*'),
host: Joi.string().default('*'),
port: Joi.string().default('*'),
path: Joi.string().default('*'),
}),
timeout: Joi.number(),
ssl: Joi.object().keys({
verify: Joi.boolean(),
ca: Joi.array().single().items(Joi.string()),
cert: Joi.string(),
key: Joi.string()
}).default()
})
).default()
timeout: Joi.number(),
ssl: Joi.object()
.keys({
verify: Joi.boolean(),
ca: Joi.array()
.single()
.items(Joi.string()),
cert: Joi.string(),
key: Joi.string(),
})
.default(),
})
)
.default(),
}).default();
},
deprecations: function () {
deprecations() {
return [
(settings, log) => {
(settings: any, log: any) => {
if (has(settings, 'proxyConfig')) {
log('Config key "proxyConfig" is deprecated. Configuration can be inferred from the "elasticsearch" settings');
log(
'Config key "proxyConfig" is deprecated. Configuration can be inferred from the "elasticsearch" settings'
);
}
}
},
];
},
@ -105,16 +117,18 @@ export default function (kibana) {
};
},
async init(server, options) {
async init(server: any, options: any) {
server.expose('addExtensionSpecFilePath', addExtensionSpecFilePath);
if (options.ssl && options.ssl.verify) {
throw new Error('sense.ssl.verify is no longer supported.');
}
const config = server.config();
const legacyEsConfig = await server.newPlatform.setup.core.elasticsearch.legacy.config$.pipe(first()).toPromise();
const legacyEsConfig = await server.newPlatform.setup.core.elasticsearch.legacy.config$
.pipe(first())
.toPromise();
const proxyConfigCollection = new ProxyConfigCollection(options.proxyConfig);
const proxyPathFilters = options.proxyFilter.map(str => new RegExp(str));
const proxyPathFilters = options.proxyFilter.map((str: string) => new RegExp(str));
defaultVars = {
elasticsearchUrl: url.format(
@ -122,54 +136,59 @@ export default function (kibana) {
),
};
server.route(createProxyRoute({
baseUrl: head(legacyEsConfig.hosts),
pathFilters: proxyPathFilters,
getConfigForReq(req, uri) {
const filteredHeaders = filterHeaders(req.headers, legacyEsConfig.requestHeadersWhitelist);
const headers = setHeaders(filteredHeaders, legacyEsConfig.customHeaders);
server.route(
createProxyRoute({
baseUrl: head(legacyEsConfig.hosts),
pathFilters: proxyPathFilters,
getConfigForReq(req: any, uri: any) {
const filteredHeaders = filterHeaders(
req.headers,
legacyEsConfig.requestHeadersWhitelist
);
const headers = setHeaders(filteredHeaders, legacyEsConfig.customHeaders);
if (!isEmpty(config.get('console.proxyConfig'))) {
return {
...proxyConfigCollection.configForUri(uri),
headers,
};
}
if (!isEmpty(config.get('console.proxyConfig'))) {
return {
...proxyConfigCollection.configForUri(uri),
...getElasticsearchProxyConfig(legacyEsConfig),
headers,
};
}
return {
...getElasticsearchProxyConfig(legacyEsConfig),
headers,
};
}
}));
},
})
);
server.route({
path: '/api/console/api_server',
method: ['GET', 'POST'],
handler: function (req, h) {
handler(req: any, h: any) {
const { sense_version: version, apis } = req.query;
if (!apis) {
throw Boom.badRequest('"apis" is a required param.');
}
return resolveApi(version, apis.split(','), h);
}
},
});
},
uiExports: {
apps: apps,
hacks: ['plugins/console/hacks/register'],
devTools: ['plugins/console/console'],
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
apps,
hacks: ['plugins/console/quarantined/hacks/register'],
devTools: [`${npSrc}/legacy`],
styleSheetPaths: resolve(__dirname, 'public/quarantined/index.scss'),
injectDefaultVars: () => defaultVars,
noParse: [
join(modules, 'ace' + sep),
join(modules, 'moment_src/moment' + sep),
join(src, 'sense_editor/mode/worker.js')
]
}
});
join(quarantinedSrc, 'sense_editor/mode/worker.js'),
],
},
} as any);
}

View file

@ -0,0 +1,6 @@
{
"id": "console",
"version": "kibana",
"server": true,
"ui": true
}

View file

@ -17,23 +17,25 @@
* under the License.
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React, {
Component,
} from 'react';
import {
EuiButtonIcon,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiPopover,
} from '@elastic/eui';
import { EuiButtonIcon, EuiContextMenuPanel, EuiContextMenuItem, EuiPopover } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export class ConsoleMenu extends Component {
constructor(props) {
interface Props {
getCurl: (cb: (text: string) => void) => void;
getDocumentation: () => Promise<string | null>;
autoIndent: (ev: React.MouseEvent) => void;
}
interface State {
isPopoverOpen: boolean;
curlCode: string;
}
export class ConsoleMenu extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
@ -47,13 +49,13 @@ export class ConsoleMenu extends Component {
this.props.getCurl(text => {
this.setState({ curlCode: text });
});
}
};
copyAsCurl() {
this.copyText(this.state.curlCode);
}
copyText(text) {
copyText(text: string) {
const textField = document.createElement('textarea');
textField.innerText = text;
document.body.appendChild(textField);
@ -74,17 +76,27 @@ export class ConsoleMenu extends Component {
});
};
openDocs = () => {
openDocs = async () => {
this.closePopover();
this.props.getDocumentation();
this.props.openDocumentation();
}
const documentation = await this.props.getDocumentation();
if (!documentation) {
return;
}
window.open(documentation, '_blank');
};
// Using `any` here per this issue: https://github.com/elastic/eui/issues/2265
autoIndent: any = (event: React.MouseEvent) => {
this.closePopover();
this.props.autoIndent(event);
};
render() {
const button = (
<EuiButtonIcon
iconType="wrench"
onClick={this.onButtonClick}
// @ts-ignore
aria-label={
<FormattedMessage
id="console.requestOptionsButtonAriaLabel"
@ -95,39 +107,37 @@ export class ConsoleMenu extends Component {
);
const items = [
(
<EuiContextMenuItem
key="Copy as cURL"
id="ConCopyAsCurl"
disabled={!document.queryCommandSupported('copy')}
onClick={() => { this.closePopover(); this.copyAsCurl(); }}
>
<FormattedMessage
id="console.requestOptions.copyAsUrlButtonLabel"
defaultMessage="Copy as cURL"
/>
</EuiContextMenuItem>
), (
<EuiContextMenuItem
key="Open documentation"
onClick={() => { this.openDocs(); }}
>
<FormattedMessage
id="console.requestOptions.openDocumentationButtonLabel"
defaultMessage="Open documentation"
/>
</EuiContextMenuItem>
), (
<EuiContextMenuItem
key="Auto indent"
onClick={(event) => { this.closePopover(); this.props.autoIndent(event); }}
>
<FormattedMessage
id="console.requestOptions.autoIndentButtonLabel"
defaultMessage="Auto indent"
/>
</EuiContextMenuItem>
)
<EuiContextMenuItem
key="Copy as cURL"
id="ConCopyAsCurl"
disabled={!document.queryCommandSupported('copy')}
onClick={() => {
this.closePopover();
this.copyAsCurl();
}}
>
<FormattedMessage
id="console.requestOptions.copyAsUrlButtonLabel"
defaultMessage="Copy as cURL"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem
key="Open documentation"
onClick={() => {
this.openDocs();
}}
>
<FormattedMessage
id="console.requestOptions.openDocumentationButtonLabel"
defaultMessage="Open documentation"
/>
</EuiContextMenuItem>,
<EuiContextMenuItem key="Auto indent" onClick={this.autoIndent}>
<FormattedMessage
id="console.requestOptions.autoIndentButtonLabel"
defaultMessage="Auto indent"
/>
</EuiContextMenuItem>,
];
return (
@ -140,18 +150,9 @@ export class ConsoleMenu extends Component {
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel
items={items}
/>
<EuiContextMenuPanel items={items} />
</EuiPopover>
</span>
);
}
}
ConsoleMenu.propTypes = {
getCurl: PropTypes.func.isRequired,
openDocumentation: PropTypes.func.isRequired,
getDocumentation: PropTypes.func.isRequired,
autoIndent: PropTypes.func.isRequired,
};

View file

@ -19,10 +19,10 @@
import React, { useEffect } from 'react';
// @ts-ignore
import exampleText from 'raw-loader!./help_example.txt';
import exampleText from 'raw-loader!../constants/help_example.txt';
import $ from 'jquery';
// @ts-ignore
import SenseEditor from '../sense_editor/editor';
import SenseEditor from '../../../../public/quarantined/src/sense_editor/editor';
interface EditorExampleProps {
panel: string;

View file

@ -0,0 +1,25 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './split_panel';
export { TopNavMenuItem, TopNavMenu } from './top_nav_menu';
export { ConsoleMenu } from './console_menu';
export { WelcomePanel } from './welcome_panel';
export { AutocompleteOptions, DevToolsSettingsModal } from './settings_modal';
export { HelpPanel } from './help_panel';

View file

@ -35,7 +35,8 @@ import {
EuiOverlayMask,
EuiSwitch,
} from '@elastic/eui';
import { DevToolsSettings } from './dev_tools_settings';
import { DevToolsSettings } from '../../services';
export type AutocompleteOptions = 'fields' | 'indices' | 'templates';
@ -145,11 +146,9 @@ export function DevToolsSettingsModal(props: Props) {
onClick={() => {
// Only refresh the currently selected settings.
props.refreshAutocompleteSettings({
autocomplete: {
fields,
indices,
templates,
},
fields,
indices,
templates,
});
}}
>

View file

@ -0,0 +1,93 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Split panel should render correctly 1`] = `
<PanelsContainer>
<PanelContextProvider
registry={
PanelRegistry {
"panels": Array [
Object {
"getWidth": [Function],
"initialWidth": "100%",
"setWidth": [Function],
},
Object {
"getWidth": [Function],
"initialWidth": "100%",
"setWidth": [Function],
},
],
}
}
>
<div
onMouseMove={[Function]}
onMouseUp={[Function]}
style={
Object {
"display": "flex",
"height": "100%",
"width": "100%",
}
}
>
<Panel
key=".0"
>
<div
style={
Object {
"display": "flex",
"width": "100%",
}
}
>
<p
style={
Object {
"width": "50px",
}
}
>
A
</p>
</div>
</Panel>
<Resizer
key="resizer"
onMouseDown={[Function]}
>
<div
className="conApp__resizer"
data-test-subj="splitPanelResizer"
onMouseDown={[Function]}
>
</div>
</Resizer>
<Panel
key=".1"
>
<div
style={
Object {
"display": "flex",
"width": "100%",
}
}
>
<p
style={
Object {
"width": "50px",
}
}
>
B
</p>
</div>
</Panel>
</div>
</PanelContextProvider>
</PanelsContainer>
`;

View file

@ -0,0 +1,37 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
type ResizerMouseEvent = React.MouseEvent<HTMLDivElement, MouseEvent>;
export interface Props {
onMouseDown: (eve: ResizerMouseEvent) => void;
}
/**
* TODO: This component uses styling constants from public UI - should be removed, next iteration should incl. horizontal and vertical resizers.
*/
export function Resizer(props: Props) {
return (
<div {...props} className="conApp__resizer" data-test-subj="splitPanelResizer">
&#xFE19;
</div>
);
}

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
import { usePanelContext } from '../context';
export interface Props {
children: ReactNode[] | ReactNode;
initialWidth?: string;
style?: CSSProperties;
}
export function Panel({ children, initialWidth = '100%', style = {} }: Props) {
const [width, setWidth] = useState(initialWidth);
const { registry } = usePanelContext();
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
registry.registerPanel({
initialWidth,
setWidth(value) {
setWidth(value + '%');
},
getWidth() {
return divRef.current!.getBoundingClientRect().width;
},
});
}, []);
return (
<div ref={divRef} style={{ ...style, width, display: 'flex' }}>
{children}
</div>
);
}

View file

@ -0,0 +1,99 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { Children, ReactNode, useRef, useState } from 'react';
import { PanelContextProvider } from '../context';
import { Resizer } from '../components/resizer';
import { PanelRegistry } from '../registry';
export interface Props {
children: ReactNode;
onPanelWidthChange?: (arrayOfPanelWidths: number[]) => any;
}
interface State {
isDragging: boolean;
currentResizerPos: number;
}
const initialState: State = { isDragging: false, currentResizerPos: -1 };
const pxToPercent = (proportion: number, whole: number) => (proportion / whole) * 100;
export function PanelsContainer({ children, onPanelWidthChange }: Props) {
const [firstChild, secondChild] = Children.toArray(children);
const registryRef = useRef(new PanelRegistry());
const containerRef = useRef<HTMLDivElement>(null);
const [state, setState] = useState<State>(initialState);
const getContainerWidth = () => {
return containerRef.current!.getBoundingClientRect().width;
};
const childrenWithResizer = [
firstChild,
<Resizer
key={'resizer'}
onMouseDown={event => {
event.preventDefault();
setState({
...state,
isDragging: true,
currentResizerPos: event.clientX,
});
}}
/>,
secondChild,
];
return (
<PanelContextProvider registry={registryRef.current}>
<div
ref={containerRef}
style={{ display: 'flex', height: '100%', width: '100%' }}
onMouseMove={event => {
if (state.isDragging) {
const { clientX: x } = event;
const { current: registry } = registryRef;
const [left, right] = registry.getPanels();
const delta = x - state.currentResizerPos;
const containerWidth = getContainerWidth();
const leftPercent = pxToPercent(left.getWidth() + delta, containerWidth);
const rightPercent = pxToPercent(right.getWidth() - delta, containerWidth);
left.setWidth(leftPercent);
right.setWidth(rightPercent);
if (onPanelWidthChange) {
onPanelWidthChange([leftPercent, rightPercent]);
}
setState({ ...state, currentResizerPos: x });
}
}}
onMouseUp={() => {
setState(initialState);
}}
>
{childrenWithResizer}
</div>
</PanelContextProvider>
);
}

View file

@ -17,31 +17,24 @@
* under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { I18nContext } from 'ui/i18n';
import { HelpPanel } from '../components/help_panel';
import React, { createContext, useContext } from 'react';
import { PanelRegistry } from './registry';
let isOpen = false;
const PanelContext = createContext({ registry: new PanelRegistry() });
export function showHelpPanel(): () => void {
const onClose = () => {
if (!container) return;
ReactDOM.unmountComponentAtNode(container);
isOpen = false;
};
const container = document.getElementById('consoleHelpPanel');
if (container && !isOpen) {
isOpen = true;
const element = (
<I18nContext>
<HelpPanel onClose={onClose} />
</I18nContext>
);
ReactDOM.render(element, container);
}
return onClose;
interface ContextProps {
children: any;
registry: PanelRegistry;
}
export function PanelContextProvider({ children, registry }: ContextProps) {
return <PanelContext.Provider value={{ registry }}>{children}</PanelContext.Provider>;
}
export const usePanelContext = () => {
const context = useContext(PanelContext);
if (context === undefined) {
throw new Error('usePanelContext must be used within a <PanelContextProvider />');
}
return context;
};

View file

@ -17,14 +17,5 @@
* under the License.
*/
export interface DevToolsSettings {
fontSize: number;
wrapMode: boolean;
autocomplete: {
fields: boolean;
indices: boolean;
templates: boolean;
};
polling: boolean;
tripleQuotes: boolean;
}
export { Panel } from './containers/panel';
export { PanelsContainer } from './containers/panel_container';

View file

@ -0,0 +1,40 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export interface PanelController {
setWidth: (percent: number) => void;
getWidth: () => number;
initialWidth: string;
}
export class PanelRegistry {
private panels: PanelController[] = [];
registerPanel(panel: PanelController) {
this.panels.push(panel);
}
getResizerNeighbours(idx: number) {
return [this.panels[idx], this.panels[idx + 1]];
}
getPanels() {
return this.panels.map(panel => ({ ...panel }));
}
}

View file

@ -0,0 +1,86 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { mount } from 'enzyme';
import toJson from 'enzyme-to-json';
import { spy } from 'sinon';
import { PanelsContainer, Panel } from '.';
const testComponentA = <p style={{ width: '50px' }}>A</p>;
const testComponentB = <p style={{ width: '50px' }}>B</p>;
describe('Split panel', () => {
it('should render correctly', () => {
const panelContainer = mount(
<PanelsContainer>
<Panel>{testComponentA}</Panel>
<Panel>{testComponentB}</Panel>
</PanelsContainer>
);
expect(toJson(panelContainer)).toMatchSnapshot();
});
it('should calculate sizes correctly on mouse drags', () => {
// Since this test is not running in the browser we can't expect all of the
// APIs for sizing to be available. The below is a very hacky way of setting
// the DOMElement width so that we have a lightweight test for width calculation
// logic.
const div = mount(<div />);
const proto = (div
.find('div')
.first()
.getDOMNode() as any).__proto__;
const originalGetBoundingClientRect = proto.getBoundingClientRect;
proto.getBoundingClientRect = spy(() => {
return {
width: 1000,
};
});
try {
// Everything here runs sync.
let widthsCache: number[] = [];
const onWidthChange = (widths: number[]) => {
widthsCache = widths;
};
const panelContainer = mount(
<PanelsContainer onPanelWidthChange={onWidthChange}>
<Panel initialWidth={'50%'}>{testComponentA}</Panel>
<Panel initialWidth={'50%'}>{testComponentB}</Panel>
</PanelsContainer>
);
const resizer = panelContainer.find(`[data-test-subj~="splitPanelResizer"]`).first();
resizer.simulate('mousedown', { clientX: 0 });
resizer.simulate('mousemove', { clientX: 250 });
resizer.simulate('mouseup');
panelContainer.update();
expect(widthsCache).toEqual([125, 75]);
} finally {
proto.getBoundingClientRect = originalGetBoundingClientRect;
}
});
});

View file

@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { EuiTabs, EuiTab } from '@elastic/eui';
export interface TopNavMenuItem {
id: string;
label: string;
description: string;
onClick: () => void;
testId: string;
}
interface Props {
items: TopNavMenuItem[];
}
export function TopNavMenu({ items }: Props) {
return (
<EuiTabs size="s">
{items.map((item, idx) => {
return (
<EuiTab key={idx} onClick={item.onClick} title={item.label} data-test-subj={item.testId}>
{item.label}
</EuiTab>
);
})}
</EuiTabs>
);
}

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { Editor, EditorOutput, ConsoleHistory, autoIndent, getDocumentation } from './legacy';

View file

@ -0,0 +1,130 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { CSSProperties, useEffect, useRef, useState } from 'react';
import { EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import $ from 'jquery';
import { EuiIcon } from '@elastic/eui';
import { useAppContext } from '../../../../context';
import { useUIAceKeyboardMode } from '../use_ui_ace_keyboard_mode';
import { ConsoleMenu } from '../../../../components';
import { autoIndent, getDocumentation } from '../console_menu_actions';
import { registerCommands } from './keyboard_shortcuts';
// @ts-ignore
import { initializeInput } from '../../../../../../../public/quarantined/src/input';
export interface EditorProps {
onEditorReady?: (editor: any) => void;
sendCurrentRequest?: () => void;
docLinkVersion: string;
}
const abs: CSSProperties = {
position: 'absolute',
top: '0',
left: '0',
bottom: '0',
right: '0',
};
function Component({ onEditorReady, docLinkVersion, sendCurrentRequest = () => {} }: EditorProps) {
const {
services: { history, settings },
} = useAppContext();
const editorRef = useRef<HTMLDivElement | null>(null);
const actionsRef = useRef<HTMLDivElement | null>(null);
const editorInstanceRef = useRef<any | null>(null);
const [textArea, setTextArea] = useState<HTMLTextAreaElement | null>(null);
useUIAceKeyboardMode(textArea);
const openDocumentation = async () => {
const documentation = await getDocumentation(editorInstanceRef.current!, docLinkVersion);
if (!documentation) {
return;
}
window.open(documentation, '_blank');
};
useEffect(() => {
const $editor = $(editorRef.current!);
const $actions = $(actionsRef.current!);
editorInstanceRef.current = initializeInput($editor, $actions, history, settings);
if (onEditorReady) {
onEditorReady({ editor: editorInstanceRef.current, element: editorRef.current! });
}
setTextArea(editorRef.current!.querySelector('textarea'));
}, []);
useEffect(() => {
registerCommands({
input: editorInstanceRef.current,
sendCurrentRequestToES: sendCurrentRequest,
openDocumentation,
});
}, [sendCurrentRequest]);
return (
<div style={abs} className="conApp">
<div className="conApp__editor">
<ul className="conApp__autoComplete" id="autocomplete" />
<div ref={actionsRef} className="conApp__editorActions" id="ConAppEditorActions">
<EuiToolTip
content={i18n.translate('console.sendRequestButtonTooltip', {
defaultMessage: 'click to send request',
})}
>
<button
onClick={sendCurrentRequest}
data-test-subj="send-request-button"
className="conApp__editorActionButton conApp__editorActionButton--success"
>
<EuiIcon type="play" />
</button>
</EuiToolTip>
<ConsoleMenu
getCurl={(cb: any) => {
editorInstanceRef.current!.getRequestsAsCURL(cb);
}}
getDocumentation={() => {
return getDocumentation(editorInstanceRef.current!, docLinkVersion);
}}
autoIndent={(event: any) => {
autoIndent(editorInstanceRef.current!, event);
}}
/>
</div>
<div
ref={editorRef}
id="ConAppEditor"
className="conApp__editorContent"
data-test-subj="request-editor"
/>
</div>
</div>
);
}
export const Editor = React.memo(Component);

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useRef } from 'react';
import $ from 'jquery';
// @ts-ignore
import { initializeOutput } from '../../../../../../../public/quarantined/src/output';
import { useAppContext } from '../../../../context';
export interface EditorOutputProps {
onReady?: (ref: any) => void;
}
function Component({ onReady }: EditorOutputProps) {
const editorRef = useRef<null | HTMLDivElement>(null);
const {
services: { settings },
} = useAppContext();
useEffect(() => {
const editor$ = $(editorRef.current!);
const outputEditor = initializeOutput(editor$, settings);
if (onReady) {
onReady({ editor: outputEditor, element: editorRef.current! });
}
});
return (
<div ref={editorRef} className="conApp__output" data-test-subj="response-editor">
<div className="conApp__outputContent" id="ConAppOutput" />
</div>
);
}
export const EditorOutput = React.memo(Component);

View file

@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { Editor } from './editor';
export { EditorOutput } from './editor_output';

View file

@ -0,0 +1,60 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
interface Actions {
input: any;
sendCurrentRequestToES: () => void;
openDocumentation: () => void;
}
export function registerCommands({ input, sendCurrentRequestToES, openDocumentation }: Actions) {
input.commands.addCommand({
name: 'send to elasticsearch',
bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },
exec: () => sendCurrentRequestToES(),
});
input.commands.addCommand({
name: 'open documentation',
bindKey: { win: 'Ctrl-/', mac: 'Command-/' },
exec: () => {
openDocumentation();
},
});
input.commands.addCommand({
name: 'auto indent request',
bindKey: { win: 'Ctrl-I', mac: 'Command-I' },
exec: () => {
input.autoIndent();
},
});
input.commands.addCommand({
name: 'move to previous request start or end',
bindKey: { win: 'Ctrl-Up', mac: 'Command-Up' },
exec: () => {
input.moveToPreviousRequestEdge();
},
});
input.commands.addCommand({
name: 'move to next request start or end',
bindKey: { win: 'Ctrl-Down', mac: 'Command-Down' },
exec: () => {
input.moveToNextRequestEdge();
},
});
}

View file

@ -0,0 +1,225 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useRef, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { memoize } from 'lodash';
import moment from 'moment';
import {
keyCodes,
EuiSpacer,
EuiIcon,
EuiTitle,
EuiFlexItem,
EuiFlexGroup,
EuiButtonEmpty,
EuiButton,
} from '@elastic/eui';
import { useAppContext } from '../../../../context';
import { HistoryViewer } from './history_viewer';
interface Props {
close: () => void;
clearHistory: () => void;
restoreFromHistory: (req: any) => void;
requests: any[];
}
const CHILD_ELEMENT_PREFIX = 'historyReq';
export function ConsoleHistory({ close, requests, clearHistory, restoreFromHistory }: Props) {
const {
services: { settings },
ResizeChecker,
} = useAppContext();
const listRef = useRef<HTMLUListElement | null>(null);
const [viewingReq, setViewingReq] = useState<any>(null);
const [selectedIndex, setSelectedIndex] = useState<number>(0);
const selectedReq = useRef<any>(null);
const scrollIntoView = (idx: number) => {
const activeDescendant = listRef.current!.querySelector(`#${CHILD_ELEMENT_PREFIX}${idx}`);
if (activeDescendant) {
activeDescendant.scrollIntoView();
}
};
const [describeReq] = useState(() => {
const _describeReq = (req: any) => {
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 initialize = () => {
const nextSelectedIndex = 0;
(describeReq as any).cache = new WeakMap();
setViewingReq(requests[nextSelectedIndex]);
selectedReq.current = requests[nextSelectedIndex];
setSelectedIndex(nextSelectedIndex);
scrollIntoView(nextSelectedIndex);
};
const clear = () => {
clearHistory();
initialize();
};
const restore = (req: any = selectedReq.current) => {
restoreFromHistory(req);
};
useEffect(() => {
initialize();
}, [requests]);
/* eslint-disable */
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.keyCode === keyCodes.ENTER) {
restore();
return;
}
let currentIdx = selectedIndex;
if (ev.keyCode === keyCodes.UP) {
ev.preventDefault();
--currentIdx;
} else if (ev.keyCode === keyCodes.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
// eslint-disable-next-line
<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);
scrollIntoView(idx);
}}
role="option"
onMouseEnter={() => setViewingReq(req)}
onMouseLeave={() => setViewingReq(selectedReq.current)}
onDoubleClick={() => restore(req)}
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" />
<HistoryViewer settings={settings} req={viewingReq} ResizeChecker={ResizeChecker} />
</div>
<EuiSpacer size="s" />
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButtonEmpty 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 color="primary" onClick={() => close()}>
{i18n.translate('console.historyPage.closehistoryButtonLabel', {
defaultMessage: 'Close',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton color="primary" disabled={!selectedReq} onClick={() => restore()}>
{i18n.translate('console.historyPage.applyHistoryButtonLabel', {
defaultMessage: 'Apply',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</div>
<EuiSpacer size="s" />
</>
);
}

View file

@ -0,0 +1,66 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import $ from 'jquery';
import { Settings } from '../../../../../services';
import { subscribeResizeChecker } from '../subscribe_console_resize_checker';
// @ts-ignore
import SenseEditor from '../../../../../../../public/quarantined/src/sense_editor/editor';
interface Props {
settings: Settings;
req: any | null;
ResizeChecker: any;
}
export function HistoryViewer({ settings, ResizeChecker, req }: Props) {
const divRef = useRef<HTMLDivElement | null>(null);
const viewerRef = useRef<any | null>(null);
useEffect(() => {
const viewer = new SenseEditor($(divRef.current!));
viewerRef.current = viewer;
viewer.renderer.setShowPrintMargin(false);
viewer.$blockScrolling = Infinity;
const unsubscribe = subscribeResizeChecker(ResizeChecker, divRef.current!, viewer);
settings.applyCurrentSettings(viewer);
return () => unsubscribe();
}, []);
if (viewerRef.current) {
const { current: viewer } = viewerRef;
if (req) {
const s = req.method + ' ' + req.endpoint + '\n' + (req.data || '');
viewer.setValue(s);
viewer.clearSelection();
} else {
viewer.getSession().setValue(
i18n.translate('console.historyPage.noHistoryTextMessage', {
defaultMessage: 'No history available',
})
);
}
}
return <div className="conHistory__viewer" ref={divRef} />;
}

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { ConsoleHistory } from './console_history';

View file

@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// @ts-ignore
import { getEndpointFromPosition } from '../../../../../../public/quarantined/src/autocomplete';
export function autoIndent(editor: any, event: any) {
editor.autoIndent();
event.preventDefault();
editor.focus();
}
export function getDocumentation(editor: any, docLinkVersion: string): Promise<string | null> {
return new Promise(resolve => {
editor.getRequestsInRange((requests: any) => {
if (!requests || requests.length === 0) {
resolve(null);
return;
}
const position = requests[0].range.end;
position.column = position.column - 1;
const endpoint = getEndpointFromPosition(editor, position);
if (endpoint && endpoint.documentation && endpoint.documentation.indexOf('http') !== -1) {
const nextDocumentation = endpoint.documentation
.replace('/master/', `/${docLinkVersion}/`)
.replace('/current/', `/${docLinkVersion}/`);
resolve(nextDocumentation);
} else {
resolve(null);
}
});
});
}

View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { EditorOutput, Editor } from './console_editor';
export { ConsoleHistory } from './console_history';
export { getDocumentation, autoIndent } from './console_menu_actions';

View file

@ -17,10 +17,8 @@
* under the License.
*/
import { ResizeChecker } from 'ui/resize_checker';
export function applyResizeCheckerToEditors($scope, $el, ...editors) {
export function subscribeResizeChecker(ResizeChecker: any, $el: any, ...editors: any[]) {
const checker = new ResizeChecker($el);
checker.on('resize', () => editors.forEach(e => e.resize()));
$scope.$on('$destroy', () => checker.destroy());
return () => checker.destroy();
}

View file

@ -0,0 +1,106 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useRef } from 'react';
import * as ReactDOM from 'react-dom';
import { keyCodes, EuiText } from '@elastic/eui';
const OverlayText = () => (
// The point of this element is for accessibility purposes, so ignore eslint error
// in this case
//
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<>
<EuiText size="s">Press Enter to start editing.</EuiText>
<EuiText size="s">When you&rsquo;re done, press Escape to stop editing.</EuiText>
</>
);
export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | null) {
const overlayMountNode = useRef<HTMLDivElement | null>(null);
const autoCompleteVisibleRef = useRef<boolean>(false);
function onDismissOverlay(event: KeyboardEvent) {
if (event.keyCode === keyCodes.ENTER) {
event.preventDefault();
aceTextAreaElement!.focus();
}
}
function enableOverlay() {
if (overlayMountNode.current) {
overlayMountNode.current.focus();
}
}
const isAutoCompleteVisible = () => {
const autoCompleter = document.querySelector<HTMLDivElement>('.ace_autocomplete');
if (!autoCompleter) {
return false;
}
// The autoComplete is just hidden when it's closed, not removed from the DOM.
return autoCompleter.style.display !== 'none';
};
const documentKeyDownListener = () => {
autoCompleteVisibleRef.current = isAutoCompleteVisible();
};
const aceKeydownListener = (event: KeyboardEvent) => {
if (event.keyCode === keyCodes.ESCAPE && !autoCompleteVisibleRef.current) {
event.preventDefault();
event.stopPropagation();
enableOverlay();
}
};
useEffect(() => {
if (aceTextAreaElement) {
// We don't control HTML elements inside of ace so we imperatively create an element
// that acts as a container and insert it just before ace's textarea element
// so that the overlay lives at the correct spot in the DOM hierarchy.
overlayMountNode.current = document.createElement('div');
overlayMountNode.current.className = 'kbnUiAceKeyboardHint';
overlayMountNode.current.setAttribute('role', 'application');
overlayMountNode.current.tabIndex = 0;
overlayMountNode.current.addEventListener('focus', enableOverlay);
overlayMountNode.current.addEventListener('keydown', onDismissOverlay);
ReactDOM.render(<OverlayText />, overlayMountNode.current);
aceTextAreaElement.parentElement!.insertBefore(overlayMountNode.current, aceTextAreaElement);
aceTextAreaElement.setAttribute('tabindex', '-1');
// Order of events:
// 1. Document capture event fires first and we check whether an autocomplete menu is open on keydown
// (not ideal because this is scoped to the entire document).
// 2. Ace changes it's state (like hiding or showing autocomplete menu)
// 3. We check what button was pressed and whether autocomplete was visible then determine
// whether it should act like a dismiss or if we should display an overlay.
document.addEventListener('keydown', documentKeyDownListener, { capture: true });
aceTextAreaElement.addEventListener('keydown', aceKeydownListener);
}
return () => {
if (aceTextAreaElement) {
document.removeEventListener('keydown', documentKeyDownListener);
aceTextAreaElement.removeEventListener('keydown', aceKeydownListener);
document.removeChild(overlayMountNode.current!);
}
};
}, [aceTextAreaElement]);
}

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { Main } from './main';

View file

@ -19,13 +19,13 @@
import { i18n } from '@kbn/i18n';
import { IScope } from 'angular';
import { showSettingsModal } from './settings_show_modal';
interface Props {
onClickHistory: () => void;
onClickSettings: () => void;
onClickHelp: () => void;
}
// help
import { showHelpPanel } from './help_show_panel';
export function getTopNavConfig($scope: IScope, toggleHistory: () => {}) {
export function getTopNavConfig({ onClickHistory, onClickSettings, onClickHelp }: Props) {
return [
{
id: 'history',
@ -35,8 +35,8 @@ export function getTopNavConfig($scope: IScope, toggleHistory: () => {}) {
description: i18n.translate('console.topNav.historyTabDescription', {
defaultMessage: 'History',
}),
run: () => {
$scope.$evalAsync(toggleHistory);
onClick: () => {
onClickHistory();
},
testId: 'consoleHistoryButton',
},
@ -48,8 +48,8 @@ export function getTopNavConfig($scope: IScope, toggleHistory: () => {}) {
description: i18n.translate('console.topNav.settingsTabDescription', {
defaultMessage: 'Settings',
}),
run: () => {
showSettingsModal();
onClick: () => {
onClickSettings();
},
testId: 'consoleSettingsButton',
},
@ -61,11 +61,8 @@ export function getTopNavConfig($scope: IScope, toggleHistory: () => {}) {
description: i18n.translate('console.topNav.helpTabDescription', {
defaultMessage: 'Help',
}),
run: () => {
const hideHelpPanel = showHelpPanel();
$scope.$on('$destroy', () => {
hideHelpPanel();
});
onClick: () => {
onClickHelp();
},
testId: 'consoleHelpButton',
},

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { Main } from './main';

View file

@ -0,0 +1,264 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { debounce } from 'lodash';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { BehaviorSubject, combineLatest } from 'rxjs';
// @ts-ignore
import mappings from '../../../../../public/quarantined/src/mappings';
// @ts-ignore
import init from '../../../../../public/quarantined/src/app';
import { EditorOutput, Editor, ConsoleHistory } from '../editor';
import { subscribeResizeChecker } from '../editor/legacy/subscribe_console_resize_checker';
import {
AutocompleteOptions,
TopNavMenu,
WelcomePanel,
DevToolsSettingsModal,
HelpPanel,
PanelsContainer,
Panel,
} from '../../components';
import { useAppContext } from '../../context';
import { StorageKeys, DevToolsSettings } from '../../../services';
import { getTopNavConfig } from './get_top_nav';
const INITIAL_PANEL_WIDTH = 50;
const PANEL_MIN_WIDTH = '100px';
// We only run certain initialization after we know all our editors have
// been instantiated -- which is what we use the below streams for.
const inputReadySubject$ = new BehaviorSubject<any>(null);
const outputReadySubject$ = new BehaviorSubject<any>(null);
const editorsReady$ = combineLatest(inputReadySubject$, outputReadySubject$);
export function Main() {
const {
services: { storage, settings, history },
docLinkVersion,
ResizeChecker,
} = useAppContext();
const [editorReady, setEditorReady] = useState<boolean>(false);
const [inputEditor, setInputEditor] = useState<any>(null);
const [outputEditor, setOutputEditor] = useState<any>(null);
const [showWelcome, setShowWelcomePanel] = useState(
() => storage.get('version_welcome_shown') !== '@@SENSE_REVISION'
);
const [showingHistory, setShowHistory] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [showHelp, setShowHelp] = useState(false);
const containerRef = useRef<null | HTMLDivElement>(null);
const onInputEditorReady = useCallback((value: any) => {
inputReadySubject$.next(value);
}, []);
const onOutputEditorReady = useCallback((value: any) => {
outputReadySubject$.next(value);
}, []);
const [firstPanelWidth, secondPanelWidth] = storage.get(StorageKeys.WIDTH, [
INITIAL_PANEL_WIDTH,
INITIAL_PANEL_WIDTH,
]);
const onPanelWidthChange = useCallback(
debounce((widths: number[]) => {
storage.set(StorageKeys.WIDTH, widths);
}, 300),
[]
);
const [pastRequests, setPastRequests] = useState<any[]>(() => history.getHistory());
const sendCurrentRequest = useCallback(() => {
inputEditor.focus();
inputEditor.sendCurrentRequestToES(() => {
setPastRequests(history.getHistory());
}, outputEditor);
}, [inputEditor, outputEditor]);
const clearHistory = useCallback(() => {
history.clearHistory();
setPastRequests(history.getHistory());
}, []);
const restoreFromHistory = useCallback((req: any) => {
history.restoreFromHistory(req);
}, []);
const renderConsoleHistory = () => {
return editorReady ? (
<ConsoleHistory
restoreFromHistory={restoreFromHistory}
clearHistory={clearHistory}
requests={pastRequests}
close={() => setShowHistory(false)}
/>
) : null;
};
const refreshAutocompleteSettings = (selectedSettings: any) => {
mappings.retrieveAutoCompleteInfo(selectedSettings);
};
const getAutocompleteDiff = (newSettings: DevToolsSettings, prevSettings: DevToolsSettings) => {
return Object.keys(newSettings.autocomplete).filter(key => {
// @ts-ignore
return prevSettings.autocomplete[key] !== newSettings.autocomplete[key];
});
};
const fetchAutocompleteSettingsIfNeeded = (
newSettings: DevToolsSettings,
prevSettings: DevToolsSettings
) => {
// We'll only retrieve settings if polling is on. The expectation here is that if the user
// disables polling it's because they want manual control over the fetch request (possibly
// because it's a very expensive request given their cluster and bandwidth). In that case,
// they would be unhappy with any request that's sent automatically.
if (newSettings.polling) {
const autocompleteDiff = getAutocompleteDiff(newSettings, prevSettings);
const isSettingsChanged = autocompleteDiff.length > 0;
const isPollingChanged = prevSettings.polling !== newSettings.polling;
if (isSettingsChanged) {
// If the user has changed one of the autocomplete settings, then we'll fetch just the
// ones which have changed.
const changedSettings: any = autocompleteDiff.reduce(
(changedSettingsAccum: any, setting: string): any => {
changedSettingsAccum[setting] =
newSettings.autocomplete[setting as AutocompleteOptions];
return changedSettingsAccum;
},
{}
);
mappings.retrieveAutoCompleteInfo(changedSettings.autocomplete);
} else if (isPollingChanged) {
// If the user has turned polling on, then we'll fetch all selected autocomplete settings.
mappings.retrieveAutoCompleteInfo();
}
}
};
const onSaveSettings = async (newSettings: DevToolsSettings) => {
const prevSettings = settings.getCurrentSettings();
settings.updateSettings(newSettings);
fetchAutocompleteSettingsIfNeeded(newSettings, prevSettings);
setShowSettings(false);
};
useEffect(() => {
let resizerSubscriptions: Array<() => void> = [];
const subscription = editorsReady$.subscribe(([input, output]) => {
settings.registerOutput(output.editor);
settings.registerInput(input.editor);
history.setEditor(input.editor);
init(input.editor, output.editor, history);
resizerSubscriptions = resizerSubscriptions.concat([
subscribeResizeChecker(ResizeChecker, containerRef.current!, input.editor, output.editor),
subscribeResizeChecker(ResizeChecker, input.element, input.editor),
subscribeResizeChecker(ResizeChecker, output.element, output.editor),
]);
setInputEditor(input.editor);
setOutputEditor(output.editor);
setEditorReady(true);
});
return () => {
resizerSubscriptions.map(done => done());
subscription.unsubscribe();
};
}, []);
return (
<div className="consoleContainer" style={{ height: '100%', width: '100%' }} ref={containerRef}>
<EuiFlexGroup
style={{ height: '100%' }}
gutterSize="none"
direction="column"
responsive={false}
>
<EuiFlexItem grow={false}>
<TopNavMenu
items={getTopNavConfig({
onClickHistory: () => setShowHistory(!showingHistory),
onClickSettings: () => setShowSettings(true),
onClickHelp: () => setShowHelp(!showHelp),
})}
/>
</EuiFlexItem>
{showingHistory ? <EuiFlexItem grow={false}>{renderConsoleHistory()}</EuiFlexItem> : null}
<EuiFlexItem>
<PanelsContainer onPanelWidthChange={onPanelWidthChange}>
<Panel
style={{ height: '100%', position: 'relative', minWidth: PANEL_MIN_WIDTH }}
initialWidth={firstPanelWidth + '%'}
>
<Editor
sendCurrentRequest={sendCurrentRequest}
onEditorReady={onInputEditorReady}
docLinkVersion={docLinkVersion}
/>
</Panel>
<Panel
style={{ height: '100%', position: 'relative', minWidth: PANEL_MIN_WIDTH }}
initialWidth={secondPanelWidth + '%'}
>
<EditorOutput onReady={onOutputEditorReady} />
</Panel>
</PanelsContainer>
</EuiFlexItem>
</EuiFlexGroup>
{showWelcome ? (
<WelcomePanel
onDismiss={() => {
storage.set('version_welcome_shown', '@@SENSE_REVISION');
setShowWelcomePanel(false);
}}
/>
) : null}
{showSettings ? (
<DevToolsSettingsModal
onSaveSettings={onSaveSettings}
onClose={() => setShowSettings(false)}
refreshAutocompleteSettings={refreshAutocompleteSettings}
settings={settings.getCurrentSettings()}
/>
) : null}
{showHelp ? <HelpPanel onClose={() => setShowHelp(false)} /> : null}
</div>
);
}

View file

@ -0,0 +1,50 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { createContext, useContext } from 'react';
import { History, Storage, Settings } from '../../services';
interface ContextValue {
services: {
history: History;
storage: Storage;
settings: Settings;
};
docLinkVersion: string;
ResizeChecker: any;
}
interface ContextProps {
value: ContextValue;
children: any;
}
const AppContext = createContext<ContextValue>(null as any);
export function AppContextProvider({ children, value }: ContextProps) {
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}
export const useAppContext = () => {
const context = useContext(AppContext);
if (context === undefined) {
throw new Error('useAppContext must be used inside the AppContextProvider.');
}
return context;
};

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { useAppContext, AppContextProvider } from './app_context';

View file

@ -0,0 +1,50 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { AppContextProvider } from './context';
import { Main } from './containers';
import { createStorage, createHistory, createSettings, Settings } from '../services';
let settingsRef: Settings;
export function legacyBackDoorToSettings() {
return settingsRef;
}
export function boot(deps: { docLinkVersion: string; I18nContext: any; ResizeChecker: any }) {
const { I18nContext, ResizeChecker } = deps;
const storage = createStorage({
engine: window.localStorage,
prefix: 'sense:',
});
const history = createHistory({ storage });
const settings = createSettings({ storage });
settingsRef = settings;
return (
<I18nContext>
<AppContextProvider
value={{ ...deps, services: { storage, history, settings }, ResizeChecker }}
>
<Main />
</AppContextProvider>
</I18nContext>
);
}

View file

Before

Width:  |  Height:  |  Size: 217 B

After

Width:  |  Height:  |  Size: 217 B

Before After
Before After

View file

@ -17,14 +17,12 @@
* under the License.
*/
import 'ngreact';
import { PluginInitializerContext } from '../../../../../core/public';
import { wrapInI18nContext } from 'ui/i18n';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/sense', ['react']);
import { ConsoleUIPlugin } from './plugin';
import { ConsoleMenu } from '../components/console_menu';
export { ConsoleUIPlugin as Plugin };
module.directive('consoleMenu', function (reactDirective) {
return reactDirective(wrapInI18nContext(ConsoleMenu));
});
export function plugin(ctx: PluginInitializerContext) {
return new ConsoleUIPlugin(ctx);
}

View file

@ -0,0 +1,93 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import 'brace';
import 'brace/ext/language_tools';
import 'brace/ext/searchbox';
import 'brace/mode/json';
import 'brace/mode/text';
/* eslint-disable @kbn/eslint/no-restricted-paths */
import { npSetup, npStart } from 'ui/new_platform';
import uiRoutes from 'ui/routes';
import { DOC_LINK_VERSION } from 'ui/documentation_links';
import { I18nContext } from 'ui/i18n';
import { ResizeChecker } from 'ui/resize_checker';
import 'ui/autoload/styles';
import 'ui/capabilities/route_setup';
/* eslint-enable @kbn/eslint/no-restricted-paths */
import template from '../../public/quarantined/index.html';
import { App } from '../../../../../core/public';
export interface XPluginSet {
__LEGACY: {
I18nContext: any;
ResizeChecker: any;
docLinkVersion: string;
};
}
import { plugin } from '.';
const pluginInstance = plugin({} as any);
const anyObject = {} as any;
uiRoutes.when('/dev_tools/console', {
requireUICapability: 'dev_tools.show',
controller: function RootController($scope) {
// Stub out this config for now...
$scope.topNavMenu = [];
$scope.initReactApp = () => {
const targetElement = document.querySelector<HTMLDivElement>('#consoleRoot');
if (!targetElement) {
const message = `Could not mount Console App!`;
npSetup.core.fatalErrors.add(message);
throw new Error(message);
}
const mockedSetupCore = {
...npSetup.core,
application: {
register(app: App): void {
try {
app.mount(anyObject, { element: targetElement, appBasePath: '' });
} catch (e) {
npSetup.core.fatalErrors.add(e);
}
},
registerMountContext() {},
},
};
pluginInstance.setup(mockedSetupCore, {
...npSetup.plugins,
__LEGACY: {
I18nContext,
ResizeChecker,
docLinkVersion: DOC_LINK_VERSION,
},
});
pluginInstance.start(npStart.core);
};
},
template,
});

View file

@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { render, unmountComponentAtNode } from 'react-dom';
import { PluginInitializerContext, Plugin, CoreStart, CoreSetup } from '../../../../../core/public';
import { XPluginSet } from './legacy';
import { boot } from './application';
export class ConsoleUIPlugin implements Plugin<any, any> {
// @ts-ignore
constructor(private readonly ctx: PluginInitializerContext) {}
async setup({ application }: CoreSetup, pluginSet: XPluginSet) {
const {
__LEGACY: { docLinkVersion, I18nContext, ResizeChecker },
} = pluginSet;
application.register({
id: 'console',
order: 1,
title: 'Console',
mount(ctx, { element }) {
render(boot({ docLinkVersion, I18nContext, ResizeChecker }), element);
return () => {
unmountComponentAtNode(element);
};
},
});
}
async start(core: CoreStart) {}
}

View file

@ -0,0 +1,117 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Storage } from './index';
export class History {
private editor: any;
constructor(private readonly storage: Storage) {}
setEditor(editor: any) {
this.editor = editor;
}
// stupid simple restore function, called when the user
// chooses to restore a request from the history
// PREVENTS history from needing to know about the input
restoreFromHistory(req: any) {
const session = this.editor.getSession();
let pos = this.editor.getCursorPosition();
let prefix = '';
let suffix = '\n';
if (this.editor.parser.isStartRequestRow(pos.row)) {
pos.column = 0;
suffix += '\n';
} else if (this.editor.parser.isEndRequestRow(pos.row)) {
const line = session.getLine(pos.row);
pos.column = line.length;
prefix = '\n\n';
} else if (this.editor.parser.isInBetweenRequestsRow(pos.row)) {
pos.column = 0;
} else {
pos = this.editor.nextRequestEnd(pos);
prefix = '\n\n';
}
let s = prefix + req.method + ' ' + req.endpoint;
if (req.data) {
s += '\n' + req.data;
}
s += suffix;
session.insert(pos, s);
this.editor.clearSelection();
this.editor.moveCursorTo(pos.row + prefix.length, 0);
this.editor.focus();
}
getHistoryKeys() {
return this.storage
.keys()
.filter((key: string) => key.indexOf('hist_elem') === 0)
.sort()
.reverse();
}
getHistory() {
return this.getHistoryKeys().map(key => this.storage.get(key));
}
addToHistory(endpoint: string, method: string, data: any) {
const keys = this.getHistoryKeys();
keys.splice(0, 500); // only maintain most recent X;
$.each(keys, (i, k) => {
this.storage.delete(k);
});
const timestamp = new Date().getTime();
const k = 'hist_elem_' + timestamp;
this.storage.set(k, {
time: timestamp,
endpoint,
method,
data,
});
}
updateCurrentState(content: any) {
const timestamp = new Date().getTime();
this.storage.set('editor_state', {
time: timestamp,
content,
});
}
getSavedEditorState() {
const saved = this.storage.get('editor_state');
if (!saved) return;
const { time, content } = saved;
return { time, content };
}
clearHistory() {
this.getHistoryKeys().forEach(key => this.storage.delete(key));
}
}
export function createHistory(deps: { storage: Storage }) {
return new History(deps.storage);
}

View file

@ -17,21 +17,6 @@
* under the License.
*/
import uiRoutes from 'ui/routes';
import template from './index.html';
require('brace');
require('ui/autoload/styles');
require('ui/capabilities/route_setup');
require('./src/controllers/sense_controller');
require('./src/directives/sense_history');
require('./src/directives/console_menu_directive');
uiRoutes.when('/dev_tools/console', {
requireUICapability: 'dev_tools.show',
controller: 'SenseController',
template,
});
export { createHistory, History } from './history';
export { createStorage, Storage, StorageKeys } from './storage';
export { createSettings, Settings, DevToolsSettings } from './settings';

View file

@ -0,0 +1,142 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Storage } from './index';
export interface DevToolsSettings {
fontSize: number;
wrapMode: boolean;
autocomplete: {
fields: boolean;
indices: boolean;
templates: boolean;
};
polling: boolean;
tripleQuotes: boolean;
}
export class Settings {
private input: any | null = null;
private output: any | null = null;
constructor(private readonly storage: Storage) {}
/**
* TODO: Slight hackiness going on here - late registration of dependencies should be refactored
*/
registerInput(input: any) {
this.input = input;
}
/**
* TODO: Slight hackiness going on here - late registration of dependencies should be refactored
*/
registerOutput(output: any) {
this.output = output;
}
getFontSize() {
return this.storage.get('font_size', 14);
}
setFontSize(size: any) {
this.storage.set('font_size', size);
this.applyCurrentSettings();
return true;
}
getWrapMode() {
return this.storage.get('wrap_mode', true);
}
setWrapMode(mode: any) {
this.storage.set('wrap_mode', mode);
this.applyCurrentSettings();
return true;
}
setTripleQuotes(tripleQuotes: any) {
this.storage.set('triple_quotes', tripleQuotes);
return true;
}
getTripleQuotes() {
return this.storage.get('triple_quotes', true);
}
getAutocomplete() {
return this.storage.get('autocomplete_settings', {
fields: true,
indices: true,
templates: true,
});
}
setAutocomplete(settings: any) {
this.storage.set('autocomplete_settings', settings);
return true;
}
getPolling() {
return this.storage.get('console_polling', true);
}
setPolling(polling: any) {
this.storage.set('console_polling', polling);
this.applyCurrentSettings();
return true;
}
applyCurrentSettings(editor?: any) {
if (typeof editor === 'undefined') {
if (this.input) this.applyCurrentSettings(this.input);
if (this.output) this.applyCurrentSettings(this.output);
} else if (editor) {
editor.getSession().setUseWrapMode(this.getWrapMode());
editor.$el.css('font-size', this.getFontSize() + 'px');
}
}
getCurrentSettings(): DevToolsSettings {
return {
autocomplete: this.getAutocomplete(),
wrapMode: this.getWrapMode(),
tripleQuotes: this.getTripleQuotes(),
fontSize: parseFloat(this.getFontSize()),
polling: Boolean(this.getPolling()),
};
}
updateSettings({ fontSize, wrapMode, tripleQuotes, autocomplete, polling }: any) {
this.setFontSize(fontSize);
this.setWrapMode(wrapMode);
this.setTripleQuotes(tripleQuotes);
this.setAutocomplete(autocomplete);
this.setPolling(polling);
this.input.focus();
return this.getCurrentSettings();
}
}
interface Deps {
storage: Storage;
}
export function createSettings({ storage }: Deps) {
return new Settings(storage);
}

View file

@ -17,44 +17,47 @@
* under the License.
*/
const { transform, keys, startsWith } = require('lodash');
import { transform, keys, startsWith } from 'lodash';
class Storage {
constructor(engine, prefix) {
this.engine = engine;
this.prefix = prefix;
}
type IStorageEngine = typeof window.localStorage;
encode(val) {
export enum StorageKeys {
WIDTH = 'widths',
}
export class Storage {
constructor(private readonly engine: IStorageEngine, private readonly prefix: string) {}
encode(val: any) {
return JSON.stringify(val);
}
decode(val) {
decode(val: any) {
if (typeof val === 'string') {
return JSON.parse(val);
}
}
encodeKey(key) {
encodeKey(key: string) {
return `${this.prefix}${key}`;
}
decodeKey(key) {
decodeKey(key: string) {
if (startsWith(key, this.prefix)) {
return `${key.slice(this.prefix.length)}`;
}
}
set(key, val) {
set(key: string, val: any) {
this.engine.setItem(this.encodeKey(key), this.encode(val));
return val;
}
has(key) {
has(key: string) {
return this.engine.getItem(this.encodeKey(key)) != null;
}
get(key, _default) {
get<T>(key: string, _default?: T) {
if (this.has(key)) {
return this.decode(this.engine.getItem(this.encodeKey(key)));
} else {
@ -62,11 +65,11 @@ class Storage {
}
}
delete(key) {
delete(key: string) {
return this.engine.removeItem(this.encodeKey(key));
}
keys() {
keys(): string[] {
return transform(keys(this.engine), (ours, key) => {
const ourKey = this.decodeKey(key);
if (ourKey != null) ours.push(ourKey);
@ -74,6 +77,6 @@ class Storage {
}
}
const instance = new Storage(window.localStorage, 'sense:');
export default instance;
export function createStorage(deps: { engine: IStorageEngine; prefix: string }) {
return new Storage(deps.engine, deps.prefix);
}

View file

@ -0,0 +1,21 @@
## New Platform (NP) Ready vs Quarantined
We want to move toward more modularised code in the Console app.
Part of the effort means separating out different console components
like:
- The language parser
- The editor rendering component
- Autocomplete
- The UI container components
In addition to this effort we want to bring Console in line with NP
requirements and ensure that we are not using angular and public ui
in this app anymore.
The quarantined folder contains all of the code that has not been cleared
for living in the new platform as it has not been properly refactored
or has dependencies on, for example, UI public.
Over time, the quarantined part of the code should shrink to nothing
and we should only have NP ready code.

View file

@ -1,42 +0,0 @@
<kbn-top-nav
app-name="'console'"
config="topNavMenu"
></kbn-top-nav>
<kbn-dev-tools-app data-test-subj="console">
<sense-history ng-show="showHistory" is-shown="showHistory" close="closeHistory()" history-dirty="lastRequestTimestamp"></sense-history>
<div class="conApp">
<div class="conApp__editor">
<ul class="conApp__autoComplete" id="autocomplete"></ul>
<div class="conApp__editorActions" id="ConAppEditorActions">
<kbn-tooltip text="{{:: 'console.sendRequestButtonTooltip' | i18n: { defaultMessage: 'click to send request' } }}">
<button
class="conApp__editorActionButton conApp__editorActionButton--success"
ng-click="sendSelected()"
data-test-subj="send-request-button">
<i class="fa fa-play"></i>
</button>
</kbn-tooltip>
<console-menu
auto-indent="autoIndent"
get-documentation="getDocumentation"
open-documentation="openDocumentation"
get-curl="getRequestsAsCURL"
>
</console-menu>
</div>
<div class="conApp__editorContent" id="ConAppEditor" data-test-subj="request-editor">GET _search
{
"query": { "match_all": {} }
}</div>
</div>
<div class="conApp__resizer" id="ConAppResizer">&#xFE19;</div>
<div class="conApp__output" data-test-subj="response-editor">
<div class="conApp__outputContent" id="ConAppOutput">{}</div>
</div>
</div>
</kbn-dev-tools-app>
<div id="consoleWelcomePanel"></div>
<div id="consoleHelpPanel"></div>
<div id="consoleSettingsModal"></div>

View file

@ -1,19 +1,26 @@
// TODO: Move all of the styles here (should be modularised by, e.g., CSS-in-JS or CSS modules).
#consoleRoot {
height: 100%;
// Make sure the editor actions don't create scrollbars on this container
// SASSTODO: Uncomment when tooltips are EUI-ified (inside portals)
overflow: hidden;
}
.consoleContainer {
padding: $euiSizeS;
}
.conApp {
display: flex;
flex: 1 1 auto;
position: relative;
padding: $euiSizeS;
// Make sure the editor actions don't create scrollbars on this container
// SASSTODO: Uncomment when tooltips are EUI-ified (inside portals)
// overflow: hidden;
}
.conApp__editor {
// Default size of left side is half the large breakpoint
// but this is inline overridden by the resizer
width: map-get($euiBreakpoints, "l") / 2;
width: 100%;
display: flex;
flex: 0 0 auto;
// Required on IE11 to render ace editor correctly after first input.
position: relative;
}
@ -24,6 +31,7 @@
.conApp__editorContent,
.conApp__outputContent {
height: 100%;
flex: 1 1 1px;
}

View file

@ -24,7 +24,7 @@ DevToolsRegistryProvider.register(() => ({
order: 1,
name: 'console',
display: i18n.translate('console.consoleDisplayName', {
defaultMessage: 'Console'
defaultMessage: 'Console',
}),
url: '#/dev_tools/console'
url: '#/dev_tools/console',
}));

View file

@ -0,0 +1,3 @@
<kbn-dev-tools-app data-test-subj="console">
<div id="consoleRoot" data-ng-init="initReactApp()"></div>
</kbn-dev-tools-app>

View file

@ -20,10 +20,11 @@
import sinon from 'sinon';
import $ from 'jquery';
import history from '../history';
import mappings from '../mappings';
import init from '../app';
const history = { getSavedEditorState() {}, };
describe('console app initialization', () => {
const sandbox = sinon.createSandbox();
@ -33,7 +34,6 @@ describe('console app initialization', () => {
beforeEach(() => {
ajaxDoneStub = sinon.stub();
sandbox.stub($, 'ajax').returns({ done: ajaxDoneStub });
sandbox.stub(history, 'getSavedEditorState');
sandbox.stub(mappings, 'retrieveAutoCompleteInfo');
inputMock = {
@ -41,11 +41,11 @@ describe('console app initialization', () => {
moveToNextRequestEdge: sinon.stub(),
highlightCurrentRequestsAndUpdateActionBar: sinon.stub(),
updateActionsBar: sinon.stub(),
getSession: sinon.stub().returns({ on() {} })
getSession: sinon.stub().returns({ on() {} }),
};
outputMock = {
update: sinon.stub()
update: sinon.stub(),
};
});
@ -57,13 +57,13 @@ describe('console app initialization', () => {
const mockContent = {};
ajaxDoneStub.yields(mockContent);
init(inputMock, outputMock, 'https://state.link.com/content');
init(inputMock, outputMock, history, 'https://state.link.com/content');
sinon.assert.calledOnce($.ajax);
sinon.assert.calledWithExactly($.ajax, {
url: 'https://state.link.com/content',
dataType: 'text',
kbnXsrfToken: false
kbnXsrfToken: false,
});
sinon.assert.calledTwice(inputMock.moveToNextRequestEdge);
@ -81,14 +81,14 @@ describe('console app initialization', () => {
const mockContent = {};
ajaxDoneStub.yields(mockContent);
init(inputMock, outputMock, 'https://api.github.com/content');
init(inputMock, outputMock, history, 'https://api.github.com/content');
sinon.assert.calledOnce($.ajax);
sinon.assert.calledWithExactly($.ajax, {
url: 'https://api.github.com/content',
dataType: 'text',
kbnXsrfToken: false,
headers: { Accept: 'application/vnd.github.v3.raw' }
headers: { Accept: 'application/vnd.github.v3.raw' },
});
sinon.assert.calledTwice(inputMock.moveToNextRequestEdge);

View file

@ -18,18 +18,21 @@
*/
const $ = require('jquery');
const history = require('./history');
const mappings = require('./mappings');
export default function init(input, output, sourceLocation = 'stored') {
const DEFAULT_INPUT_VALUE = `GET _search
{
"query": {
"match_all": {}
}
}`;
export default function init(input, output, history, sourceLocation = 'stored') {
$(document.body).removeClass('fouc');
// set the value of the input and clear the output
function resetToValues(content) {
if (content != null) {
input.update(content);
}
input.update(content != null ? content : DEFAULT_INPUT_VALUE);
output.update('');
}
@ -49,8 +52,7 @@ export default function init(input, output, sourceLocation = 'stored') {
try {
const content = input.getValue();
history.updateCurrentState(content);
}
catch (e) {
} catch (e) {
console.log('Ignoring saving error: ' + e);
}
}
@ -60,76 +62,34 @@ export default function init(input, output, sourceLocation = 'stored') {
if (sourceLocation === 'stored') {
if (previousSaveState) {
resetToValues(previousSaveState.content);
}
else {
} else {
resetToValues();
input.autoIndent();
}
}
else if (/^https?:\/\//.test(sourceLocation)) {
} else if (/^https?:\/\//.test(sourceLocation)) {
const loadFrom = {
url: sourceLocation,
// 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
kbnXsrfToken: false,
};
if (/https?:\/\/api.github.com/.test(sourceLocation)) {
loadFrom.headers = { Accept: 'application/vnd.github.v3.raw' };
}
$.ajax(loadFrom).done((data) => {
$.ajax(loadFrom).done(data => {
resetToValues(data);
input.moveToNextRequestEdge(true);
input.highlightCurrentRequestsAndUpdateActionBar();
input.updateActionsBar();
});
}
else {
} else {
resetToValues();
}
input.moveToNextRequestEdge(true);
}
// stupid simple restore function, called when the user
// chooses to restore a request from the history
// PREVENTS history from needing to know about the input
history.restoreFromHistory = function applyHistoryElem(req) {
const session = input.getSession();
let pos = input.getCursorPosition();
let prefix = '';
let suffix = '\n';
if (input.parser.isStartRequestRow(pos.row)) {
pos.column = 0;
suffix += '\n';
}
else if (input.parser.isEndRequestRow(pos.row)) {
const line = session.getLine(pos.row);
pos.column = line.length;
prefix = '\n\n';
}
else if (input.parser.isInBetweenRequestsRow(pos.row)) {
pos.column = 0;
}
else {
pos = input.nextRequestEnd(pos);
prefix = '\n\n';
}
let s = prefix + req.method + ' ' + req.endpoint;
if (req.data) {
s += '\n' + req.data;
}
s += suffix;
session.insert(pos, s);
input.clearSelection();
input.moveCursorTo(pos.row + prefix.length, 0);
input.focus();
};
setupAutosave();
loadSavedState();
mappings.retrieveAutoCompleteInfo();

View file

@ -28,7 +28,6 @@ import { populateContext } from './autocomplete/engine';
import { URL_PATH_END_MARKER } from './autocomplete/components';
import _ from 'lodash';
import ace from 'brace';
import 'brace/ext/language_tools';
import { i18n } from '@kbn/i18n';
const AceRange = ace.acequire('ace/range').Range;
@ -914,30 +913,34 @@ export default function (editor) {
callback(null, []);
}
else {
const terms = _.map(context.autoCompleteSet, function (term) {
if (typeof term !== 'object') {
term = {
name: term
const terms = _.map(
context
.autoCompleteSet
.filter(term => Boolean(term) && term.name != null),
function (term) {
if (typeof term !== 'object') {
term = {
name: term
};
} else {
term = _.clone(term);
}
const defaults = {
value: term.name,
meta: 'API',
score: 0,
context: context,
};
} else {
term = _.clone(term);
}
const defaults = {
value: term.name,
meta: 'API',
score: 0,
context: context,
};
// we only need out custom insertMatch behavior for the body
if (context.autoCompleteType === 'body') {
defaults.completer = {
insertMatch: function () {
return applyTerm(term);
}
};
}
return _.defaults(term, defaults);
});
// we only need out custom insertMatch behavior for the body
if (context.autoCompleteType === 'body') {
defaults.completer = {
insertMatch: function () {
return applyTerm(term);
}
};
}
return _.defaults(term, defaults);
});
terms.sort(function (t1, t2) {
/* score sorts from high to low */

View file

@ -8,27 +8,22 @@
.conHistory__body {
display: flex;
height: $euiSizeXL * 10;
> ul {
margin-bottom: 0;
}
}
.conHistory__footer {
display: flex;
justify-content: space-between;
padding-top: $euiSize;
}
.conHistory__footerButtonsRight {
text-align: right;
.conHistory__body__spacer {
flex: 0 0 1%;
}
.conHistory__reqs,
.conHistory__viewer {
flex: 0 0 50%;
flex: 0 0 49.5%;
}
.conHistory__reqs {
overflow: auto;
margin-right: $euiSizeL;
}
.conHistory__req {

View file

@ -17,81 +17,49 @@
* under the License.
*/
require('brace');
require('brace/ext/searchbox');
import Autocomplete from './autocomplete';
import Autocomplete from './autocomplete';
import mappings from './mappings';
const SenseEditor = require('./sense_editor/editor');
const settings = require('./settings');
const utils = require('./utils');
const es = require('./es');
const history = require('./history');
import { uiModules } from 'ui/modules';
let input;
export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocumentation = () => {}) {
export function initializeInput($el, $actionsEl, history, settings) {
input = new SenseEditor($el);
// this may not exist if running from tests
if (uiModules) {
const appSense = uiModules.get('app/sense');
if (appSense && appSense.setupResizeCheckerForRootEditors) {
appSense.setupResizeCheckerForRootEditors($el, input, output);
}
}
input.autocomplete = new Autocomplete(input);
input.$actions = $actionsEl;
input.commands.addCommand({
name: 'auto indent request',
bindKey: { win: 'Ctrl-I', mac: 'Command-I' },
exec: function () {
input.autoIndent();
}
});
input.commands.addCommand({
name: 'move to previous request start or end',
bindKey: { win: 'Ctrl-Up', mac: 'Command-Up' },
exec: function () {
input.moveToPreviousRequestEdge();
}
});
input.commands.addCommand({
name: 'move to next request start or end',
bindKey: { win: 'Ctrl-Down', mac: 'Command-Down' },
exec: function () {
input.moveToNextRequestEdge();
}
});
/**
* Setup the "send" shortcut
*/
let CURRENT_REQ_ID = 0;
function sendCurrentRequestToES(addedToHistoryCb) {
function sendCurrentRequestToES(addedToHistoryCb, output) {
const reqId = ++CURRENT_REQ_ID;
input.getRequestsInRange(function (requests) {
input.getRequestsInRange(requests => {
if (reqId !== CURRENT_REQ_ID) {
return;
}
output.update('');
if (output) {
output.update('');
}
if (requests.length === 0) {
return;
}
const isMultiRequest = requests.length > 1;
const finishChain = function () { /* noop */ };
const finishChain = () => {
/* noop */
};
let isFirstRequest = true;
const sendNextRequest = function () {
const sendNextRequest = () => {
if (reqId !== CURRENT_REQ_ID) {
return;
}
@ -107,7 +75,7 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu
esData += '\n';
} //append a new line for bulk requests.
es.send(esMethod, esPath, esData).always(function (dataOrjqXHR, textStatus, jqXhrORerrorThrown) {
es.send(esMethod, esPath, esData).always((dataOrjqXHR, textStatus, jqXhrORerrorThrown) => {
if (reqId !== CURRENT_REQ_ID) {
return;
}
@ -117,14 +85,14 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu
function modeForContentType(contentType) {
if (contentType.indexOf('text/plain') >= 0) {
return 'ace/mode/text';
}
else if (contentType.indexOf('application/yaml') >= 0) {
} else if (contentType.indexOf('application/yaml') >= 0) {
return 'ace/mode/yaml';
}
return null;
}
const isSuccess = typeof xhr.status === 'number' &&
const isSuccess =
typeof xhr.status === 'number' &&
// Things like DELETE index where the index is not there are OK.
((xhr.status >= 200 && xhr.status < 300) || xhr.status === 404);
@ -151,8 +119,7 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu
// assume json - auto pretty
try {
value = utils.expandLiteralStrings(value);
}
catch (e) {
} catch (e) {
// nothing to do here
}
}
@ -166,12 +133,15 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu
if (isMultiRequest) {
value = '# ' + req.method + ' ' + req.url + '\n' + value;
}
if (isFirstRequest) {
output.update(value, mode);
}
else {
output.append('\n' + value);
if (output) {
if (isFirstRequest) {
output.update(value, mode);
} else {
output.append('\n' + value);
}
}
isFirstRequest = false;
// single request terminate via sendNextRequest as well
sendNextRequest();
@ -184,8 +154,7 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu
if (value[0] === '{') {
try {
value = JSON.stringify(JSON.parse(value), null, 2);
}
catch (e) {
} catch (e) {
// nothing to do here
}
}
@ -196,11 +165,12 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu
if (isMultiRequest) {
value = '# ' + req.method + ' ' + req.url + '\n' + value;
}
if (isFirstRequest) {
output.update(value, mode);
}
else {
output.append('\n' + value);
if (output) {
if (isFirstRequest) {
output.update(value, mode);
} else {
output.append('\n' + value);
}
}
finishChain();
}
@ -211,20 +181,6 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu
});
}
input.commands.addCommand({
name: 'send to elasticsearch',
bindKey: { win: 'Ctrl-Enter', mac: 'Command-Enter' },
exec: () => sendCurrentRequestToES()
});
input.commands.addCommand({
name: 'open documentation',
bindKey: { win: 'Ctrl-/', mac: 'Command-/' },
exec: () => {
openDocumentation();
}
});
/**
* Init the editor
*/
@ -235,7 +191,6 @@ export function initializeInput($el, $actionsEl, $copyAsCurlEl, output, openDocu
input.highlightCurrentRequestsAndUpdateActionBar();
input.sendCurrentRequestToES = sendCurrentRequestToES;
require('./input_resize')(input, output);
return input;
}

View file

@ -17,10 +17,11 @@
* under the License.
*/
import { legacyBackDoorToSettings } from '../../../np_ready/public/application';
const $ = require('jquery');
const _ = require('lodash');
const es = require('./es');
const settings = require('./settings');
// NOTE: If this value ever changes to be a few seconds or less, it might introduce flakiness
// due to timing issues in our app.js tests.
@ -257,7 +258,7 @@ function clear() {
}
function retrieveSettings(settingsKey, settingsToRetrieve) {
const currentSettings = settings.getAutocomplete();
const currentSettings = legacyBackDoorToSettings().getAutocomplete();
const settingKeyToPathMap = {
fields: '_mapping',
indices: '_aliases',
@ -289,7 +290,7 @@ function retrieveSettings(settingsKey, settingsToRetrieve) {
// unchanged alone (both selected and unselected).
// 3. Poll: Use saved. Fetch selected. Ignore unselected.
function retrieveAutoCompleteInfo(settingsToRetrieve = settings.getAutocomplete()) {
function retrieveAutoCompleteInfo(settingsToRetrieve = legacyBackDoorToSettings().getAutocomplete()) {
if (pollTimeoutId) {
clearTimeout(pollTimeoutId);
}
@ -329,7 +330,7 @@ function retrieveAutoCompleteInfo(settingsToRetrieve = settings.getAutocomplete(
pollTimeoutId = setTimeout(() => {
// This looks strange/inefficient, but it ensures correct behavior because we don't want to send
// a scheduled request if the user turns off polling.
if (settings.getPolling()) {
if (legacyBackDoorToSettings().getPolling()) {
retrieveAutoCompleteInfo();
}
}, POLL_INTERVAL);

View file

@ -19,19 +19,18 @@
import _ from 'lodash';
const ace = require('brace');
const settings = require('./settings');
const OutputMode = require('./sense_editor/mode/output');
const smartResize = require('./smart_resize');
let output;
export function initializeOutput($el) {
export function initializeOutput($el, settings) {
output = ace.acequire('ace/ace').edit($el[0]);
const outputMode = new OutputMode.Mode();
output.$blockScrolling = Infinity;
output.resize = smartResize(output);
output.update = function (val, mode, cb) {
output.update = (val, mode, cb) => {
if (typeof mode === 'function') {
cb = mode;
mode = void 0;
@ -39,14 +38,14 @@ export function initializeOutput($el) {
const session = output.getSession();
session.setMode(val ? (mode || outputMode) : 'ace/mode/text');
session.setMode(val ? mode || outputMode : 'ace/mode/text');
session.setValue(val);
if (typeof cb === 'function') {
setTimeout(cb);
}
};
output.append = function (val, foldPrevious, cb) {
output.append = (val, foldPrevious, cb) => {
if (typeof foldPrevious === 'function') {
cb = foldPrevious;
foldPrevious = true;
@ -68,12 +67,13 @@ export function initializeOutput($el) {
output.$el = $el;
(function (session) {
// eslint-disable-next-line
(function setupSession(session) {
session.setMode('ace/mode/text');
session.setFoldStyle('markbeginend');
session.setTabSize(2);
session.setUseWrapMode(true);
}(output.getSession()));
})(output.getSession());
output.setShowPrintMargin(false);
output.setReadOnly(true);

View file

@ -17,7 +17,7 @@
* under the License.
*/
const ace = require('brace');
import ace from 'brace';
require('./output_highlight_rules');

View file

@ -21,8 +21,8 @@ import { get, throttle } from 'lodash';
export default function (editor) {
const resize = editor.resize;
const throttledResize = throttle(() => {
const throttledResize = throttle(() => {
resize.call(editor);
// Keep current top line in view when resizing to avoid losing user context

Some files were not shown because too many files have changed in this diff Show more