Add support for PATCH requests in Console (#165634)

## Summary

This PR adds support for PATCH requests in Console.


![patch-request](8257ca4b-303e-4f46-bbcc-6e6f95336c30)

Closes #154274

### Checklist

- [x]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

## Release note

Adds support for PATCH requests in Console.

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Youhei Sakurai 2023-09-09 10:33:56 +09:00 committed by GitHub
parent fac644cc49
commit b46a737703
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 47 additions and 12 deletions

View file

@ -53,6 +53,8 @@ Calls to the API endpoints require different operations. To interact with the {k
* *PUT* - Updates the existing information.
* *PATCH* - Applies partial modifications to the existing information.
* *DELETE* - Removes the information.
[float]

View file

@ -2090,6 +2090,12 @@ ace.define(
case 'p':
next('p');
switch (ch) {
case 'a':
next('a');
next('t');
next('c');
next('h');
return 'patch';
case 'u':
next('u');
next('t');
@ -2106,6 +2112,12 @@ ace.define(
case 'P':
next('P');
switch (ch) {
case 'A':
next('A');
next('T');
next('C');
next('H');
return 'PATCH';
case 'U':
next('U');
next('T');
@ -2120,7 +2132,7 @@ ace.define(
}
break;
default:
error('Expected one of GET/POST/PUT/DELETE/HEAD');
error('Expected one of GET/POST/PUT/DELETE/HEAD/PATCH');
}
},
value, // Place holder for the value function.
@ -2254,7 +2266,7 @@ ace.define(
annotate('error', e.message);
// snap
const substring = text.substr(at);
const nextMatch = substring.search(/^POST|HEAD|GET|PUT|DELETE/m);
const nextMatch = substring.search(/^POST|HEAD|GET|PUT|DELETE|PATCH/m);
if (nextMatch < 1) return;
reset(at + nextMatch);
}

View file

@ -38,13 +38,13 @@ export function parseCURL(text: string) {
const EscapedQuotes = /^((?:[^\\"']|\\.)+)/;
const LooksLikeCurl = /^\s*curl\s+/;
const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE)/;
const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE|PATCH)/;
const HasProtocol = /[\s"']https?:\/\//;
const CurlRequestWithProto = /[\s"']https?:\/\/[^\/ ]+\/+([^\s"']+)/;
const CurlRequestWithoutProto = /[\s"'][^\/ ]+\/+([^\s"']+)/;
const CurlData = /^.+\s(--data|-d)\s*/;
const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE)\s+\/?(.+)/;
const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE|PATCH)\s+\/?(.+)/;
if (lines.length > 0 && ExecutionComment.test(lines[0])) {
lines.shift();

View file

@ -985,7 +985,7 @@ describe('Integration', () => {
{
name: 'Cursor rows after request end',
cursor: { lineNumber: 5, column: 1 },
autoCompleteSet: ['GET', 'PUT', 'POST', 'DELETE', 'HEAD'],
autoCompleteSet: ['GET', 'PUT', 'POST', 'DELETE', 'HEAD', 'PATCH'],
prefixToAdd: '',
suffixToAdd: ' ',
},

View file

@ -967,7 +967,7 @@ export default function ({
}
function addMethodAutoCompleteSetToContext(context: AutoCompleteContext) {
context.autoCompleteSet = ['GET', 'PUT', 'POST', 'DELETE', 'HEAD'].map((m, i) => ({
context.autoCompleteSet = ['GET', 'PUT', 'POST', 'DELETE', 'HEAD', 'PATCH'].map((m, i) => ({
name: m,
score: -i,
meta: i18n.translate('console.autocomplete.addMethodMetaText', { defaultMessage: 'method' }),

View file

@ -33,7 +33,7 @@ export class UrlPatternMatcher {
// We'll group endpoints by the methods which are attached to them,
//to avoid suggesting endpoints that are incompatible with the
//method that the user has entered.
['HEAD', 'GET', 'PUT', 'POST', 'DELETE'].forEach((method) => {
['HEAD', 'GET', 'PUT', 'POST', 'DELETE', 'PATCH'].forEach((method) => {
this[method] = {
rootComponent: new SharedComponent('ROOT'),
parametrizedComponentFactories: parametrizedComponentFactories || {

View file

@ -38,13 +38,13 @@ export function parseCURL(text) {
const EscapedQuotes = /^((?:[^\\"']|\\.)+)/;
const LooksLikeCurl = /^\s*curl\s+/;
const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE)/;
const CurlVerb = /-X ?(GET|HEAD|POST|PUT|DELETE|PATCH)/;
const HasProtocol = /[\s"']https?:\/\//;
const CurlRequestWithProto = /[\s"']https?:\/\/[^\/ ]+\/+([^\s"']+)/;
const CurlRequestWithoutProto = /[\s"'][^\/ ]+\/+([^\s"']+)/;
const CurlData = /^.+\s(--data|-d)\s*/;
const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE)\s+\/?(.+)/;
const SenseLine = /^\s*(GET|HEAD|POST|PUT|DELETE|PATCH)\s+\/?(.+)/;
if (lines.length > 0 && ExecutionComment.test(lines[0])) {
lines.shift();

View file

@ -90,5 +90,11 @@ describe('Console Proxy Route', () => {
});
});
});
describe('PATCH request', () => {
it('returns the exact body', async () => {
const { payload } = await request('PATCH', '/', 'foobar');
expect(await readStream(payload)).toBe('foobar');
});
});
});
});

View file

@ -13,11 +13,11 @@ export type Body = TypeOf<typeof routeValidationConfig.body>;
const acceptedHttpVerb = schema.string({
validate: (method) => {
return ['HEAD', 'GET', 'POST', 'PUT', 'DELETE'].some(
return ['HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'].some(
(verb) => verb.toLowerCase() === method.toLowerCase()
)
? undefined
: `Method must be one of, case insensitive ['HEAD', 'GET', 'POST', 'PUT', 'DELETE']. Received '${method}'.`;
: `Method must be one of, case insensitive ['HEAD', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH']. Received '${method}'.`;
},
});

View file

@ -54,7 +54,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('HTTP methods', async () => {
const suggestions = {
G: ['GET'],
P: ['PUT', 'POST'],
P: ['PUT', 'POST', 'PATCH'],
D: ['DELETE'],
H: ['HEAD'],
};
@ -234,6 +234,8 @@ GET _search
dELETE dELETe dELEtE dELEte dELeTE dELeTe dELetE dELete dElETE dElETe dElEtE dElEte dEleTE dEleTe dEletE dElete
deLETE deLETe deLEtE deLEte deLeTE deLeTe deLetE deLete delETE delETe delEtE delEte deleTE deleTe deletE delete
HEAD HEAd HEaD HEad HeAD HeAd HeaD Head hEAD hEAd hEaD hEad heAD heAd heaD head
PATCH PATCh PATcH PATch PAtCH PAtCh PAtcH PAtch PaTCH PaTCh PaTcH PaTch PatCH PatCh PatcH Patch pATCH pATCh pATcH
pATch pAtCH pAtCh pAtcH pAtch paTCH paTCh paTcH paTch patCH patCh patcH patch
`.split(/\s+/m)
),
20 // 20 of 112 (approx. one-fifth) should be enough for testing

View file

@ -68,6 +68,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(initialSize.width).to.be.greaterThan(afterSize.width);
});
it('should return statusCode 400 to unsupported HTTP verbs', async () => {
const expectedResponseContains = '"statusCode": 400';
await PageObjects.console.enterRequest('\n OPTIONS /');
await PageObjects.console.clickPlay();
await retry.try(async () => {
const actualResponse = await PageObjects.console.getResponse();
log.debug(actualResponse);
expect(actualResponse).to.contain(expectedResponseContains);
expect(await PageObjects.console.hasSuccessBadge()).to.be(false);
});
});
describe('with kbn: prefix in request', () => {
before(async () => {
await PageObjects.console.clearTextArea();