mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
0790ec0da2
commit
57d6f413a1
11 changed files with 782 additions and 6 deletions
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
);
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import '../../../../__mocks__/shallow_useeffect.mock';
|
||||
import '../../../../__mocks__/kea_logic';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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],
|
||||
}),
|
||||
});
|
|
@ -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==');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'],
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue