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

[8.x] [ES|QL] Comment parsing and pretty-printing (#192173) #194117

Merged
merged 1 commit into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/esql_ast_inspector/.storybook/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

module.exports = require('@kbn/storybook').defaultConfig;
75 changes: 6 additions & 69 deletions examples/esql_ast_inspector/public/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,84 +7,21 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React, { useRef, useState } from 'react';
import {
EuiPage,
EuiPageBody,
EuiPageSection,
EuiPageHeader,
EuiSpacer,
EuiForm,
EuiTextArea,
EuiFormRow,
EuiButton,
} from '@elastic/eui';
import * as React from 'react';
import { EuiPage, EuiPageBody, EuiPageSection, EuiPageHeader, EuiSpacer } from '@elastic/eui';
import { EuiProvider } from '@elastic/eui';
import { EsqlInspector } from './components/esql_inspector';

import type { CoreStart } from '@kbn/core/public';

import { EditorError, ESQLAst, getAstAndSyntaxErrors } from '@kbn/esql-ast';
import { CodeEditor } from '@kbn/code-editor';
import type { StartDependencies } from './plugin';

export const App = (props: { core: CoreStart; plugins: StartDependencies }) => {
const [currentErrors, setErrors] = useState<EditorError[]>([]);
const [currentQuery, setQuery] = useState(
'from index1 | eval var0 = round(numberField, 2) | stats by stringField'
);

const inputRef = useRef<HTMLTextAreaElement | null>(null);

const [ast, setAST] = useState<ESQLAst>(getAstAndSyntaxErrors(currentQuery).ast);

const parseQuery = (query: string) => {
const { ast: _ast, errors } = getAstAndSyntaxErrors(query);
setErrors(errors);
setAST(_ast);
};

export const App = () => {
return (
<EuiProvider>
<EuiPage>
<EuiPageBody style={{ maxWidth: 800, margin: '0 auto' }}>
<EuiPageBody style={{ maxWidth: 1200, margin: '0 auto' }}>
<EuiPageHeader paddingSize="s" bottomBorder={true} pageTitle="ES|QL AST Inspector" />
<EuiPageSection paddingSize="s">
<p>This app gives you the AST for a particular ES|QL query.</p>

<EuiSpacer />

<EuiForm>
<EuiFormRow
fullWidth
label="Query"
isInvalid={Boolean(currentErrors.length)}
error={currentErrors.map((error) => error.message)}
>
<EuiTextArea
inputRef={(node) => {
inputRef.current = node;
}}
isInvalid={Boolean(currentErrors.length)}
fullWidth
value={currentQuery}
onChange={(e) => setQuery(e.target.value)}
css={{
height: '5em',
}}
/>
</EuiFormRow>
<EuiFormRow fullWidth>
<EuiButton fullWidth onClick={() => parseQuery(inputRef.current?.value ?? '')}>
Parse
</EuiButton>
</EuiFormRow>
</EuiForm>
<EuiSpacer />
<CodeEditor
allowFullScreen={true}
languageId={'json'}
value={JSON.stringify(ast, null, 2)}
/>
<EsqlInspector />
</EuiPageSection>
</EuiPageBody>
</EuiPage>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import * as React from 'react';
import { Annotations } from './annotations';

export default {
title: '<Annotations>',
parameters: {},
};

export const Default = () => (
<Annotations
value={'FROM index | LIMIT 10 | SORT some_field'}
annotations={[
[0, 4, (text) => <span style={{ color: 'red' }}>{text}</span>],
[5, 10, (text) => <span style={{ color: 'blue' }}>{text}</span>],
[13, 18, (text) => <span style={{ color: 'red' }}>{text}</span>],
[19, 21, (text) => <span style={{ color: 'green' }}>{text}</span>],
]}
/>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import * as React from 'react';
import type { Annotation } from './types';

export interface AnnotationsProps {
value: string;
annotations?: Annotation[];
}

export const Annotations: React.FC<AnnotationsProps> = (props) => {
const { value, annotations = [] } = props;
const annotationNodes: React.ReactNode[] = [];

let pos = 0;

for (const [start, end, render] of annotations) {
if (start > pos) {
const text = value.slice(pos, start);

annotationNodes.push(<span>{text}</span>);
}

const text = value.slice(start, end);

pos = end;
annotationNodes.push(render(text));
}

if (pos < value.length) {
const text = value.slice(pos);
annotationNodes.push(<span>{text}</span>);
}

return React.createElement('span', {}, ...annotationNodes);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export { Annotations, type AnnotationsProps } from './annotations';
export type { Annotation } from './types';
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,10 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { Token } from 'antlr4';
import * as React from 'react';

export function getPosition(
token: Pick<Token, 'start' | 'stop'> | null,
lastToken?: Pick<Token, 'stop'> | undefined
) {
if (!token || token.start < 0) {
return { min: 0, max: 0 };
}
const endFirstToken = token.stop > -1 ? Math.max(token.stop + 1, token.start) : undefined;
const endLastToken = lastToken?.stop;
return {
min: token.start,
max: endLastToken ?? endFirstToken ?? Infinity,
};
}
export type Annotation = [
start: number,
end: number,
annotation: (text: string) => React.ReactNode
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import * as React from 'react';
import { css } from '@emotion/react';
import { Annotations, type Annotation } from '../annotations';
import { FlexibleInput } from '../flexible_input/flexible_input';

const blockCss = css({
display: 'inline-block',
position: 'relative',
width: '100%',
fontSize: '18px',
lineHeight: '1.3',
fontFamily:
"'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', Menlo, Consolas, monospace",
});

const backdropCss = css({
display: 'inline-block',
position: 'absolute',
left: 0,
width: '100%',
pointerEvents: 'all',
userSelect: 'none',
whiteSpace: 'pre',
color: 'rgba(255, 255, 255, 0.01)',
});

const inputCss = css({
display: 'inline-block',
color: 'rgba(255, 255, 255, 0.01)',
caretColor: '#07f',
});

const overlayCss = css({
display: 'inline-block',
position: 'absolute',
left: 0,
width: '100%',
pointerEvents: 'none',
userSelect: 'none',
whiteSpace: 'pre',
});

export interface EsqlEditorProps {
src: string;
backdrops?: Annotation[][];
highlight?: Annotation[];
onChange: (src: string) => void;
}

export const EsqlEditor: React.FC<EsqlEditorProps> = (props) => {
const { src, highlight, onChange } = props;

const backdrops: React.ReactNode[] = [];

if (props.backdrops) {
for (let i = 0; i < props.backdrops.length; i++) {
const backdrop = props.backdrops[i];

backdrops.push(
<div key={i} css={backdropCss}>
<Annotations value={src} annotations={backdrop} />
</div>
);
}
}

const overlay = !!highlight && (
<div css={overlayCss}>
<Annotations value={src} annotations={highlight} />
</div>
);

return (
<div css={blockCss}>
{backdrops}
<div css={inputCss}>
<FlexibleInput multiline value={src} onChange={(e) => onChange(e.target.value)} />
</div>
{overlay}
</div>
);
};
Loading