Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create Painless Lab app #57538

Merged
merged 30 commits into from Mar 23, 2020
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
530b409
Create Painless Playground app (#54578)
kertal Feb 13, 2020
455ce24
Replace heart script with smiley face script. (#57755)
cjcenizal Feb 19, 2020
b8bd2e6
Rename Painless Playground -> Painless Lab. (#57545)
cjcenizal Feb 20, 2020
e683bc6
Fix i18n namespace.
cjcenizal Feb 20, 2020
74cd1c6
Improve smiley face proportions.
cjcenizal Feb 20, 2020
35de7e0
[Painless Lab] Minor Fixes (#58135)
jloleysens Feb 24, 2020
6689519
Merge branch 'master' into app/painless
elasticmachine Mar 2, 2020
d33a82b
[Painless Lab] NP migration (#59794)
alisonelizabeth Mar 13, 2020
b7840ff
Fix sample document editor.
cjcenizal Mar 14, 2020
07abc99
Merge branch 'master' into app/painless
elasticmachine Mar 14, 2020
3de6244
[Painless Lab] Fix float -> integer coercion bug (#60201)
cjcenizal Mar 17, 2020
903e5f9
Merge branch 'master' into app/painless
elasticmachine Mar 17, 2020
f89b489
Rename helpers lib to format. Add tests for formatRequestPayload.
cjcenizal Mar 17, 2020
f5bec8e
Add query parameter to score context (#60414)
jloleysens Mar 18, 2020
1027b8a
Fix i18n
jloleysens Mar 18, 2020
1966250
Merge branch 'master' into app/painless
elasticmachine Mar 18, 2020
bcdcbe1
Another i18n issue
jloleysens Mar 18, 2020
8733480
Merge branch 'app/painless' of github.com:elastic/kibana into app/pai…
jloleysens Mar 18, 2020
c206320
[Painless] Minor state update model refactor (#60532)
jloleysens Mar 19, 2020
470da3e
Fix i18n in context_tab
jloleysens Mar 19, 2020
4506714
i18n
jloleysens Mar 19, 2020
ec81bf6
[Painless] Language Service (#60612)
jloleysens Mar 20, 2020
ec762ab
[Painless] Replace hard-coded links (#60603)
jloleysens Mar 20, 2020
5533a7e
Merge branch 'master' into app/painless
elasticmachine Mar 20, 2020
e03801b
Remove responsive stacking from tabs with icons in them.
cjcenizal Mar 20, 2020
2a2080d
Merge branch 'master' into app/painless
elasticmachine Mar 23, 2020
093f524
Resize Painless Lab bottom bar to accommodate nav drawer width (#60833)
cjcenizal Mar 23, 2020
d0d3036
Validate Painless Lab index field (#60841)
cjcenizal Mar 23, 2020
e77eb94
Fix bottom bar z-index.
cjcenizal Mar 23, 2020
111f2f8
Position flyout help link so it's bottom-aligned with the title and f…
cjcenizal Mar 23, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Expand Up @@ -181,6 +181,7 @@
/x-pack/plugins/remote_clusters/ @elastic/es-ui
/x-pack/legacy/plugins/rollup/ @elastic/es-ui
/x-pack/plugins/searchprofiler/ @elastic/es-ui
/x-pack/plugins/painless_lab/ @elastic/es-ui
/x-pack/legacy/plugins/snapshot_restore/ @elastic/es-ui
/x-pack/legacy/plugins/upgrade_assistant/ @elastic/es-ui
/x-pack/plugins/upgrade_assistant/ @elastic/es-ui
Expand Down
2 changes: 2 additions & 0 deletions packages/kbn-ui-shared-deps/monaco.ts
Expand Up @@ -25,6 +25,8 @@ import 'monaco-editor/esm/vs/base/worker/defaultWorkerFactory';
import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands.js';
import 'monaco-editor/esm/vs/editor/browser/widget/codeEditorWidget.js';

import 'monaco-editor/esm/vs/editor/contrib/wordOperations/wordOperations.js'; // Needed for word-wise char navigation

import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController.js'; // Needed for suggestions
import 'monaco-editor/esm/vs/editor/contrib/hover/hover.js'; // Needed for hover
import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints.js'; // Needed for signature
Expand Down
1 change: 1 addition & 0 deletions x-pack/.i18nrc.json
Expand Up @@ -30,6 +30,7 @@
"xpack.ml": ["plugins/ml", "legacy/plugins/ml"],
"xpack.monitoring": "legacy/plugins/monitoring",
"xpack.remoteClusters": "plugins/remote_clusters",
"xpack.painlessLab": "plugins/painless_lab",
"xpack.reporting": ["plugins/reporting", "legacy/plugins/reporting"],
"xpack.rollupJobs": "legacy/plugins/rollup",
"xpack.searchProfiler": "plugins/searchprofiler",
Expand Down
15 changes: 15 additions & 0 deletions x-pack/plugins/painless_lab/common/constants.ts
@@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { LicenseType } from '../../licensing/common/types';

const basicLicense: LicenseType = 'basic';

export const PLUGIN = {
id: 'painlessLab',
minimumLicenseType: basicLicense,
};

export const API_BASE_PATH = '/api/painless_lab';
16 changes: 16 additions & 0 deletions x-pack/plugins/painless_lab/kibana.json
@@ -0,0 +1,16 @@
{
"id": "painlessLab",
"version": "8.0.0",
"kibanaVersion": "kibana",
"requiredPlugins": [
"devTools",
"licensing",
"home"
],
"configPath": [
"xpack",
"painless_lab"
],
"server": true,
"ui": true
}
135 changes: 135 additions & 0 deletions x-pack/plugins/painless_lab/public/application/common/constants.tsx
@@ -0,0 +1,135 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React from 'react';

import { EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

const defaultLabel = i18n.translate('xpack.painlessLab.contextDefaultLabel', {
defaultMessage: 'Basic',
});

const filterLabel = i18n.translate('xpack.painlessLab.contextFilterLabel', {
defaultMessage: 'Filter',
});

const scoreLabel = i18n.translate('xpack.painlessLab.contextScoreLabel', {
defaultMessage: 'Score',
});

export const painlessContextOptions = [
{
value: 'painless_test',
inputDisplay: defaultLabel,
dropdownDisplay: (
<>
<strong>{defaultLabel}</strong>
<EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">
{i18n.translate('xpack.painlessLab.context.defaultLabel', {
defaultMessage: 'The script result will be converted to a string',
})}
</p>
</EuiText>
</>
),
},
{
value: 'filter',
inputDisplay: filterLabel,
dropdownDisplay: (
<>
<strong>{filterLabel}</strong>
<EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">
{i18n.translate('xpack.painlessLab.context.filterLabel', {
defaultMessage: "Use the context of a filter's script query",
})}
</p>
</EuiText>
</>
),
},
{
value: 'score',
inputDisplay: scoreLabel,
dropdownDisplay: (
<>
<strong>{scoreLabel}</strong>
<EuiText size="s" color="subdued">
<p className="euiTextColor--subdued">
{i18n.translate('xpack.painlessLab.context.scoreLabel', {
defaultMessage: 'Use the context of a script_score function in function_score query',
})}
</p>
</EuiText>
</>
),
},
];

// Render a smiley face as an example.
export const exampleScript = `boolean isInCircle(def posX, def posY, def circleX, def circleY, def radius) {
double distanceFromCircleCenter = Math.sqrt(Math.pow(circleX - posX, 2) + Math.pow(circleY - posY, 2));
return distanceFromCircleCenter <= radius;
}

boolean isOnCircle(def posX, def posY, def circleX, def circleY, def radius, def thickness, def squashY) {
double distanceFromCircleCenter = Math.sqrt(Math.pow(circleX - posX, 2) + Math.pow((circleY - posY) / squashY, 2));
return (
distanceFromCircleCenter >= radius - thickness
&& distanceFromCircleCenter <= radius + thickness
);
}

def result = '';
int charCount = 0;

// Canvas dimensions
int width = 31;
int height = 31;
double halfWidth = Math.floor(width * 0.5);
double halfHeight = Math.floor(height * 0.5);

// Style constants
double strokeWidth = 0.6;

// Smiley face configuration
int headSize = 13;
double headSquashY = 0.78;
int eyePositionX = 10;
int eyePositionY = 12;
int eyeSize = 1;
int mouthSize = 15;
int mouthPositionX = width / 2;
int mouthPositionY = 5;
int mouthOffsetY = 11;

for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
boolean isHead = isOnCircle(x, y, halfWidth, halfHeight, headSize, strokeWidth, headSquashY);
boolean isLeftEye = isInCircle(x, y, eyePositionX, eyePositionY, eyeSize);
boolean isRightEye = isInCircle(x, y, width - eyePositionX - 1, eyePositionY, eyeSize);
boolean isMouth = isOnCircle(x, y, mouthPositionX, mouthPositionY, mouthSize, strokeWidth, 1) && y > mouthPositionY + mouthOffsetY;

if (isLeftEye || isRightEye || isMouth || isHead) {
result += "*";
} else {
result += ".";
}

result += " ";

// Make sure the smiley face doesn't deform as the container changes width.
charCount++;
if (charCount % width === 0) {
result += "\\\\n";
}
}
}

return result;`;
37 changes: 37 additions & 0 deletions x-pack/plugins/painless_lab/public/application/common/types.ts
@@ -0,0 +1,37 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

// This should be an enumerated list
export type Context = string;

export enum PayloadFormat {
UGLY = 'ugly',
PRETTY = 'pretty',
}

export interface Response {
error?: ExecutionError | Error;
result?: string;
}

export type ExecutionErrorScriptStack = string[];

export interface ExecutionErrorPosition {
start: number;
end: number;
offset: number;
}

export interface ExecutionError {
script_stack?: ExecutionErrorScriptStack;
caused_by?: {
type: string;
reason: string;
};
message?: string;
position: ExecutionErrorPosition;
script: string;
}
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { CodeEditor } from '../../../../../../src/plugins/kibana_react/public';

interface Props {
code: string;
onChange: (code: string) => void;
}

export function Editor({ code, onChange }: Props) {
return (
<CodeEditor
languageId="painless"
// 99% width allows the editor to resize horizontally. 100% prevents it from resizing.
width="99%"
height="100%"
value={code}
onChange={onChange}
options={{
fontSize: 12,
minimap: {
enabled: false,
},
scrollBeyondLastLine: false,
wordWrap: 'on',
wrappingIndent: 'indent',
automaticLayout: true,
}}
/>
);
}
76 changes: 76 additions & 0 deletions x-pack/plugins/painless_lab/public/application/components/main.tsx
@@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useState, useEffect, FunctionComponent } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { formatRequestPayload, formatJson } from '../lib/format';
import { exampleScript } from '../common/constants';
import { PayloadFormat } from '../common/types';
import { useSubmitCode } from '../hooks';
import { OutputPane } from './output_pane';
import { MainControls } from './main_controls';
import { Editor } from './editor';
import { RequestFlyout } from './request_flyout';
import { useAppContext } from '../context';

export const Main: FunctionComponent = () => {
const { state, updateState, services, links } = useAppContext();

const [isRequestFlyoutOpen, setRequestFlyoutOpen] = useState(false);
const { inProgress, response, submit } = useSubmitCode(services.http);

// Live-update the output and persist state as the user changes it.
useEffect(() => {
submit(state);
}, [state, submit]);

const toggleRequestFlyout = () => {
setRequestFlyoutOpen(!isRequestFlyoutOpen);
};

return (
<div className="painlessLabMainContainer">
<EuiFlexGroup className="painlessLabPanelsContainer" responsive={false} gutterSize="none">
<EuiFlexItem>
<EuiTitle className="euiScreenReaderOnly">
<h1>
{i18n.translate('xpack.painlessLab.title', {
defaultMessage: 'Painless Lab',
})}
</h1>
</EuiTitle>

<Editor
code={state.code}
onChange={nextCode => updateState(() => ({ code: nextCode }))}
/>
</EuiFlexItem>

<EuiFlexItem>
<OutputPane isLoading={inProgress} response={response} />
</EuiFlexItem>
</EuiFlexGroup>

<MainControls
links={links}
isLoading={inProgress}
toggleRequestFlyout={toggleRequestFlyout}
isRequestFlyoutOpen={isRequestFlyoutOpen}
reset={() => updateState(() => ({ code: exampleScript }))}
/>

{isRequestFlyoutOpen && (
<RequestFlyout
links={links}
onClose={() => setRequestFlyoutOpen(false)}
requestBody={formatRequestPayload(state, PayloadFormat.PRETTY)}
response={response ? formatJson(response.result || response.error) : ''}
/>
)}
</div>
);
};