Console to NP ready (#43346) (#45511)

*  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 18:30:03 +02:00 committed by GitHub
parent 37868533df
commit c1da167c1e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
292 changed files with 2816 additions and 1261 deletions

View file

@ -0,0 +1,334 @@
- Start Date: 2019-05-10
- RFC PR: (leave this empty)
- Kibana Issue: (leave this empty)
# Summary
A front-end service to manage registration and root-level routing for
first-class applications.
# Basic example
```tsx
// my_plugin/public/application.js
import React from 'react';
import ReactDOM from 'react-dom';
import { MyApp } from './componnets';
export function renderApp(context, { element }) {
ReactDOM.render(
<MyApp mountContext={context} deps={pluginStart} />,
element
);
return () => {
ReactDOM.unmountComponentAtNode(element);
};
}
```
```tsx
// my_plugin/public/plugin.js
class MyPlugin {
setup({ application }) {
application.register({
id: 'my-app',
title: 'My Application',
async mount(context, params) {
const { renderApp } = await import('./application');
return renderApp(context, params);
}
});
}
}
```
# Motivation
By having centralized management of applications we can have a true single page
application. It also gives us a single place to enforce authorization and/or
licensing constraints on application access.
By making the mounting interface of the ApplicationService generic, we can
support many different rendering technologies simultaneously to avoid framework
lock-in.
# Detailed design
## Interface
```ts
/** A context type that implements the Handler Context pattern from RFC-0003 */
export interface AppMountContext {
/** These services serve as an example, but are subject to change. */
core: {
http: {
fetch(...): Promise<any>;
};
i18n: {
translate(
id: string,
defaultMessage: string,
values?: Record<string, string>
): string;
};
notifications: {
toasts: {
add(...): void;
};
};
overlays: {
showFlyout(render: (domElement) => () => void): Flyout;
showModal(render: (domElement) => () => void): Modal;
};
uiSettings: { ... };
};
/** Other plugins can inject context by registering additional context providers */
[contextName: string]: unknown;
}
export interface AppMountParams {
/** The base path the application is mounted on. Used to configure routers. */
appBasePath: string;
/** The element the application should render into */
element: HTMLElement;
}
export type Unmount = () => Promise<void> | void;
export interface AppSpec {
/**
* A unique identifier for this application. Used to build the route for this
* application in the browser.
*/
id: string;
/**
* The title of the application.
*/
title: string;
/**
* A mount function called when the user navigates to this app's route.
* @param context the `AppMountContext` generated for this app
* @param params the `AppMountParams`
* @returns An unmounting function that will be called to unmount the application.
*/
mount(context: MountContext, params: AppMountParams): Unmount | Promise<Unmount>;
/**
* A EUI iconType that will be used for the app's icon. This icon
* takes precendence over the `icon` property.
*/
euiIconType?: string;
/**
* A URL to an image file used as an icon. Used as a fallback
* if `euiIconType` is not provided.
*/
icon?: string;
/**
* Custom capabilities defined by the app.
*/
capabilities?: Partial<Capabilities>;
}
export interface ApplicationSetup {
/**
* Registers an application with the system.
*/
register(app: AppSpec): void;
registerMountContext<T extends keyof MountContext>(
contextName: T,
provider: (context: Partial<MountContext>) => MountContext[T] | Promise<MountContext[T]>
): void;
}
export interface ApplicationStart {
/**
* The UI capabilities for the current user.
*/
capabilities: Capabilties;
}
```
## Mounting
When an app is registered via `register`, it must provide a `mount` function
that will be invoked whenever the window's location has changed from another app
to this app.
This function is called with a `AppMountContext` and an
`AppMountParams` which contains a `HTMLElement` for the application to
render itself to. The mount function must also return a function that can be
called by the ApplicationService to unmount the application at the given DOM
Element. The mount function may return a Promise of an unmount function in order
to import UI code dynamically.
The ApplicationService's `register` method will only be available during the
*setup* lifecycle event. This allows the system to know when all applications
have been registered.
The `mount` function will also get access to the `AppMountContext` that
has many of the same core services available during the `start` lifecycle.
Plugins can also register additional context attributes via the
`registerMountContext` function.
## Routing
The ApplicationService will serve as the global frontend router for Kibana,
enabling Kibana to be a 100% single page application. However, the router will
only manage top-level routes. Applications themselves will need to implement
their own routing as subroutes of the top-level route.
An example:
- "MyApp" is registered with `id: 'my-app'`
- User navigates from mykibana.com/app/home to mykibana.com/app/my-app
- ApplicationService sees the root app has changed and mounts the new
application:
- Calls the `Unmount` function returned my "Home"'s `mount`
- Calls the `mount` function registered by "MyApp"
- MyApp's internal router takes over rest of routing. Redirects to initial
"overview" page: mykibana.com/app/my-app/overview
When setting up a router, your application should only handle the part of the
URL following the `params.appBasePath` provided when you application is mounted.
### Legacy Applications
In order to introduce this service now, the ApplicationService will need to be
able to handle "routing" to legacy applications. We will not be able to run
multiple legacy applications on the same page load due to shared stateful
modules in `ui/public`.
Instead, the ApplicationService should do a full-page refresh when rendering
legacy applications. Internally, this will be managed by registering legacy apps
with the ApplicationService separately and handling those top-level routes by
starting a full-page refresh rather than a mounting cycle.
## Complete Example
Here is a complete example that demonstrates rendering a React application with
a full-featured router and code-splitting. Note that using React or any other
3rd party tools featured here is not required to build a Kibana Application.
```tsx
// my_plugin/public/application.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import loadable from '@loadable/component';
// Apps can choose to load components statically in the same bundle or
// dynamically when routes are rendered.
import { HomePage } from './pages';
const LazyDashboard = loadable(() => import('./pages/dashboard'));
const MyApp = ({ basename }) => (
// Setup router's basename from the basename provided from MountContext
<BrowserRouter basename={basename}>
{/* mykibana.com/app/my-app/ */}
<Route path="/" exact component={HomePage} />
{/* mykibana.com/app/my-app/dashboard/42 */}
<Route
path="/dashboard/:id"
render={({ match }) => <LazyDashboard dashboardId={match.params.id} />}
/>
</BrowserRouter>,
);
export function renderApp(context, params) {
ReactDOM.render(
// `params.appBasePath` would be `/app/my-app` in this example.
// This exact string is not guaranteed to be stable, always reference the
// provided value at `params.appBasePath`.
<MyApp basename={params.appBasePath} />,
params.element
);
return () => ReactDOM.unmountComponentAtNode(params.element);
}
```
```tsx
// my_plugin/public/plugin.tsx
export class MyPlugin {
setup({ application }) {
application.register({
id: 'my-app',
async mount(context, params) {
const { renderApp } = await import('./application');
return renderApp(context, params);
}
});
}
}
```
## Core Entry Point
Once we can support application routing for new and legacy applications, we
should create a new entry point bundle that only includes Core and any necessary
uiExports (hacks for example). This should be served by the backend whenever a
`/app/<app-id>` request is received for an app that the legacy platform does not
have a bundle for.
# Drawbacks
- Implementing this will be significant work and requires migrating legacy code
from `ui/chrome`
- Making Kibana a single page application may lead to problems if applications
do not clean themselves up properly when unmounted
- Application `mount` functions will have access to *setup* via the closure. We
may want to lock down these APIs from being used after *setup* to encourage
usage of the `MountContext` instead.
- In order to support new applications being registered in the legacy platform,
we will need to create a new `uiExport` that is imported during the new
platform's *setup* lifecycle event. This is necessary because app registration
must happen prior to starting the legacy platform. This is only an issue for
plugins that are migrating using a shim in the legacy platform.
# Alternatives
- We could provide a full featured react-router instance that plugins could
plug directly into. The downside is this locks us more into React and makes
code splitting a bit more challenging.
# Adoption strategy
Adoption of the application service will have to happen as part of the migration
of each plugin. We should be able to support legacy plugins registering new
platform-style applications before they actually move all of their code
over to the new platform.
# How we teach this
Introducing this service makes applications a first-class feature of the Kibana
platform. Right now, plugins manage their own routes and can export "navlinks"
that get rendered in the navigation UI, however there is a not a self-contained
concept like an application to encapsulate these related responsibilities. It
will need to be emphasized that plugins can register zero, one, or multiple
applications.
Most new and existing Kibana developers will need to understand how the
ApplicationService works and how multiple apps run in a single page application.
This should be accomplished through thorough documentation in the
ApplicationService's API implementation as well as in general plugin development
tutorials and documentation.
# Unresolved questions
- Are there any major caveats to having multiple routers on the page? If so, how
can these be prevented or worked around?
- How should global URL state be shared across applications, such as timepicker
state?

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