[Behavioral Analytics] Add api key modal in integration page (#154738)

Add API Key modal to integration page.

<img width="812" alt="Screenshot 2023-04-11 at 16 13 40"
src="https://user-images.githubusercontent.com/49480/231208531-920272be-bd48-4bb0-971d-c740a2e8b2e2.png">
<img width="882" alt="Screenshot 2023-04-11 at 16 13 46"
src="https://user-images.githubusercontent.com/49480/231208527-d8fb2a41-ad9c-4fd3-b7c9-a1cd1de7fe02.png">
<img width="820" alt="Screenshot 2023-04-11 at 16 13 53"
src="https://user-images.githubusercontent.com/49480/231208520-e9ec8ae3-e3a7-4dc3-8e6b-f6f339e9f084.png">
<img width="991" alt="Screenshot 2023-04-11 at 16 13 59"
src="https://user-images.githubusercontent.com/49480/231208512-791f6a8c-e8ad-4960-840c-8a2bea5ab556.png">

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Joseph McElroy 2023-04-12 12:02:48 +01:00 committed by GitHub
parent 0790ec0da2
commit 57d6f413a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 782 additions and 6 deletions

View file

@ -0,0 +1,54 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { mockHttpValues } from '../../../__mocks__/kea_logic';
import { nextTick } from '@kbn/test-jest-helpers';
import { generateAnalyticsApiKey } from './generate_analytics_api_key_logic';
describe('GenerateAnalyticsApiKeyLogic', () => {
const { http } = mockHttpValues;
beforeEach(() => {
jest.clearAllMocks();
});
describe('GenerateAnalyticsApiKeyLogic', () => {
it('calls correct api', async () => {
const promise = Promise.resolve({
apiKey: {
api_key: 'api_key',
encoded: 'encoded',
id: 'id',
name: 'name',
},
});
http.post.mockReturnValue(promise);
const result = generateAnalyticsApiKey({
collectionName: 'puggles',
keyName: 'puggles read only key',
});
await nextTick();
expect(http.post).toHaveBeenCalledWith(
'/internal/enterprise_search/analytics/collections/puggles/api_key',
{
body: JSON.stringify({
keyName: 'puggles read only key',
}),
}
);
await expect(result).resolves.toEqual({
apiKey: {
api_key: 'api_key',
encoded: 'encoded',
id: 'id',
name: 'name',
},
});
});
});
});

View file

@ -0,0 +1,39 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createApiLogic } from '../../../shared/api_logic/create_api_logic';
import { HttpLogic } from '../../../shared/http';
interface APIKeyResponse {
apiKey: {
api_key: string;
encoded: string;
id: string;
name: string;
};
}
export const generateAnalyticsApiKey = async ({
collectionName,
keyName,
}: {
collectionName: string;
keyName: string;
}) => {
const route = `/internal/enterprise_search/analytics/collections/${collectionName}/api_key`;
return await HttpLogic.values.http.post<APIKeyResponse>(route, {
body: JSON.stringify({
keyName,
}),
});
};
export const generateAnalyticsApiKeyLogic = createApiLogic(
['generate_analytics_api_key_logic'],
generateAnalyticsApiKey
);

View file

@ -6,6 +6,7 @@
*/
import '../../../../__mocks__/shallow_useeffect.mock';
import '../../../../__mocks__/kea_logic';
import React from 'react';

View file

@ -5,23 +5,39 @@
* 2.0.
*/
import React from 'react';
import React, { useState } from 'react';
import { EuiSpacer, EuiSteps, EuiTab, EuiTabs } from '@elastic/eui';
import { useValues } from 'kea';
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiSpacer,
EuiSteps,
EuiTab,
EuiTabs,
EuiLink,
EuiText,
} from '@elastic/eui';
import { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
import { i18n } from '@kbn/i18n';
import { AnalyticsCollection } from '../../../../../../common/types/analytics';
import { docLinks } from '../../../../shared/doc_links';
import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url';
import { KibanaLogic } from '../../../../shared/kibana';
import { EnterpriseSearchAnalyticsPageTemplate } from '../../layout/page_template';
import { javascriptClientEmbedSteps } from './analytics_collection_integrate_javascript_client_embed';
import { javascriptEmbedSteps } from './analytics_collection_integrate_javascript_embed';
import { searchUIEmbedSteps } from './analytics_collection_integrate_searchui';
import { GenerateAnalyticsApiKeyModal } from './api_key_modal/generate_analytics_api_key_modal';
import { GenerateApiKeyModalLogic } from './api_key_modal/generate_analytics_api_key_modal.logic';
interface AnalyticsCollectionIntegrateProps {
analyticsCollection: AnalyticsCollection;
@ -35,13 +51,93 @@ export interface AnalyticsConfig {
endpoint: string;
}
const apiKeyStep = (
openApiKeyModal: () => void,
navigateToUrl: typeof KibanaLogic.values.navigateToUrl
): EuiContainedStepProps => ({
title: i18n.translate(
'xpack.enterpriseSearch.analytics.collections.collectionsView.apiKey.title',
{
defaultMessage: 'Create an API Key',
}
),
children: (
<>
<EuiText>
<p>
{i18n.translate(
'xpack.enterpriseSearch.analytics.collectionsView.integration.apiKeyStep.apiKeyWarning',
{
defaultMessage:
"Elastic does not store API keys. Once generated, you'll only be able to view the key one time. Make sure you save it somewhere secure. If you lose access to it you'll need to generate a new API key from this screen.",
}
)}{' '}
<EuiLink
href={docLinks.apiKeys}
data-telemetry-id="entSearchContent-analytics-apiKey-learnMoreLink"
external
target="_blank"
>
{i18n.translate(
'xpack.enterpriseSearch.analytics.collectionsView.integration.apiKeyStep.learnMoreLink',
{
defaultMessage: 'Learn more about API keys.',
}
)}
</EuiLink>
</p>
</EuiText>
<EuiSpacer size="l" />
<EuiFlexGroup gutterSize="s">
<EuiFlexItem grow={false}>
<EuiButton
iconSide="left"
iconType="plusInCircleFilled"
onClick={openApiKeyModal}
data-telemetry-id="entSearchContent-analytics-apiKey-createApiKeyButton"
>
{i18n.translate(
'xpack.enterpriseSearch.analytics.collectionsView.integration.apiKeyStep.createAPIKeyButton',
{
defaultMessage: 'Create API Key',
}
)}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
iconSide="left"
iconType="popout"
data-telemetry-id="entSearchContent-analytics-apiKey-viewKeysButton"
onClick={() =>
navigateToUrl('/app/management/security/api_keys', {
shouldNotCreateHref: true,
})
}
>
{i18n.translate(
'xpack.enterpriseSearch.analytics.collectionsView.integration.apiKeyStep.viewKeysButton',
{
defaultMessage: 'View Keys',
}
)}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
),
});
export const AnalyticsCollectionIntegrateView: React.FC<AnalyticsCollectionIntegrateProps> = ({
analyticsCollection,
}) => {
const [selectedTab, setSelectedTab] = React.useState<TabKey>('javascriptEmbed');
const [selectedTab, setSelectedTab] = useState<TabKey>('javascriptEmbed');
const [apiKeyModelOpen, setApiKeyModalOpen] = useState<boolean>(false);
const { navigateToUrl } = useValues(KibanaLogic);
const { apiKey } = useValues(GenerateApiKeyModalLogic);
const analyticsConfig: AnalyticsConfig = {
apiKey: '########',
apiKey: apiKey || '########',
collectionName: analyticsCollection?.name,
endpoint: getEnterpriseSearchUrl(),
};
@ -80,9 +176,11 @@ export const AnalyticsCollectionIntegrateView: React.FC<AnalyticsCollectionInteg
},
];
const apiKeyStepGuide = apiKeyStep(() => setApiKeyModalOpen(true), navigateToUrl);
const steps: Record<TabKey, EuiContainedStepProps[]> = {
javascriptClientEmbed: javascriptClientEmbedSteps(analyticsConfig),
javascriptEmbed: javascriptEmbedSteps(webClientSrc, analyticsConfig),
javascriptClientEmbed: [apiKeyStepGuide, ...javascriptClientEmbedSteps(analyticsConfig)],
javascriptEmbed: [apiKeyStepGuide, ...javascriptEmbedSteps(webClientSrc, analyticsConfig)],
searchuiEmbed: searchUIEmbedSteps(setSelectedTab),
};
@ -111,6 +209,14 @@ export const AnalyticsCollectionIntegrateView: React.FC<AnalyticsCollectionInteg
}}
>
<>
{apiKeyModelOpen ? (
<GenerateAnalyticsApiKeyModal
collectionName={analyticsCollection.name}
onClose={() => {
setApiKeyModalOpen(false);
}}
/>
) : null}
<EuiTabs>
{tabs.map((tab) => (
<EuiTab

View file

@ -0,0 +1,144 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { LogicMounter } from '../../../../../__mocks__/kea_logic';
import { Status } from '../../../../../../../common/types/api';
import { generateAnalyticsApiKeyLogic } from '../../../../api/generate_analytics_api_key/generate_analytics_api_key_logic';
import { GenerateApiKeyModalLogic } from './generate_analytics_api_key_modal.logic';
const DEFAULT_VALUES = {
apiKey: '',
data: undefined,
isLoading: false,
isSuccess: false,
keyName: '',
status: Status.IDLE,
};
describe('GenerateAnalyticsApiKeyModal Logic', () => {
const { mount: apiLogicMount } = new LogicMounter(GenerateApiKeyModalLogic);
const { mount } = new LogicMounter(GenerateApiKeyModalLogic);
beforeEach(() => {
jest.clearAllMocks();
apiLogicMount();
mount();
});
it('has expected default values', () => {
expect(GenerateApiKeyModalLogic.values).toEqual(DEFAULT_VALUES);
});
describe('actions', () => {
describe('setKeyName', () => {
it('sets keyName value to the reducer', () => {
const keyName = 'test-key-name 8888*^7896&*^*&';
expect(GenerateApiKeyModalLogic.values).toEqual(DEFAULT_VALUES);
GenerateApiKeyModalLogic.actions.setKeyName(keyName);
expect(GenerateApiKeyModalLogic.values).toEqual({
...DEFAULT_VALUES,
keyName,
});
});
});
});
describe('reducers', () => {
describe('keyName', () => {
it('updates when setKeyName action is triggered', () => {
const keyName = 'test-key-name';
expect(GenerateApiKeyModalLogic.values).toEqual(DEFAULT_VALUES);
GenerateApiKeyModalLogic.actions.setKeyName(keyName);
expect(GenerateApiKeyModalLogic.values).toEqual({
...DEFAULT_VALUES,
keyName,
});
});
});
});
describe('selectors', () => {
describe('apiKey', () => {
it('updates when apiSuccess listener triggered', () => {
expect(GenerateApiKeyModalLogic.values).toEqual(DEFAULT_VALUES);
generateAnalyticsApiKeyLogic.actions.apiSuccess({
apiKey: {
api_key: 'some-api-key-123123',
encoded: 'encoded-api-key123123==',
id: 'api_key_id',
name: 'test-key-123',
},
});
expect(GenerateApiKeyModalLogic.values).toEqual({
apiKey: 'encoded-api-key123123==',
data: {
apiKey: {
api_key: 'some-api-key-123123',
encoded: 'encoded-api-key123123==',
id: 'api_key_id',
name: 'test-key-123',
},
},
isLoading: false,
isSuccess: true,
keyName: '',
status: Status.SUCCESS,
});
});
});
describe('isLoading', () => {
it('should update with API status', () => {
expect(GenerateApiKeyModalLogic.values).toEqual(DEFAULT_VALUES);
generateAnalyticsApiKeyLogic.actions.makeRequest({
collectionName: 'puggles',
keyName: 'test',
});
expect(GenerateApiKeyModalLogic.values).toEqual({
...DEFAULT_VALUES,
isLoading: true,
status: Status.LOADING,
});
});
});
describe('isSuccess', () => {
it('should update with API status', () => {
expect(GenerateApiKeyModalLogic.values).toEqual(DEFAULT_VALUES);
generateAnalyticsApiKeyLogic.actions.apiSuccess({
apiKey: {
api_key: 'some-api-key-123123',
encoded: 'encoded-api-key123123==',
id: 'api_key_id',
name: 'test-key-123',
},
});
expect(GenerateApiKeyModalLogic.values).toEqual({
apiKey: 'encoded-api-key123123==',
data: {
apiKey: {
api_key: 'some-api-key-123123',
encoded: 'encoded-api-key123123==',
id: 'api_key_id',
name: 'test-key-123',
},
},
isLoading: false,
isSuccess: true,
keyName: '',
status: Status.SUCCESS,
});
});
});
});
});

View file

@ -0,0 +1,50 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { kea, MakeLogicType } from 'kea';
import { Status } from '../../../../../../../common/types/api';
import { generateAnalyticsApiKeyLogic } from '../../../../api/generate_analytics_api_key/generate_analytics_api_key_logic';
interface GenerateApiKeyModalActions {
setKeyName(keyName: string): { keyName: string };
}
interface GenerateApiKeyModalValues {
apiKey: string;
data: typeof generateAnalyticsApiKeyLogic.values.data;
isLoading: boolean;
isSuccess: boolean;
keyName: string;
status: typeof generateAnalyticsApiKeyLogic.values.status;
}
export const GenerateApiKeyModalLogic = kea<
MakeLogicType<GenerateApiKeyModalValues, GenerateApiKeyModalActions>
>({
actions: {
setKeyName: (keyName) => ({ keyName }),
},
connect: {
values: [generateAnalyticsApiKeyLogic, ['data', 'status']],
},
path: ['enterprise_search', 'analytics', 'integration', 'generate_api_key_modal'],
reducers: () => ({
keyName: [
'',
{
setKeyName: (_, { keyName }) => keyName,
},
],
}),
selectors: ({ selectors }) => ({
apiKey: [() => [selectors.data], (data) => data?.apiKey?.encoded || ''],
isLoading: [() => [selectors.status], (status) => status === Status.LOADING],
isSuccess: [() => [selectors.status], (status) => status === Status.SUCCESS],
}),
});

View file

@ -0,0 +1,94 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { setMockValues, setMockActions } from '../../../../../__mocks__/kea_logic';
import React from 'react';
import { shallow, mount } from 'enzyme';
import { EuiModal, EuiFieldText, EuiCodeBlock } from '@elastic/eui';
const mockActions = { makeRequest: jest.fn(), setKeyName: jest.fn() };
const mockValues = { apiKey: '', isLoading: false, isSuccess: false, keyName: '' };
import { GenerateAnalyticsApiKeyModal } from './generate_analytics_api_key_modal';
const onCloseMock = jest.fn();
describe('GenerateAnalyticsApiKeyModal', () => {
beforeEach(() => {
jest.clearAllMocks();
setMockValues(mockValues);
setMockActions(mockActions);
});
it('renders the empty modal', () => {
const wrapper = shallow(
<GenerateAnalyticsApiKeyModal collectionName="puggles" onClose={onCloseMock} />
);
expect(wrapper.find(EuiModal)).toHaveLength(1);
wrapper.find(EuiModal).prop('onClose')();
expect(onCloseMock).toHaveBeenCalled();
});
describe('Modal content', () => {
it('renders API key name form', () => {
const wrapper = shallow(
<GenerateAnalyticsApiKeyModal collectionName="puggles" onClose={onCloseMock} />
);
expect(wrapper.find(EuiFieldText)).toHaveLength(1);
expect(wrapper.find('[data-test-subj="generateApiKeyButton"]')).toHaveLength(1);
});
it('pre-set the key name with collection name', () => {
mount(<GenerateAnalyticsApiKeyModal collectionName="puggles" onClose={onCloseMock} />);
expect(mockActions.setKeyName).toHaveBeenCalledWith('puggles API key');
});
it('sets keyName name on form', () => {
const wrapper = shallow(
<GenerateAnalyticsApiKeyModal collectionName="puggles" onClose={onCloseMock} />
);
const textField = wrapper.find(EuiFieldText);
expect(textField).toHaveLength(1);
textField.simulate('change', { currentTarget: { value: 'changeEvent-key-name' } });
expect(mockActions.setKeyName).toHaveBeenCalledWith('changeEvent-key-name');
});
it('should trigger api call from the form', () => {
setMockValues({ ...mockValues, collectionName: 'test-123', keyName: ' with-spaces ' });
const wrapper = shallow(
<GenerateAnalyticsApiKeyModal collectionName="puggles" onClose={onCloseMock} />
);
expect(wrapper.find(EuiFieldText)).toHaveLength(1);
wrapper.find('[data-test-subj="generateApiKeyButton"]').simulate('click');
expect(mockActions.makeRequest).toHaveBeenCalledWith({
collectionName: 'puggles',
keyName: 'with-spaces',
});
});
it('renders created API key results', () => {
setMockValues({
...mockValues,
apiKey: 'apiKeyFromBackend123123==',
collectionName: 'test-123',
isSuccess: true,
keyName: 'keyname',
});
const wrapper = shallow(
<GenerateAnalyticsApiKeyModal collectionName="puggles" onClose={onCloseMock} />
);
expect(wrapper.find(EuiFieldText)).toHaveLength(0);
expect(wrapper.find('[data-test-subj="generateApiKeyButton"]')).toHaveLength(0);
expect(wrapper.find(EuiCodeBlock)).toHaveLength(1);
expect(wrapper.find(EuiCodeBlock).children().text()).toEqual('apiKeyFromBackend123123==');
});
});
});

View file

@ -0,0 +1,196 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect } from 'react';
import { useValues, useActions } from 'kea';
import {
EuiModal,
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiModalFooter,
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiButton,
EuiButtonEmpty,
EuiButtonIcon,
EuiFieldText,
EuiFormRow,
EuiText,
EuiSpacer,
EuiFormLabel,
EuiCodeBlock,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { generateAnalyticsApiKeyLogic } from '../../../../api/generate_analytics_api_key/generate_analytics_api_key_logic';
import { GenerateApiKeyModalLogic } from './generate_analytics_api_key_modal.logic';
interface GenerateAnalyticsApiKeyModalProps {
collectionName: string;
onClose(): void;
}
export const GenerateAnalyticsApiKeyModal: React.FC<GenerateAnalyticsApiKeyModalProps> = ({
collectionName,
onClose,
}) => {
const { keyName, apiKey, isLoading, isSuccess } = useValues(GenerateApiKeyModalLogic);
const { setKeyName } = useActions(GenerateApiKeyModalLogic);
const { makeRequest } = useActions(generateAnalyticsApiKeyLogic);
useEffect(() => {
setKeyName(`${collectionName} API key`);
}, [collectionName]);
return (
<EuiModal onClose={onClose}>
<EuiModalHeader>
<EuiModalHeaderTitle>
{i18n.translate(
'xpack.enterpriseSearch.content.analytics.api.generateAnalyticsApiKeyModal.title',
{
defaultMessage: 'Create analytics API Key',
}
)}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<>
<EuiPanel hasShadow={false} color="primary">
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFlexGroup direction="row" alignItems="flexEnd">
{!isSuccess ? (
<>
<EuiFlexItem>
<EuiFormRow label="Name your API key" fullWidth>
<EuiFieldText
data-telemetry-id="entSearchContent-analyticss-api-generateAnalyticsApiKeyModal-editName"
fullWidth
placeholder="Type a name for your API key"
onChange={(event) => setKeyName(event.currentTarget.value)}
value={keyName}
isLoading={isLoading}
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-telemetry-id="entSearchContent-analyticss-api-generateAnalyticsApiKeyModal-generateApiKeyButton"
data-test-subj="generateApiKeyButton"
iconSide="left"
iconType="plusInCircle"
fill
onClick={() => {
makeRequest({
collectionName,
keyName: keyName.trim(),
});
}}
disabled={keyName.trim().length <= 0}
>
{i18n.translate(
'xpack.enterpriseSearch.content.analytics.api.generateAnalyticsApiKeyModal.generateButton',
{
defaultMessage: 'Generate key',
}
)}
</EuiButton>
</EuiFlexItem>
</>
) : (
<EuiFlexItem>
<EuiFormLabel>{keyName}</EuiFormLabel>
<EuiSpacer size="xs" />
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
<EuiCodeBlock
aria-label={keyName}
fontSize="m"
paddingSize="m"
color="dark"
isCopyable
>
{apiKey}
</EuiCodeBlock>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon
data-telemetry-id="entSearchContent-analyticss-api-generateAnalyticsApiKeyModal-csvDownloadButton"
aria-label={i18n.translate(
'xpack.enterpriseSearch.content.analytics.api.generateAnalyticsApiKeyModal.csvDownloadButton',
{ defaultMessage: 'Download API key' }
)}
iconType="download"
href={encodeURI(`data:text/csv;charset=utf-8,${apiKey}`)}
download={`${keyName}.csv`}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="row">
<EuiFlexItem>
<EuiText size="s" color="#006bb8">
<p>
{i18n.translate(
'xpack.enterpriseSearch.content.analytics.api.generateAnalyticsApiKeyModal.apiKeyWarning',
{
defaultMessage:
"Elastic does not store API keys. Once generated, you'll only be able to view the key one time. Make sure you save it somewhere secure. If you lose access to it you'll need to generate a new API key from this screen.",
}
)}
</p>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
</>
</EuiModalBody>
<EuiModalFooter>
{apiKey ? (
<EuiButton
data-telemetry-id="entSearchContent-analyticss-api-generateAnalyticsApiKeyModal-done"
fill
onClick={onClose}
>
{i18n.translate(
'xpack.enterpriseSearch.content.analytics.api.generateAnalyticsApiKeyModal.done',
{
defaultMessage: 'Done',
}
)}
</EuiButton>
) : (
<EuiButtonEmpty
data-telemetry-id="entSearchContent-analyticss-api-generateAnalyticsApiKeyModal-cancel"
onClick={onClose}
>
{i18n.translate(
'xpack.enterpriseSearch.content.analytics.api.generateAnalyticsApiKeyModal.cancel',
{
defaultMessage: 'Cancel',
}
)}
</EuiButtonEmpty>
)}
</EuiModalFooter>
</EuiModal>
);
};

View file

@ -0,0 +1,46 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IScopedClusterClient } from '@kbn/core/server';
import { createApiKey } from './create_api_key';
describe('createApiKey lib function', () => {
const createResponse = {
api_key: 'ui2lp2axTNmsyakw9tvNnw',
encoded: 'VnVhQ2ZHY0JDZGJrUW0tZTVhT3g6dWkybHAyYXhUTm1zeWFrdzl0dk5udw==',
id: 'VuaCfGcBCdbkQm-e5aOx',
name: 'website key',
};
const mockClient = {
asCurrentUser: {
security: {
createApiKey: jest.fn().mockReturnValue(createResponse),
},
},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should create an api key via the security plugin', async () => {
await expect(
createApiKey(mockClient as unknown as IScopedClusterClient, 'website', 'website key')
).resolves.toEqual(createResponse);
expect(mockClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({
name: 'website key',
role_descriptors: {
'website-key-role': {
cluster: ['post_behavioral_analytics_event'],
},
},
});
});
});

View file

@ -0,0 +1,19 @@
/*
* 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; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
export const createApiKey = async (client: IScopedClusterClient, name: string, keyName: string) => {
return await client.asCurrentUser.security.createApiKey({
name: keyName,
role_descriptors: {
[`${name}-key-role`]: {
cluster: ['post_behavioral_analytics_event'],
},
},
});
};

View file

@ -15,6 +15,7 @@ import { i18n } from '@kbn/i18n';
import { ErrorCode } from '../../../common/types/error_codes';
import { addAnalyticsCollection } from '../../lib/analytics/add_analytics_collection';
import { analyticsEventsIndexExists } from '../../lib/analytics/analytics_events_index_exists';
import { createApiKey } from '../../lib/analytics/create_api_key';
import { deleteAnalyticsCollectionById } from '../../lib/analytics/delete_analytics_collection';
import { fetchAnalyticsCollections } from '../../lib/analytics/fetch_analytics_collection';
import { RouteDependencies } from '../../plugin';
@ -84,6 +85,32 @@ export function registerAnalyticsRoutes({
})
);
router.post(
{
path: '/internal/enterprise_search/analytics/collections/{name}/api_key',
validate: {
body: schema.object({
keyName: schema.string(),
}),
params: schema.object({
name: schema.string(),
}),
},
},
elasticsearchErrorHandler(log, async (context, request, response) => {
const collectionName = decodeURIComponent(request.params.name);
const { keyName } = request.body;
const { client } = (await context.core).elasticsearch;
const apiKey = await createApiKey(client, collectionName, keyName);
return response.ok({
body: { apiKey },
headers: { 'content-type': 'application/json' },
});
})
);
router.post(
{
path: '/internal/enterprise_search/analytics/collections',