[Console] Fix auto-indentation issues (#214358)

Fixes https://github.com/elastic/kibana/issues/210231
Fixes https://github.com/elastic/kibana/issues/212499

## Summary
Test request:

```
GET _ingest/pipeline/_simulate
{
    "docs": [
        {
            "_source": {
                "trace": {
                    "name": "GET /actuator/health/**"
                },
                "transaction": {
                    "outcome": "success"
                }
            }
        },
            {
      "_source": {
        "vulnerability": {
          "reference": [
            "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-15778"
          ]
        }
      }
            }
    ]
}
```
This commit is contained in:
Elena Stoeva 2025-03-21 10:23:43 +00:00 committed by GitHub
parent 0aa226cad4
commit 18aa055a6a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 138 additions and 14 deletions

View file

@ -76,8 +76,8 @@ export const MonacoEditor = ({ localStorageValue, value, setValue }: EditorProps
}, [docLinkVersion]);
const autoIndentCallback = useCallback(async () => {
return actionsProvider.current!.autoIndent();
}, []);
return actionsProvider.current!.autoIndent(context);
}, [context]);
const sendRequestsCallback = useCallback(async () => {
await actionsProvider.current?.sendRequests(dispatch, context);
@ -103,7 +103,7 @@ export const MonacoEditor = ({ localStorageValue, value, setValue }: EditorProps
registerKeyboardCommands({
editor: editorInstance,
sendRequest: sendRequestsCallback,
autoIndent: async () => await actionsProvider.current?.autoIndent(),
autoIndent: async () => await actionsProvider.current?.autoIndent(context),
getDocumentationLink: getDocumenationLink,
moveToPreviousRequestEdge: async () =>
await actionsProvider.current?.moveToPreviousRequestEdge(),
@ -119,6 +119,7 @@ export const MonacoEditor = ({ localStorageValue, value, setValue }: EditorProps
registerKeyboardCommands,
unregisterKeyboardCommands,
settings.isKeyboardShortcutsEnabled,
context,
]);
const editorWillUnmountCallback = useCallback(() => {

View file

@ -620,7 +620,11 @@ export class MonacoEditorActionsProvider {
/**
* This function applies indentations to the request in the selected text.
*/
public async autoIndent() {
public async autoIndent(context: ContextValue) {
const {
services: { notifications },
} = context;
const { toasts } = notifications;
const parsedRequests = await this.getSelectedParsedRequests();
const selectionStartLineNumber = parsedRequests[0].startLineNumber;
const selectionEndLineNumber = parsedRequests[parsedRequests.length - 1].endLineNumber;
@ -638,7 +642,12 @@ export class MonacoEditorActionsProvider {
const selectedText = this.getTextInRange(selectedRange);
const allText = this.getTextInRange();
const autoIndentedText = getAutoIndentedRequests(parsedRequests, selectedText, allText);
const autoIndentedText = getAutoIndentedRequests(
parsedRequests,
selectedText,
allText,
(text) => toasts.addWarning(text)
);
this.editor.executeEdits(AUTO_INDENTATION_ACTION_LABEL, [
{

View file

@ -16,6 +16,7 @@ import {
replaceRequestVariables,
trackSentRequests,
getRequestFromEditor,
containsComments,
} from './requests_utils';
describe('requests_utils', () => {
@ -168,6 +169,7 @@ describe('requests_utils', () => {
});
describe('getAutoIndentedRequests', () => {
const mockAddToastWarning = jest.fn();
const sampleEditorTextLines = [
' ', // line 1
'GET _search ', // line 2
@ -241,7 +243,8 @@ describe('requests_utils', () => {
sampleEditorTextLines
.slice(TEST_REQUEST_1.startLineNumber - 1, TEST_REQUEST_1.endLineNumber)
.join('\n'),
sampleEditorTextLines.join('\n')
sampleEditorTextLines.join('\n'),
mockAddToastWarning
);
const expectedResultLines = [
'GET _search',
@ -253,6 +256,7 @@ describe('requests_utils', () => {
];
expect(formattedData).toBe(expectedResultLines.join('\n'));
expect(mockAddToastWarning).not.toHaveBeenCalled();
});
it('correctly auto-indents a single request with no data', () => {
@ -261,11 +265,13 @@ describe('requests_utils', () => {
sampleEditorTextLines
.slice(TEST_REQUEST_2.startLineNumber - 1, TEST_REQUEST_2.endLineNumber)
.join('\n'),
sampleEditorTextLines.join('\n')
sampleEditorTextLines.join('\n'),
mockAddToastWarning
);
const expectedResult = 'GET _all';
expect(formattedData).toBe(expectedResult);
expect(mockAddToastWarning).not.toHaveBeenCalled();
});
it('correctly auto-indents a single request with multiple data', () => {
@ -274,7 +280,8 @@ describe('requests_utils', () => {
sampleEditorTextLines
.slice(TEST_REQUEST_3.startLineNumber - 1, TEST_REQUEST_3.endLineNumber)
.join('\n'),
sampleEditorTextLines.join('\n')
sampleEditorTextLines.join('\n'),
mockAddToastWarning
);
const expectedResultLines = [
'POST /_bulk',
@ -292,13 +299,15 @@ describe('requests_utils', () => {
];
expect(formattedData).toBe(expectedResultLines.join('\n'));
expect(mockAddToastWarning).not.toHaveBeenCalled();
});
it('auto-indents multiple request with comments in between', () => {
const formattedData = getAutoIndentedRequests(
[TEST_REQUEST_1, TEST_REQUEST_2, TEST_REQUEST_3],
sampleEditorTextLines.slice(1, 23).join('\n'),
sampleEditorTextLines.join('\n')
sampleEditorTextLines.join('\n'),
mockAddToastWarning
);
const expectedResultLines = [
'GET _search',
@ -329,6 +338,7 @@ describe('requests_utils', () => {
];
expect(formattedData).toBe(expectedResultLines.join('\n'));
expect(mockAddToastWarning).not.toHaveBeenCalled();
});
it(`auto-indents method line but doesn't auto-indent data with comments`, () => {
@ -339,10 +349,16 @@ describe('requests_utils', () => {
const formattedData = getAutoIndentedRequests(
[TEST_REQUEST_4],
`${methodLine}\n${dataText}`,
sampleEditorTextLines.join('\n')
sampleEditorTextLines.join('\n'),
mockAddToastWarning
);
expect(formattedData).toBe(`GET _search // test comment\n${dataText}`);
expect(mockAddToastWarning).toHaveBeenCalledWith(
expect.stringContaining(
'Auto-indentation is currently not supported for requests containing comments. Please remove comments to enable formatting.'
)
);
});
});
@ -469,4 +485,75 @@ describe('requests_utils', () => {
});
});
});
describe('containsComments', () => {
it('should return false for JSON with // and /* inside strings', () => {
const requestData = `{
"docs": [
{
"_source": {
"trace": {
"name": "GET /actuator/health/**"
},
"transaction": {
"outcome": "success"
}
}
},
{
"_source": {
"vulnerability": {
"reference": [
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-15778"
]
}
}
}
]
}`;
expect(containsComments(requestData)).toBe(false);
});
it('should return true for text with actual line comment', () => {
const requestData = `{
// This is a comment
"query": { "match_all": {} }
}`;
expect(containsComments(requestData)).toBe(true);
});
it('should return true for text with actual block comment', () => {
const requestData = `{
/* Bulk insert */
"index": { "_index": "test" },
"field1": "value1"
}`;
expect(containsComments(requestData)).toBe(true);
});
it('should return false for text without any comments', () => {
const requestData = `{
"field": "value"
}`;
expect(containsComments(requestData)).toBe(false);
});
it('should return false for empty string', () => {
expect(containsComments('')).toBe(false);
});
it('should correctly handle escaped quotes within strings', () => {
const requestData = `{
"field": \"value with \\\"escaped quotes\\\"\"
}`;
expect(containsComments(requestData)).toBe(false);
});
it('should return true if comment is outside of strings', () => {
const requestData = `{
"field": "value" // comment here
}`;
expect(containsComments(requestData)).toBe(true);
});
});
});

View file

@ -9,6 +9,7 @@
import { monaco, ParsedRequest } from '@kbn/monaco';
import { parse } from 'hjson';
import { i18n } from '@kbn/i18n';
import { constructUrl } from '../../../../lib/es';
import type { MetricsTracker } from '../../../../types';
import type { DevToolsVariable } from '../../../components';
@ -141,7 +142,8 @@ export const getRequestEndLineNumber = ({
export const getAutoIndentedRequests = (
requests: AdjustedParsedRequest[],
selectedText: string,
allText: string
allText: string,
addToastWarning: (text: string) => void
): string => {
const selectedTextLines = selectedText.split(`\n`);
const allTextLines = allText.split(`\n`);
@ -162,10 +164,16 @@ export const getAutoIndentedRequests = (
const firstLine = cleanUpWhitespaces(requestLines[0]);
formattedTextLines.push(firstLine);
const dataLines = requestLines.slice(1);
if (dataLines.some((line) => containsComments(line))) {
if (containsComments(dataLines.join(''))) {
// If data has comments, add it as it is - without formatting
// TODO: Format requests with comments https://github.com/elastic/kibana/issues/182138
formattedTextLines.push(...dataLines);
addToastWarning(
i18n.translate('console.notification.monaco.warning.nonSupportedAutoindentation', {
defaultMessage:
'Auto-indentation is currently not supported for requests containing comments. Please remove comments to enable formatting.',
})
);
} else {
// If no comments, indent data
if (requestLines.length > 1) {
@ -224,8 +232,27 @@ export const getRequestFromEditor = (
return { method: upperCaseMethod, url, data };
};
export const containsComments = (text: string) => {
return text.indexOf('//') >= 0 || text.indexOf('/*') >= 0;
export const containsComments = (requestData: string) => {
let insideString = false;
let prevChar = '';
for (let i = 0; i < requestData.length; i++) {
const char = requestData[i];
const nextChar = requestData[i + 1];
if (!insideString && char === '"') {
insideString = true;
} else if (insideString && char === '"' && prevChar !== '\\') {
insideString = false;
} else if (!insideString) {
if (char === '/' && (nextChar === '/' || nextChar === '*')) {
return true;
}
}
prevChar = char;
}
return false;
};
export const indentData = (dataString: string): string => {