Skip to content

Commit 7508321

Browse files
author
Constance
authored
[App Search] Credentials: implement working flyout form (#81541)
* Add key name field * Add key type field * Add key read/write fields * Add key engine access / selection * Add key update warning callout * Update flyout body with form components * [PR feedback] i18n - change appSearch.tokens to appSearch.credentials * [PR feedback] Remove unnecessary conditional
1 parent 9f7ccc6 commit 7508321

File tree

15 files changed

+855
-4
lines changed

15 files changed

+855
-4
lines changed

x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,5 @@ export const TOKEN_TYPE_INFO = [
9393
];
9494

9595
export const FLYOUT_ARIA_LABEL_ID = 'credentialsFlyoutTitle';
96+
97+
export const DOCS_HREF = 'https://www.elastic.co/guide/en/app-search/current/authentication.html';

x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,98 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7+
import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock';
8+
79
import React from 'react';
810
import { shallow } from 'enzyme';
9-
import { EuiFlyoutBody } from '@elastic/eui';
11+
import { EuiFlyoutBody, EuiForm } from '@elastic/eui';
12+
13+
import { ApiTokenTypes } from '../constants';
14+
import { defaultApiToken } from '../credentials_logic';
1015

16+
import {
17+
FormKeyName,
18+
FormKeyType,
19+
FormKeyReadWriteAccess,
20+
FormKeyEngineAccess,
21+
FormKeyUpdateWarning,
22+
} from './form_components';
1123
import { CredentialsFlyoutBody } from './body';
1224

1325
describe('CredentialsFlyoutBody', () => {
26+
const values = {
27+
activeApiToken: defaultApiToken,
28+
activeApiTokenExists: false,
29+
};
30+
const actions = {
31+
onApiTokenChange: jest.fn(),
32+
};
33+
34+
beforeEach(() => {
35+
jest.clearAllMocks();
36+
setMockValues(values);
37+
setMockActions(actions);
38+
});
39+
1440
it('renders', () => {
1541
const wrapper = shallow(<CredentialsFlyoutBody />);
42+
1643
expect(wrapper.find(EuiFlyoutBody)).toHaveLength(1);
44+
expect(wrapper.find(EuiForm)).toHaveLength(1);
45+
});
46+
47+
it('shows the expected form components on default private key creation', () => {
48+
const wrapper = shallow(<CredentialsFlyoutBody />);
49+
50+
expect(wrapper.find(FormKeyName)).toHaveLength(1);
51+
expect(wrapper.find(FormKeyType)).toHaveLength(1);
52+
expect(wrapper.find(FormKeyReadWriteAccess)).toHaveLength(1);
53+
expect(wrapper.find(FormKeyEngineAccess)).toHaveLength(1);
54+
expect(wrapper.find(FormKeyUpdateWarning)).toHaveLength(0);
55+
});
56+
57+
it('does not show read-write access options for search keys', () => {
58+
setMockValues({
59+
...values,
60+
activeApiToken: {
61+
...defaultApiToken,
62+
type: ApiTokenTypes.Search,
63+
},
64+
});
65+
const wrapper = shallow(<CredentialsFlyoutBody />);
66+
67+
expect(wrapper.find(FormKeyReadWriteAccess)).toHaveLength(0);
68+
expect(wrapper.find(FormKeyEngineAccess)).toHaveLength(1);
69+
});
70+
71+
it('does not show read-write or engine access options for admin keys', () => {
72+
setMockValues({
73+
...values,
74+
activeApiToken: {
75+
...defaultApiToken,
76+
type: ApiTokenTypes.Admin,
77+
},
78+
});
79+
const wrapper = shallow(<CredentialsFlyoutBody />);
80+
81+
expect(wrapper.find(FormKeyReadWriteAccess)).toHaveLength(0);
82+
expect(wrapper.find(FormKeyEngineAccess)).toHaveLength(0);
83+
});
84+
85+
it('shows a warning if updating an existing key', () => {
86+
setMockValues({ ...values, activeApiTokenExists: true });
87+
const wrapper = shallow(<CredentialsFlyoutBody />);
88+
89+
expect(wrapper.find(FormKeyUpdateWarning)).toHaveLength(1);
90+
});
91+
92+
it('calls onApiTokenChange on form submit', () => {
93+
const wrapper = shallow(<CredentialsFlyoutBody />);
94+
95+
const preventDefault = jest.fn();
96+
wrapper.find(EuiForm).simulate('submit', { preventDefault });
97+
98+
expect(preventDefault).toHaveBeenCalled();
99+
expect(actions.onApiTokenChange).toHaveBeenCalled();
17100
});
18101
});

x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,41 @@
55
*/
66

77
import React from 'react';
8-
import { EuiFlyoutBody } from '@elastic/eui';
8+
import { useValues, useActions } from 'kea';
9+
import { EuiFlyoutBody, EuiForm } from '@elastic/eui';
910

1011
import { FlashMessages } from '../../../../shared/flash_messages';
12+
import { CredentialsLogic } from '../credentials_logic';
13+
import { ApiTokenTypes } from '../constants';
14+
15+
import {
16+
FormKeyName,
17+
FormKeyType,
18+
FormKeyReadWriteAccess,
19+
FormKeyEngineAccess,
20+
FormKeyUpdateWarning,
21+
} from './form_components';
1122

1223
export const CredentialsFlyoutBody: React.FC = () => {
24+
const { onApiTokenChange } = useActions(CredentialsLogic);
25+
const { activeApiToken, activeApiTokenExists } = useValues(CredentialsLogic);
26+
1327
return (
1428
<EuiFlyoutBody>
1529
<FlashMessages />
16-
Details go here
30+
<EuiForm
31+
onSubmit={(e) => {
32+
e.preventDefault();
33+
onApiTokenChange();
34+
}}
35+
component="form"
36+
>
37+
<FormKeyName />
38+
<FormKeyType />
39+
{activeApiToken.type === ApiTokenTypes.Private && <FormKeyReadWriteAccess />}
40+
{activeApiToken.type !== ApiTokenTypes.Admin && <FormKeyEngineAccess />}
41+
</EuiForm>
42+
{activeApiTokenExists && <FormKeyUpdateWarning />}
1743
</EuiFlyoutBody>
1844
);
1945
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export { FormKeyName } from './key_name';
8+
export { FormKeyType } from './key_type';
9+
export { FormKeyReadWriteAccess } from './key_read_write_access';
10+
export { FormKeyEngineAccess } from './key_engine_access';
11+
export { FormKeyUpdateWarning } from './key_update_warning';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock';
8+
9+
import React from 'react';
10+
import { shallow } from 'enzyme';
11+
import { EuiRadio, EuiCheckbox } from '@elastic/eui';
12+
13+
import { FormKeyEngineAccess, EngineSelection } from './key_engine_access';
14+
15+
describe('FormKeyEngineAccess', () => {
16+
const values = {
17+
myRole: { canAccessAllEngines: true },
18+
fullEngineAccessChecked: true,
19+
};
20+
const actions = {
21+
setAccessAllEngines: jest.fn(),
22+
};
23+
24+
beforeEach(() => {
25+
jest.clearAllMocks();
26+
setMockValues(values);
27+
setMockActions(actions);
28+
});
29+
30+
it('renders', () => {
31+
const wrapper = shallow(<FormKeyEngineAccess />);
32+
33+
expect(wrapper.find(EuiRadio)).toHaveLength(2);
34+
expect(wrapper.find(EngineSelection)).toHaveLength(0);
35+
});
36+
37+
it('hides the full access radio option if the user does not have access to all engines', () => {
38+
setMockValues({
39+
...values,
40+
myRole: { canAccessAllEngines: false },
41+
});
42+
const wrapper = shallow(<FormKeyEngineAccess />);
43+
44+
expect(wrapper.find('#all_engines').prop('hidden')).toEqual(true);
45+
});
46+
47+
it('controls the checked values for access radios', () => {
48+
setMockValues({
49+
...values,
50+
fullEngineAccessChecked: true,
51+
});
52+
const wrapper = shallow(<FormKeyEngineAccess />);
53+
54+
expect(wrapper.find('#all_engines').prop('checked')).toEqual(true);
55+
expect(wrapper.find('#all_engines').prop('value')).toEqual('true');
56+
expect(wrapper.find('#specific_engines').prop('checked')).toEqual(false);
57+
expect(wrapper.find('#specific_engines').prop('value')).toEqual('false');
58+
59+
setMockValues({
60+
...values,
61+
fullEngineAccessChecked: false,
62+
});
63+
wrapper.setProps({}); // Re-render
64+
65+
expect(wrapper.find('#all_engines').prop('checked')).toEqual(false);
66+
expect(wrapper.find('#all_engines').prop('value')).toEqual('false');
67+
expect(wrapper.find('#specific_engines').prop('checked')).toEqual(true);
68+
expect(wrapper.find('#specific_engines').prop('value')).toEqual('true');
69+
});
70+
71+
it('calls setAccessAllEngines when the radios are changed', () => {
72+
const wrapper = shallow(<FormKeyEngineAccess />);
73+
74+
wrapper.find('#all_engines').simulate('change');
75+
expect(actions.setAccessAllEngines).toHaveBeenCalledWith(true);
76+
77+
wrapper.find('#specific_engines').simulate('change');
78+
expect(actions.setAccessAllEngines).toHaveBeenCalledWith(false);
79+
});
80+
81+
it('displays the engine selection panel if the limited access radio is selected', () => {
82+
setMockValues({
83+
...values,
84+
fullEngineAccessChecked: false,
85+
});
86+
const wrapper = shallow(<FormKeyEngineAccess />);
87+
88+
expect(wrapper.find(EngineSelection)).toHaveLength(1);
89+
});
90+
});
91+
92+
describe('EngineSelection', () => {
93+
const values = {
94+
activeApiToken: { engines: [] },
95+
engines: [{ name: 'engine1' }, { name: 'engine2' }, { name: 'engine3' }],
96+
};
97+
const actions = {
98+
onEngineSelect: jest.fn(),
99+
};
100+
101+
beforeEach(() => {
102+
jest.clearAllMocks();
103+
setMockValues(values);
104+
setMockActions(actions);
105+
});
106+
107+
it('renders', () => {
108+
const wrapper = shallow(<EngineSelection />);
109+
110+
expect(wrapper.find('h4').text()).toEqual('Select Engines');
111+
expect(wrapper.find(EuiCheckbox)).toHaveLength(3);
112+
expect(wrapper.find(EuiCheckbox).first().prop('label')).toEqual('engine1');
113+
});
114+
115+
it('controls the engines checked state', () => {
116+
setMockValues({
117+
...values,
118+
activeApiToken: { engines: ['engine3'] },
119+
});
120+
const wrapper = shallow(<EngineSelection />);
121+
122+
expect(wrapper.find(EuiCheckbox).first().prop('checked')).toEqual(false);
123+
expect(wrapper.find(EuiCheckbox).last().prop('checked')).toEqual(true);
124+
});
125+
126+
it('calls onEngineSelect when the checkboxes are changed', () => {
127+
const wrapper = shallow(<EngineSelection />);
128+
129+
wrapper.find(EuiCheckbox).first().simulate('change');
130+
expect(actions.onEngineSelect).toHaveBeenCalledWith('engine1');
131+
132+
wrapper.find(EuiCheckbox).last().simulate('change');
133+
expect(actions.onEngineSelect).toHaveBeenCalledWith('engine3');
134+
});
135+
});

0 commit comments

Comments
 (0)