diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts index d5774adc0d516..1006d39138759 100644 --- a/x-pack/plugins/enterprise_search/common/types/index.ts +++ b/x-pack/plugins/enterprise_search/common/types/index.ts @@ -30,3 +30,14 @@ export interface IConfiguredLimits { appSearch: IAppSearchConfiguredLimits; workplaceSearch: IWorkplaceSearchConfiguredLimits; } + +export interface IMetaPage { + current: number; + size: number; + total_pages: number; + total_results: number; +} + +export interface IMeta { + page: IMetaPage; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts new file mode 100644 index 0000000000000..92d14f7275185 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -0,0 +1,41 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ADMIN = 'admin'; +export const PRIVATE = 'private'; +export const SEARCH = 'search'; + +export const TOKEN_TYPE_DESCRIPTION = { + [SEARCH]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.search.description', { + defaultMessage: 'Public Search Keys are used for search endpoints only.', + }), + [PRIVATE]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.private.description', { + defaultMessage: + 'Private API Keys are used for read and/or write access on one or more Engines.', + }), + [ADMIN]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.admin.description', { + defaultMessage: 'Private Admin Keys are used to interact with the Credentials API.', + }), +}; + +export const TOKEN_TYPE_DISPLAY_NAMES = { + [SEARCH]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.search.name', { + defaultMessage: 'Public Search Key', + }), + [PRIVATE]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.private.name', { + defaultMessage: 'Private API Key', + }), + [ADMIN]: i18n.translate('xpack.enterpriseSearch.appSearch.tokens.admin.name', { + defaultMessage: 'Private Admin Key', + }), +}; + +export const TOKEN_TYPE_INFO = [ + { value: SEARCH, text: TOKEN_TYPE_DISPLAY_NAMES[SEARCH] }, + { value: PRIVATE, text: TOKEN_TYPE_DISPLAY_NAMES[PRIVATE] }, + { value: ADMIN, text: TOKEN_TYPE_DISPLAY_NAMES[ADMIN] }, +]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts new file mode 100644 index 0000000000000..c5cb8a2c61759 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -0,0 +1,1196 @@ +/* + * 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 { resetContext } from 'kea'; + +import { CredentialsLogic } from './credentials_logic'; +import { ADMIN, PRIVATE } from './constants'; + +jest.mock('../../../shared/http', () => ({ + HttpLogic: { values: { http: { get: jest.fn(), delete: jest.fn() } } }, +})); +import { HttpLogic } from '../../../shared/http'; +jest.mock('../../../shared/flash_messages', () => ({ + flashAPIErrors: jest.fn(), +})); +import { flashAPIErrors } from '../../../shared/flash_messages'; + +describe('CredentialsLogic', () => { + const DEFAULT_VALUES = { + activeApiToken: { + name: '', + type: PRIVATE, + read: true, + write: true, + access_all_engines: true, + }, + activeApiTokenIsExisting: false, + activeApiTokenRawName: '', + apiTokens: [], + dataLoading: true, + engines: [], + formErrors: [], + isCredentialsDataComplete: false, + isCredentialsDetailsComplete: false, + meta: {}, + nameInputBlurred: false, + showCredentialsForm: false, + }; + + const mount = (defaults?: object) => { + if (!defaults) { + resetContext({}); + } else { + resetContext({ + defaults: { + enterprise_search: { + app_search: { + credentials_logic: { + ...defaults, + }, + }, + }, + }, + }); + } + CredentialsLogic.mount(); + }; + + const newToken = { + id: 1, + name: 'myToken', + type: PRIVATE, + read: true, + write: true, + access_all_engines: true, + engines: [], + }; + + const credentialsDetails = { + engines: [ + { name: 'engine1', type: 'indexed', language: 'english', result_fields: [] }, + { name: 'engine1', type: 'indexed', language: 'english', result_fields: [] }, + ], + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(CredentialsLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('addEngineName', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: expect.any(Object), + }; + + describe('activeApiToken', () => { + it("should add an engine to the active api token's engine list", () => { + mount({ + activeApiToken: { + ...newToken, + engines: ['someEngine'], + }, + }); + + CredentialsLogic.actions.addEngineName('newEngine'); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, engines: ['someEngine', 'newEngine'] }, + }); + }); + + it("should create a new engines list if one doesn't exist", () => { + mount({ + activeApiToken: { + ...newToken, + engines: undefined, + }, + }); + + CredentialsLogic.actions.addEngineName('newEngine'); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, engines: ['newEngine'] }, + }); + }); + }); + }); + + describe('removeEngineName', () => { + describe('activeApiToken', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: expect.any(Object), + }; + + it("should remove an engine from the active api token's engine list", () => { + mount({ + activeApiToken: { + ...newToken, + engines: ['someEngine', 'anotherEngine'], + }, + }); + + CredentialsLogic.actions.removeEngineName('someEngine'); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, engines: ['anotherEngine'] }, + }); + }); + + it('will not remove the engine if it is not found', () => { + mount({ + activeApiToken: { + ...newToken, + engines: ['someEngine', 'anotherEngine'], + }, + }); + + CredentialsLogic.actions.removeEngineName('notfound'); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, engines: ['someEngine', 'anotherEngine'] }, + }); + }); + + it('does not throw a type error if no engines are stored in state', () => { + mount({ + activeApiToken: {}, + }); + CredentialsLogic.actions.removeEngineName(''); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { engines: [] }, + }); + }); + }); + }); + + describe('setAccessAllEngines', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: expect.any(Object), + }; + + describe('activeApiToken', () => { + it('should set the value of access_all_engines and clear out engines list if true', () => { + mount({ + activeApiToken: { + ...newToken, + access_all_engines: false, + engines: ['someEngine', 'anotherEngine'], + }, + }); + + CredentialsLogic.actions.setAccessAllEngines(true); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, engines: [], access_all_engines: true }, + }); + }); + + it('should set the value of access_all_engines and but maintain engines list if false', () => { + mount({ + activeApiToken: { + ...newToken, + access_all_engines: true, + engines: ['someEngine', 'anotherEngine'], + }, + }); + + CredentialsLogic.actions.setAccessAllEngines(false); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...newToken, + access_all_engines: false, + engines: ['someEngine', 'anotherEngine'], + }, + }); + }); + }); + }); + + describe('onApiKeyDelete', () => { + const values = { + ...DEFAULT_VALUES, + apiTokens: expect.any(Array), + }; + + describe('apiTokens', () => { + it('should remove specified token from apiTokens if name matches', () => { + mount({ + apiTokens: [newToken], + }); + + CredentialsLogic.actions.onApiKeyDelete(newToken.name); + expect(CredentialsLogic.values).toEqual({ + ...values, + apiTokens: [], + }); + }); + + it('should not remove specified token from apiTokens if name does not match', () => { + mount({ + apiTokens: [newToken], + }); + + CredentialsLogic.actions.onApiKeyDelete('foo'); + expect(CredentialsLogic.values).toEqual({ + ...values, + apiTokens: [newToken], + }); + }); + }); + }); + + describe('onApiTokenCreateSuccess', () => { + const values = { + ...DEFAULT_VALUES, + apiTokens: expect.any(Array), + activeApiToken: expect.any(Object), + activeApiTokenRawName: expect.any(String), + showCredentialsForm: expect.any(Boolean), + formErrors: expect.any(Array), + }; + + describe('apiTokens', () => { + const existingToken = { + name: 'some_token', + type: PRIVATE, + }; + + it('should add the provided token to the apiTokens list', () => { + mount({ + apiTokens: [existingToken], + }); + + CredentialsLogic.actions.onApiTokenCreateSuccess(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + apiTokens: [existingToken, newToken], + }); + }); + }); + + describe('activeApiToken', () => { + // TODO It is weird that methods like this update activeApiToken but not activeApiTokenIsExisting... + it('should reset to the default value, which effectively clears out the current form', () => { + mount({ + activeApiToken: newToken, + }); + + CredentialsLogic.actions.onApiTokenCreateSuccess(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: DEFAULT_VALUES.activeApiToken, + }); + }); + }); + + describe('activeApiTokenRawName', () => { + it('should reset to the default value, which effectively clears out the current form', () => { + mount({ + activeApiTokenRawName: 'foo', + }); + + CredentialsLogic.actions.onApiTokenCreateSuccess(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenRawName: DEFAULT_VALUES.activeApiTokenRawName, + }); + }); + }); + + describe('showCredentialsForm', () => { + it('should reset to the default value, which closes the credentials form', () => { + mount({ + showCredentialsForm: true, + }); + + CredentialsLogic.actions.onApiTokenCreateSuccess(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + showCredentialsForm: false, + }); + }); + }); + + describe('formErrors', () => { + it('should reset `formErrors`', () => { + mount({ + formErrors: ['I am an error'], + }); + + CredentialsLogic.actions.onApiTokenCreateSuccess(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + formErrors: [], + }); + }); + }); + }); + + describe('onApiTokenError', () => { + const values = { + ...DEFAULT_VALUES, + formErrors: expect.any(Array), + }; + + describe('formErrors', () => { + it('should set `formErrors`', () => { + mount({ + formErrors: ['I am an error'], + }); + + CredentialsLogic.actions.onApiTokenError(['I am the NEW error']); + expect(CredentialsLogic.values).toEqual({ + ...values, + formErrors: ['I am the NEW error'], + }); + }); + }); + }); + + describe('onApiTokenUpdateSuccess', () => { + const values = { + ...DEFAULT_VALUES, + apiTokens: expect.any(Array), + activeApiToken: expect.any(Object), + activeApiTokenRawName: expect.any(String), + showCredentialsForm: expect.any(Boolean), + }; + + describe('apiTokens', () => { + const existingToken = { + name: 'some_token', + type: PRIVATE, + }; + + it('should replace the existing token with the new token by name', () => { + mount({ + apiTokens: [newToken, existingToken], + }); + const updatedExistingToken = { + ...existingToken, + type: ADMIN, + }; + + CredentialsLogic.actions.onApiTokenUpdateSuccess(updatedExistingToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + apiTokens: [newToken, updatedExistingToken], + }); + }); + + // TODO Not sure if this is a good behavior or not + it('if for some reason the existing token is not found, it adds a new token...', () => { + mount({ + apiTokens: [newToken, existingToken], + }); + const brandNewToken = { + name: 'brand new token', + type: ADMIN, + }; + + CredentialsLogic.actions.onApiTokenUpdateSuccess(brandNewToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + apiTokens: [newToken, existingToken, brandNewToken], + }); + }); + }); + + describe('activeApiToken', () => { + it('should reset to the default value, which effectively clears out the current form', () => { + mount({ + activeApiToken: newToken, + }); + + CredentialsLogic.actions.onApiTokenUpdateSuccess({ ...newToken, type: ADMIN }); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: DEFAULT_VALUES.activeApiToken, + }); + }); + }); + + describe('activeApiTokenRawName', () => { + it('should reset to the default value, which effectively clears out the current form', () => { + mount({ + activeApiTokenRawName: 'foo', + }); + + CredentialsLogic.actions.onApiTokenUpdateSuccess({ ...newToken, type: ADMIN }); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenRawName: DEFAULT_VALUES.activeApiTokenRawName, + }); + }); + }); + + describe('showCredentialsForm', () => { + it('should reset to the default value, which closes the credentials form', () => { + mount({ + showCredentialsForm: true, + }); + + CredentialsLogic.actions.onApiTokenUpdateSuccess({ ...newToken, type: ADMIN }); + expect(CredentialsLogic.values).toEqual({ + ...values, + showCredentialsForm: false, + }); + }); + }); + }); + + describe('setCredentialsData', () => { + const meta = { + page: { + current: 1, + size: 1, + total_pages: 1, + total_results: 1, + }, + }; + + const values = { + ...DEFAULT_VALUES, + apiTokens: expect.any(Array), + meta: expect.any(Object), + isCredentialsDataComplete: expect.any(Boolean), + }; + + describe('apiTokens', () => { + it('should be set', () => { + mount(); + + CredentialsLogic.actions.setCredentialsData(meta, [newToken, newToken]); + expect(CredentialsLogic.values).toEqual({ + ...values, + apiTokens: [newToken, newToken], + }); + }); + }); + + describe('meta', () => { + it('should be set', () => { + mount(); + + CredentialsLogic.actions.setCredentialsData(meta, [newToken, newToken]); + expect(CredentialsLogic.values).toEqual({ + ...values, + meta, + }); + }); + }); + + describe('isCredentialsDataComplete', () => { + it('should be set to true so we know that data fetching has completed', () => { + mount({ + isCredentialsDataComplete: false, + }); + + CredentialsLogic.actions.setCredentialsData(meta, [newToken, newToken]); + expect(CredentialsLogic.values).toEqual({ + ...values, + isCredentialsDataComplete: true, + }); + }); + }); + }); + + describe('setCredentialsDetails', () => { + const values = { + ...DEFAULT_VALUES, + engines: expect.any(Array), + isCredentialsDetailsComplete: expect.any(Boolean), + }; + + describe('isCredentialsDataComplete', () => { + it('should be set to true so that we know data fetching has been completed', () => { + mount({ + isCredentialsDetailsComplete: false, + }); + + CredentialsLogic.actions.setCredentialsDetails(credentialsDetails); + expect(CredentialsLogic.values).toEqual({ + ...values, + isCredentialsDetailsComplete: true, + }); + }); + }); + + describe('engines', () => { + it('should set `engines` from the provided details object', () => { + mount({ + engines: [], + }); + + CredentialsLogic.actions.setCredentialsDetails(credentialsDetails); + expect(CredentialsLogic.values).toEqual({ + ...values, + engines: credentialsDetails.engines, + }); + }); + }); + }); + + describe('setNameInputBlurred', () => { + const values = { + ...DEFAULT_VALUES, + nameInputBlurred: expect.any(Boolean), + }; + + describe('nameInputBlurred', () => { + it('should set this value', () => { + mount({ + nameInputBlurred: false, + }); + + CredentialsLogic.actions.setNameInputBlurred(true); + expect(CredentialsLogic.values).toEqual({ + ...values, + nameInputBlurred: true, + }); + }); + }); + }); + + describe('setTokenReadWrite', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: expect.any(Object), + }; + + describe('activeApiToken', () => { + it('should set "read" or "write" values', () => { + mount({ + activeApiToken: { + ...newToken, + read: false, + }, + }); + + CredentialsLogic.actions.setTokenReadWrite({ name: 'read', checked: true }); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...newToken, + read: true, + }, + }); + }); + }); + }); + + describe('setTokenName', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: expect.any(Object), + activeApiTokenRawName: expect.any(String), + }; + + describe('activeApiToken', () => { + it('update the name property on the activeApiToken, formatted correctly', () => { + mount({ + activeApiToken: { + ...newToken, + name: 'bar', + }, + }); + + CredentialsLogic.actions.setTokenName('New Name'); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, name: 'new-name' }, + }); + }); + }); + + describe('activeApiTokenRawName', () => { + it('updates the raw name, with no formatting applied', () => { + mount(); + + CredentialsLogic.actions.setTokenName('New Name'); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenRawName: 'New Name', + }); + }); + }); + }); + + describe('setTokenType', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: { + ...newToken, + type: expect.any(String), + read: expect.any(Boolean), + write: expect.any(Boolean), + access_all_engines: expect.any(Boolean), + engines: expect.any(Array), + }, + }; + + describe('activeApiToken.access_all_engines', () => { + describe('when value is ADMIN', () => { + it('updates access_all_engines to false', () => { + mount({ + activeApiToken: { + ...newToken, + access_all_engines: true, + }, + }); + + CredentialsLogic.actions.setTokenType(ADMIN); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + access_all_engines: false, + }, + }); + }); + }); + + describe('when value is not ADMIN', () => { + it('will maintain access_all_engines value when true', () => { + mount({ + activeApiToken: { + ...newToken, + access_all_engines: true, + }, + }); + + CredentialsLogic.actions.setTokenType(PRIVATE); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + access_all_engines: true, + }, + }); + }); + + it('will maintain access_all_engines value when false', () => { + mount({ + activeApiToken: { + ...newToken, + access_all_engines: false, + }, + }); + + CredentialsLogic.actions.setTokenType(PRIVATE); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + access_all_engines: false, + }, + }); + }); + }); + }); + + describe('activeApiToken.engines', () => { + describe('when value is ADMIN', () => { + it('clears the array', () => { + mount({ + activeApiToken: { + ...newToken, + engines: [{}, {}], + }, + }); + + CredentialsLogic.actions.setTokenType(ADMIN); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + engines: [], + }, + }); + }); + }); + + describe('when value is not ADMIN', () => { + it('will maintain engines array', () => { + mount({ + activeApiToken: { + ...newToken, + engines: [{}, {}], + }, + }); + + CredentialsLogic.actions.setTokenType(PRIVATE); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + engines: [{}, {}], + }, + }); + }); + }); + }); + + describe('activeApiToken.write', () => { + describe('when value is PRIVATE', () => { + it('sets this to true', () => { + mount({ + activeApiToken: { + ...newToken, + write: false, + }, + }); + + CredentialsLogic.actions.setTokenType(PRIVATE); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + write: true, + }, + }); + }); + }); + + describe('when value is not PRIVATE', () => { + it('sets this to false', () => { + mount({ + activeApiToken: { + ...newToken, + write: true, + }, + }); + + CredentialsLogic.actions.setTokenType(ADMIN); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + write: false, + }, + }); + }); + }); + }); + + describe('activeApiToken.read', () => { + describe('when value is PRIVATE', () => { + it('sets this to true', () => { + mount({ + activeApiToken: { + ...newToken, + read: false, + }, + }); + + CredentialsLogic.actions.setTokenType(PRIVATE); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + read: true, + }, + }); + }); + }); + + describe('when value is not PRIVATE', () => { + it('sets this to false', () => { + mount({ + activeApiToken: { + ...newToken, + read: true, + }, + }); + + CredentialsLogic.actions.setTokenType(ADMIN); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + read: false, + }, + }); + }); + }); + }); + + describe('activeApiToken.type', () => { + it('sets the type value', () => { + mount({ + activeApiToken: { + ...newToken, + type: ADMIN, + }, + }); + + CredentialsLogic.actions.setTokenType(PRIVATE); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: { + ...values.activeApiToken, + type: PRIVATE, + }, + }); + }); + }); + }); + + describe('toggleCredentialsForm', () => { + const values = { + ...DEFAULT_VALUES, + activeApiTokenIsExisting: expect.any(Boolean), + activeApiToken: expect.any(Object), + activeApiTokenRawName: expect.any(String), + formErrors: expect.any(Array), + showCredentialsForm: expect.any(Boolean), + }; + + describe('showCredentialsForm', () => { + it('should toggle `showCredentialsForm`', () => { + mount({ + showCredentialsForm: false, + }); + + CredentialsLogic.actions.toggleCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + showCredentialsForm: true, + }); + + CredentialsLogic.actions.toggleCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + showCredentialsForm: false, + }); + }); + }); + + describe('formErrors', () => { + it('should reset `formErrors`', () => { + mount({ + formErrors: ['I am an error'], + }); + + CredentialsLogic.actions.toggleCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + formErrors: [], + }); + }); + }); + + describe('activeApiTokenRawName', () => { + it('should set `activeApiTokenRawName` to the name of the provided token', () => { + mount(); + + CredentialsLogic.actions.toggleCredentialsForm(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenRawName: 'myToken', + }); + }); + + it('should set `activeApiTokenRawName` to the default value if no token is provided', () => { + mount(); + + CredentialsLogic.actions.toggleCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenRawName: DEFAULT_VALUES.activeApiTokenRawName, + }); + }); + + // TODO: This fails, is this an issue? Instead of reseting back to the default value, it sets it to the previously + // used value... to be honest, this should probably just be a selector + // it('should set `activeApiTokenRawName` back to the default value if no token is provided', () => { + // mount(); + // CredentialsLogic.actions.toggleCredentialsForm(newToken); + // CredentialsLogic.actions.toggleCredentialsForm(); + // expect(CredentialsLogic.values).toEqual({ + // ...values, + // activeApiTokenRawName: DEFAULT_VALUES.activeApiTokenRawName, + // }); + // }); + }); + + describe('activeApiToken', () => { + it('should set `activeApiToken` to the provided token', () => { + mount(); + + CredentialsLogic.actions.toggleCredentialsForm(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: newToken, + }); + }); + + it('should set `activeApiToken` to the default value if no token is provided', () => { + mount({ + activeApiToken: newToken, + }); + + CredentialsLogic.actions.toggleCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiToken: DEFAULT_VALUES.activeApiToken, + }); + }); + }); + + // TODO: This should probably just be a selector... + describe('activeApiTokenIsExisting', () => { + it('should set `activeApiTokenIsExisting` to true when the provided token has an id', () => { + mount({ + activeApiTokenIsExisting: false, + }); + + CredentialsLogic.actions.toggleCredentialsForm(newToken); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenIsExisting: true, + }); + }); + + it('should set `activeApiTokenIsExisting` to false when the provided token has no id', () => { + mount({ + activeApiTokenIsExisting: true, + }); + const { id, ...newTokenWithoutId } = newToken; + + CredentialsLogic.actions.toggleCredentialsForm(newTokenWithoutId); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenIsExisting: false, + }); + }); + + it('should set `activeApiTokenIsExisting` to false when no token is provided', () => { + mount({ + activeApiTokenIsExisting: true, + }); + + CredentialsLogic.actions.toggleCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenIsExisting: false, + }); + }); + }); + }); + + describe('hideCredentialsForm', () => { + const values = { + ...DEFAULT_VALUES, + showCredentialsForm: expect.any(Boolean), + activeApiTokenRawName: expect.any(String), + }; + + describe('activeApiTokenRawName', () => { + it('resets this value', () => { + mount({ + activeApiTokenRawName: 'foo', + }); + + CredentialsLogic.actions.hideCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + activeApiTokenRawName: '', + }); + }); + }); + + describe('showCredentialsForm', () => { + it('resets this value', () => { + mount({ + showCredentialsForm: true, + }); + + CredentialsLogic.actions.hideCredentialsForm(); + expect(CredentialsLogic.values).toEqual({ + ...values, + showCredentialsForm: false, + }); + }); + }); + }); + + describe('resetCredentials', () => { + const values = { + ...DEFAULT_VALUES, + isCredentialsDetailsComplete: expect.any(Boolean), + isCredentialsDataComplete: expect.any(Boolean), + formErrors: expect.any(Array), + }; + + describe('isCredentialsDetailsComplete', () => { + it('should reset to false', () => { + mount({ + isCredentialsDetailsComplete: true, + }); + + CredentialsLogic.actions.resetCredentials(); + expect(CredentialsLogic.values).toEqual({ + ...values, + isCredentialsDetailsComplete: false, + }); + }); + }); + + describe('isCredentialsDataComplete', () => { + it('should reset to false', () => { + mount({ + isCredentialsDataComplete: true, + }); + + CredentialsLogic.actions.resetCredentials(); + expect(CredentialsLogic.values).toEqual({ + ...values, + isCredentialsDataComplete: false, + }); + }); + }); + + describe('formErrors', () => { + it('should reset', () => { + mount({ + formErrors: ['I am an error'], + }); + + CredentialsLogic.actions.resetCredentials(); + expect(CredentialsLogic.values).toEqual({ + ...values, + formErrors: [], + }); + }); + }); + }); + + describe('initializeCredentialsData', () => { + it('should call fetchCredentials and fetchDetails', () => { + mount(); + jest.spyOn(CredentialsLogic.actions, 'fetchCredentials').mockImplementationOnce(() => {}); + jest.spyOn(CredentialsLogic.actions, 'fetchDetails').mockImplementationOnce(() => {}); + + CredentialsLogic.actions.initializeCredentialsData(); + expect(CredentialsLogic.actions.fetchCredentials).toHaveBeenCalled(); + expect(CredentialsLogic.actions.fetchDetails).toHaveBeenCalled(); + }); + }); + + describe('fetchCredentials', () => { + const meta = { + page: { + current: 1, + size: 1, + total_pages: 1, + total_results: 1, + }, + }; + const results: object[] = []; + + it('will call an API endpoint and set the results with the `setCredentialsData` action', async () => { + mount(); + jest.spyOn(CredentialsLogic.actions, 'setCredentialsData').mockImplementationOnce(() => {}); + const promise = Promise.resolve({ meta, results }); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + CredentialsLogic.actions.fetchCredentials(2); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/app_search/credentials', { + query: { + 'page[current]': 2, + }, + }); + await promise; + expect(CredentialsLogic.actions.setCredentialsData).toHaveBeenCalledWith(meta, results); + }); + + it('handles errors', async () => { + mount(); + const promise = Promise.reject('An error occured'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + CredentialsLogic.actions.fetchCredentials(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + } + }); + }); + + describe('fetchDetails', () => { + it('will call an API endpoint and set the results with the `setCredentialsDetails` action', async () => { + mount(); + jest + .spyOn(CredentialsLogic.actions, 'setCredentialsDetails') + .mockImplementationOnce(() => {}); + const promise = Promise.resolve(credentialsDetails); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + CredentialsLogic.actions.fetchDetails(); + expect(HttpLogic.values.http.get).toHaveBeenCalledWith('/api/app_search/credentials/details'); + await promise; + expect(CredentialsLogic.actions.setCredentialsDetails).toHaveBeenCalledWith( + credentialsDetails + ); + }); + + it('handles errors', async () => { + mount(); + const promise = Promise.reject('An error occured'); + (HttpLogic.values.http.get as jest.Mock).mockReturnValue(promise); + + CredentialsLogic.actions.fetchDetails(); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + } + }); + }); + + describe('deleteApiKey', () => { + const tokenName = 'abc123'; + + it('will call an API endpoint and set the results with the `onApiKeyDelete` action', async () => { + mount(); + jest.spyOn(CredentialsLogic.actions, 'onApiKeyDelete').mockImplementationOnce(() => {}); + const promise = Promise.resolve(); + (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise); + + CredentialsLogic.actions.deleteApiKey(tokenName); + expect(HttpLogic.values.http.delete).toHaveBeenCalledWith( + `/api/app_search/credentials/${tokenName}` + ); + await promise; + expect(CredentialsLogic.actions.onApiKeyDelete).toHaveBeenCalledWith(tokenName); + }); + + it('handles errors', async () => { + mount(); + const promise = Promise.reject('An error occured'); + (HttpLogic.values.http.delete as jest.Mock).mockReturnValue(promise); + + CredentialsLogic.actions.deleteApiKey(tokenName); + try { + await promise; + } catch { + expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); + } + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts new file mode 100644 index 0000000000000..43f2731711823 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts @@ -0,0 +1,264 @@ +/* + * 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 { kea, MakeLogicType } from 'kea'; + +import { formatApiName } from '../../utils/format_api_name'; +import { ADMIN, PRIVATE } from './constants'; + +import { HttpLogic } from '../../../shared/http'; +import { IMeta } from '../../../../../common/types'; +import { flashAPIErrors } from '../../../shared/flash_messages'; +import { IEngine } from '../../types'; +import { IApiToken, ICredentialsDetails } from './types'; + +interface ITokenReadWrite { + name: 'read' | 'write'; + checked: boolean; +} + +const defaultApiToken: IApiToken = { + name: '', + type: PRIVATE, + read: true, + write: true, + access_all_engines: true, +}; + +// TODO CREATE_MESSAGE, UPDATE_MESSAGE, and DELETE_MESSAGE from ent-search + +export interface ICredentialsLogicActions { + addEngineName(engineName: string): string; + onApiKeyDelete(tokenName: string): string; + onApiTokenCreateSuccess(apiToken: IApiToken): IApiToken; + onApiTokenError(formErrors: string[]): string[]; + onApiTokenUpdateSuccess(apiToken: IApiToken): IApiToken; + removeEngineName(engineName: string): string; + setAccessAllEngines(accessAll: boolean): boolean; + setCredentialsData(meta: IMeta, apiTokens: IApiToken[]): { meta: IMeta; apiTokens: IApiToken[] }; + setCredentialsDetails(details: ICredentialsDetails): ICredentialsDetails; + setNameInputBlurred(isBlurred: boolean): boolean; + setTokenReadWrite(tokenReadWrite: ITokenReadWrite): ITokenReadWrite; + setTokenName(name: string): string; + setTokenType(tokenType: string): string; + toggleCredentialsForm(apiToken?: IApiToken): IApiToken; + hideCredentialsForm(): { value: boolean }; + resetCredentials(): { value: boolean }; + initializeCredentialsData(): { value: boolean }; + fetchCredentials(page?: number): number; + fetchDetails(): { value: boolean }; + deleteApiKey(tokenName: string): string; +} + +export interface ICredentialsLogicValues { + activeApiToken: IApiToken; + activeApiTokenIsExisting: boolean; + activeApiTokenRawName: string; + apiTokens: IApiToken[]; + dataLoading: boolean; + engines: IEngine[]; + formErrors: string[]; + isCredentialsDataComplete: boolean; + isCredentialsDetailsComplete: boolean; + fullEngineAccessChecked: boolean; + meta: Partial; + nameInputBlurred: boolean; + showCredentialsForm: boolean; +} + +export const CredentialsLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'app_search', 'credentials_logic'], + actions: () => ({ + addEngineName: (engineName) => engineName, + onApiKeyDelete: (tokenName) => tokenName, + onApiTokenCreateSuccess: (apiToken) => apiToken, + onApiTokenError: (formErrors) => formErrors, + onApiTokenUpdateSuccess: (apiToken) => apiToken, + removeEngineName: (engineName) => engineName, + setAccessAllEngines: (accessAll) => accessAll, + setCredentialsData: (meta, apiTokens) => ({ meta, apiTokens }), + setCredentialsDetails: (details) => details, + setNameInputBlurred: (nameInputBlurred) => nameInputBlurred, + setTokenReadWrite: ({ name, checked }) => ({ + name, + checked, + }), + setTokenName: (name) => name, + setTokenType: (tokenType) => tokenType, + toggleCredentialsForm: (apiToken = { ...defaultApiToken }) => apiToken, + hideCredentialsForm: false, + resetCredentials: false, + initializeCredentialsData: true, + fetchCredentials: (page) => page, + fetchDetails: true, + deleteApiKey: (tokenName) => tokenName, + }), + reducers: () => ({ + apiTokens: [ + [], + { + setCredentialsData: (_, { apiTokens }) => apiTokens, + onApiTokenCreateSuccess: (apiTokens, apiToken) => [...apiTokens, apiToken], + onApiTokenUpdateSuccess: (apiTokens, apiToken) => [ + ...apiTokens.filter((token) => token.name !== apiToken.name), + apiToken, + ], + onApiKeyDelete: (apiTokens, tokenName) => + apiTokens.filter((token) => token.name !== tokenName), + }, + ], + meta: [ + {}, + { + setCredentialsData: (_, { meta }) => meta, + }, + ], + isCredentialsDetailsComplete: [ + false, + { + setCredentialsDetails: () => true, + resetCredentials: () => false, + }, + ], + isCredentialsDataComplete: [ + false, + { + setCredentialsData: () => true, + resetCredentials: () => false, + }, + ], + engines: [ + [], + { + setCredentialsDetails: (_, { engines }) => engines, + }, + ], + nameInputBlurred: [ + false, + { + setNameInputBlurred: (_, nameInputBlurred) => nameInputBlurred, + }, + ], + activeApiToken: [ + defaultApiToken, + { + addEngineName: (activeApiToken, engineName) => ({ + ...activeApiToken, + engines: [...(activeApiToken.engines || []), engineName], + }), + removeEngineName: (activeApiToken, engineName) => ({ + ...activeApiToken, + engines: (activeApiToken.engines || []).filter((name) => name !== engineName), + }), + setAccessAllEngines: (activeApiToken, accessAll) => ({ + ...activeApiToken, + access_all_engines: accessAll, + engines: accessAll ? [] : activeApiToken.engines, + }), + onApiTokenCreateSuccess: () => defaultApiToken, + onApiTokenUpdateSuccess: () => defaultApiToken, + setTokenName: (activeApiToken, name) => ({ ...activeApiToken, name: formatApiName(name) }), + setTokenReadWrite: (activeApiToken, { name, checked }) => ({ + ...activeApiToken, + [name]: checked, + }), + setTokenType: (activeApiToken, tokenType) => ({ + ...activeApiToken, + access_all_engines: tokenType === ADMIN ? false : activeApiToken.access_all_engines, + engines: tokenType === ADMIN ? [] : activeApiToken.engines, + write: tokenType === PRIVATE, + read: tokenType === PRIVATE, + type: tokenType, + }), + toggleCredentialsForm: (_, activeApiToken) => activeApiToken, + }, + ], + activeApiTokenRawName: [ + '', + { + setTokenName: (_, activeApiTokenRawName) => activeApiTokenRawName, + toggleCredentialsForm: (activeApiTokenRawName, activeApiToken) => + activeApiToken.name || activeApiTokenRawName, + hideCredentialsForm: () => '', + onApiTokenCreateSuccess: () => '', + onApiTokenUpdateSuccess: () => '', + }, + ], + activeApiTokenIsExisting: [ + false, + { + toggleCredentialsForm: (_, activeApiToken) => !!activeApiToken.id, + }, + ], + showCredentialsForm: [ + false, + { + toggleCredentialsForm: (showCredentialsForm) => !showCredentialsForm, + hideCredentialsForm: () => false, + onApiTokenCreateSuccess: () => false, + onApiTokenUpdateSuccess: () => false, + }, + ], + formErrors: [ + [], + { + onApiTokenError: (_, formErrors) => formErrors, + onApiTokenCreateSuccess: () => [], + toggleCredentialsForm: () => [], + resetCredentials: () => [], + }, + ], + }), + selectors: ({ selectors }) => ({ + // TODO fullEngineAccessChecked from ent-search + dataLoading: [ + () => [selectors.isCredentialsDetailsComplete, selectors.isCredentialsDataComplete], + (isCredentialsDetailsComplete, isCredentialsDataComplete) => { + return isCredentialsDetailsComplete === false || isCredentialsDataComplete === false; + }, + ], + }), + listeners: ({ actions, values }) => ({ + initializeCredentialsData: () => { + actions.fetchCredentials(); + actions.fetchDetails(); + }, + fetchCredentials: async (page = 1) => { + try { + const { http } = HttpLogic.values; + const query = { 'page[current]': page }; + const response = await http.get('/api/app_search/credentials', { query }); + actions.setCredentialsData(response.meta, response.results); + } catch (e) { + flashAPIErrors(e); + } + }, + fetchDetails: async () => { + try { + const { http } = HttpLogic.values; + const response = await http.get('/api/app_search/credentials/details'); + + actions.setCredentialsDetails(response); + } catch (e) { + flashAPIErrors(e); + } + }, + deleteApiKey: async (tokenName) => { + try { + const { http } = HttpLogic.values; + await http.delete(`/api/app_search/credentials/${tokenName}`); + + actions.onApiKeyDelete(tokenName); + } catch (e) { + flashAPIErrors(e); + } + }, + // TODO onApiTokenChange from ent-search + // TODO onEngineSelect from ent-search + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts new file mode 100644 index 0000000000000..9b09bd13a9086 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts @@ -0,0 +1,22 @@ +/* + * 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 { IEngine } from '../../types'; + +export interface ICredentialsDetails { + engines: IEngine[]; +} + +export interface IApiToken { + access_all_engines?: boolean; + key?: string; + engines?: string[]; + id?: number; + name: string; + read?: boolean; + type: string; + write?: boolean; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts index 3cabc1051c74a..568a0a3365982 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/types.ts @@ -6,3 +6,10 @@ export * from '../../../common/types/app_search'; export { IRole, TRole, TAbility } from './utils/role'; + +export interface IEngine { + name: string; + type: string; + language: string; + result_fields: object[]; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/format_api_name/index.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/format_api_name/index.test.ts new file mode 100644 index 0000000000000..352ff237e4f08 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/format_api_name/index.test.ts @@ -0,0 +1,25 @@ +/* + * 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 { formatApiName } from '.'; + +describe('formatApiName', () => { + it('replaces non-alphanumeric characters with dashes', () => { + expect(formatApiName('f1 &&o$ 1 2 *&%da')).toEqual('f1-o-1-2-da'); + }); + + it('strips leading and trailing non-alphanumeric characters', () => { + expect(formatApiName('$$hello world**')).toEqual('hello-world'); + }); + + it('strips leading and trailing whitespace', () => { + expect(formatApiName(' test ')).toEqual('test'); + }); + + it('lowercases text', () => { + expect(formatApiName('SomeName')).toEqual('somename'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/format_api_name/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/format_api_name/index.ts new file mode 100644 index 0000000000000..cd1b1cfe15637 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/format_api_name/index.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +export const formatApiName = (rawName: string) => + rawName + .trim() + .replace(/[^a-zA-Z0-9]+/g, '-') // Replace all special/non-alphanumerical characters with dashes + .replace(/^[-]+|[-]+$/g, '') // Strip all leading and trailing dashes + .toLowerCase(); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts index 000e6d63b5999..6b5f4a05b3aa6 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.test.ts @@ -25,41 +25,6 @@ describe('credentials routes', () => { it('creates a request handler', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/as/credentials/collection', - hasValidData: expect.any(Function), - }); - }); - - describe('hasValidData', () => { - it('should correctly validate that a response has data', () => { - const response = { - meta: { - page: { - current: 1, - total_pages: 1, - total_results: 1, - size: 25, - }, - }, - results: [ - { - id: 'loco_moco_account_id:5f3575de2b76ff13405f3155|name:asdfasdf', - key: 'search-fe49u2z8d5gvf9s4ekda2ad4', - name: 'asdfasdf', - type: 'search', - access_all_engines: true, - }, - ], - }; - - expect(mockRequestHandler.hasValidData(response)).toBe(true); - }); - - it('should correctly validate that a response does not have data', () => { - const response = { - foo: 'bar', - }; - - expect(mockRequestHandler.hasValidData(response)).toBe(false); }); }); @@ -75,4 +40,52 @@ describe('credentials routes', () => { }); }); }); + + describe('GET /api/app_search/credentials/details', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'get', payload: 'query' }); + + registerCredentialsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/credentials/details', + }); + }); + }); + + describe('DELETE /api/app_search/credentials/{name}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ method: 'delete', payload: 'params' }); + + registerCredentialsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + const mockRequest = { + params: { + name: 'abc123', + }, + }; + + mockRouter.callRoute(mockRequest); + + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/credentials/abc123', + }); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts index 432f54c8e5b1c..0f2c1133192c5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/credentials.ts @@ -8,25 +8,6 @@ import { schema } from '@kbn/config-schema'; import { IRouteDependencies } from '../../plugin'; -interface ICredential { - id: string; - key: string; - name: string; - type: string; - access_all_engines: boolean; -} -interface ICredentialsResponse { - results: ICredential[]; - meta?: { - page?: { - current: number; - total_results: number; - total_pages: number; - size: number; - }; - }; -} - export function registerCredentialsRoutes({ router, enterpriseSearchRequestHandler, @@ -42,9 +23,30 @@ export function registerCredentialsRoutes({ }, enterpriseSearchRequestHandler.createRequest({ path: '/as/credentials/collection', - hasValidData: (body?: ICredentialsResponse) => { - return Array.isArray(body?.results) && typeof body?.meta?.page?.total_results === 'number'; - }, }) ); + router.get( + { + path: '/api/app_search/credentials/details', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/credentials/details', + }) + ); + router.delete( + { + path: '/api/app_search/credentials/{name}', + validate: { + params: schema.object({ + name: schema.string(), + }), + }, + }, + async (context, request, response) => { + return enterpriseSearchRequestHandler.createRequest({ + path: `/as/credentials/${request.params.name}`, + })(context, request, response); + } + ); }