Skip to content

Commit 13d42a1

Browse files
ConstanceJasonStoltz
authored andcommitted
[App Search] Credentials: add FlashMessages, stub out credentials flyout (#81391)
* Added an empty Flyout * Refactor CredentialsFlyout to its own component folder + split out child sub components for easier testing/reading * Add initial FlashMessages setup - mostly just DELETE_MESSAGE currently, since that's what's already wired up - CREATE_MESSAGE and UPDATE_MESSAGE will be used in an upcoming commit + adds FlashMessages in flyout, which will show returned form errors from the API * Fix flash messages appearing on flyout open e.g. deletion success messages + incidental linting/cleanup Co-authored-by: Jason Stoltzfus <jastoltz24@gmail.com>
1 parent e831c92 commit 13d42a1

13 files changed

Lines changed: 373 additions & 13 deletions

File tree

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ export enum ApiTokenTypes {
1111
Search = 'search',
1212
}
1313

14+
export const CREATE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.created', {
15+
defaultMessage: 'Successfully created key.',
16+
});
17+
export const UPDATE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.update', {
18+
defaultMessage: 'Successfully updated API Key.',
19+
});
20+
export const DELETE_MESSAGE = i18n.translate('xpack.enterpriseSearch.appSearch.tokens.deleted', {
21+
defaultMessage: 'Successfully deleted key.',
22+
});
23+
1424
export const SEARCH_DISPLAY = i18n.translate(
1525
'xpack.enterpriseSearch.appSearch.tokens.permissions.display.search',
1626
{
@@ -81,3 +91,5 @@ export const TOKEN_TYPE_INFO = [
8191
{ value: ApiTokenTypes.Private, text: TOKEN_TYPE_DISPLAY_NAMES[ApiTokenTypes.Private] },
8292
{ value: ApiTokenTypes.Admin, text: TOKEN_TYPE_DISPLAY_NAMES[ApiTokenTypes.Admin] },
8393
];
94+
95+
export const FLYOUT_ARIA_LABEL_ID = 'credentialsFlyoutTitle';

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Credentials } from './credentials';
1414
import { EuiCopy, EuiLoadingContent, EuiPageContentBody } from '@elastic/eui';
1515

1616
import { externalUrl } from '../../../shared/enterprise_search_url';
17+
import { CredentialsFlyout } from './credentials_flyout';
1718

1819
describe('Credentials', () => {
1920
// Kea mocks
@@ -71,4 +72,16 @@ describe('Credentials', () => {
7172
button.props().onClick();
7273
expect(actions.showCredentialsForm).toHaveBeenCalledTimes(1);
7374
});
75+
76+
it('will render CredentialsFlyout if shouldShowCredentialsForm is true', () => {
77+
setMockValues({ shouldShowCredentialsForm: true });
78+
const wrapper = shallow(<Credentials />);
79+
expect(wrapper.find(CredentialsFlyout)).toHaveLength(1);
80+
});
81+
82+
it('will NOT render CredentialsFlyout if shouldShowCredentialsForm is false', () => {
83+
setMockValues({ shouldShowCredentialsForm: false });
84+
const wrapper = shallow(<Credentials />);
85+
expect(wrapper.find(CredentialsFlyout)).toHaveLength(0);
86+
});
7487
});

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,19 @@ import {
2424
import { i18n } from '@kbn/i18n';
2525

2626
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
27+
import { FlashMessages } from '../../../shared/flash_messages';
28+
2729
import { CredentialsLogic } from './credentials_logic';
2830
import { externalUrl } from '../../../shared/enterprise_search_url/external_url';
2931
import { CredentialsList } from './credentials_list';
32+
import { CredentialsFlyout } from './credentials_flyout';
3033

3134
export const Credentials: React.FC = () => {
3235
const { initializeCredentialsData, resetCredentials, showCredentialsForm } = useActions(
3336
CredentialsLogic
3437
);
3538

36-
const { dataLoading } = useValues(CredentialsLogic);
39+
const { dataLoading, shouldShowCredentialsForm } = useValues(CredentialsLogic);
3740

3841
useEffect(() => {
3942
initializeCredentialsData();
@@ -63,6 +66,7 @@ export const Credentials: React.FC = () => {
6366
</EuiPageHeaderSection>
6467
</EuiPageHeader>
6568
<EuiPageContentBody>
69+
{shouldShowCredentialsForm && <CredentialsFlyout />}
6670
<EuiPanel className="eui-textCenter">
6771
<EuiTitle size="s">
6872
<h2>
@@ -120,7 +124,8 @@ export const Credentials: React.FC = () => {
120124
)}
121125
</EuiPageContentHeaderSection>
122126
</EuiPageContentHeader>
123-
<EuiSpacer size="s" />
127+
<EuiSpacer size="m" />
128+
<FlashMessages />
124129
<EuiPanel>{!!dataLoading ? <EuiLoadingContent lines={3} /> : <CredentialsList />}</EuiPanel>
125130
</EuiPageContentBody>
126131
</>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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 React from 'react';
8+
import { shallow } from 'enzyme';
9+
import { EuiFlyoutBody } from '@elastic/eui';
10+
11+
import { CredentialsFlyoutBody } from './body';
12+
13+
describe('CredentialsFlyoutBody', () => {
14+
it('renders', () => {
15+
const wrapper = shallow(<CredentialsFlyoutBody />);
16+
expect(wrapper.find(EuiFlyoutBody)).toHaveLength(1);
17+
});
18+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 React from 'react';
8+
import { EuiFlyoutBody } from '@elastic/eui';
9+
10+
import { FlashMessages } from '../../../../shared/flash_messages';
11+
12+
export const CredentialsFlyoutBody: React.FC = () => {
13+
return (
14+
<EuiFlyoutBody>
15+
<FlashMessages />
16+
Details go here
17+
</EuiFlyoutBody>
18+
);
19+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 { EuiFlyoutFooter, EuiButtonEmpty } from '@elastic/eui';
12+
13+
import { CredentialsFlyoutFooter } from './footer';
14+
15+
describe('CredentialsFlyoutFooter', () => {
16+
const values = {
17+
activeApiTokenExists: false,
18+
};
19+
const actions = {
20+
hideCredentialsForm: jest.fn(),
21+
};
22+
23+
beforeEach(() => {
24+
jest.clearAllMocks();
25+
setMockValues(values);
26+
setMockActions(actions);
27+
});
28+
29+
it('renders', () => {
30+
const wrapper = shallow(<CredentialsFlyoutFooter />);
31+
expect(wrapper.find(EuiFlyoutFooter)).toHaveLength(1);
32+
});
33+
34+
it('closes the flyout', () => {
35+
const wrapper = shallow(<CredentialsFlyoutFooter />);
36+
const button = wrapper.find(EuiButtonEmpty);
37+
button.simulate('click');
38+
expect(button.prop('children')).toEqual('Close');
39+
expect(actions.hideCredentialsForm).toHaveBeenCalled();
40+
});
41+
42+
it('renders action button text for new tokens', () => {
43+
const wrapper = shallow(<CredentialsFlyoutFooter />);
44+
const button = wrapper.find('[data-test-subj="APIKeyActionButton"]');
45+
46+
expect(button.prop('children')).toEqual('Save');
47+
});
48+
49+
it('renders action button text for existing tokens', () => {
50+
setMockValues({ activeApiTokenExists: true });
51+
const wrapper = shallow(<CredentialsFlyoutFooter />);
52+
const button = wrapper.find('[data-test-subj="APIKeyActionButton"]');
53+
54+
expect(button.prop('children')).toEqual('Update');
55+
});
56+
57+
it('calls onApiTokenChange on action button press', () => {
58+
const wrapper = shallow(<CredentialsFlyoutFooter />);
59+
const button = wrapper.find('[data-test-subj="APIKeyActionButton"]');
60+
button.simulate('click');
61+
62+
// TODO: Expect onApiTokenChange to have been called
63+
});
64+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 React from 'react';
8+
import { useValues, useActions } from 'kea';
9+
import {
10+
EuiFlyoutFooter,
11+
EuiFlexGroup,
12+
EuiFlexItem,
13+
EuiButtonEmpty,
14+
EuiButton,
15+
} from '@elastic/eui';
16+
import { i18n } from '@kbn/i18n';
17+
18+
import { CredentialsLogic } from '../credentials_logic';
19+
20+
export const CredentialsFlyoutFooter: React.FC = () => {
21+
const { hideCredentialsForm } = useActions(CredentialsLogic);
22+
const { activeApiTokenExists } = useValues(CredentialsLogic);
23+
24+
return (
25+
<EuiFlyoutFooter>
26+
<EuiFlexGroup justifyContent="spaceBetween">
27+
<EuiFlexItem grow={false}>
28+
<EuiButtonEmpty iconType="cross" onClick={hideCredentialsForm}>
29+
{i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.closeText', {
30+
defaultMessage: 'Close',
31+
})}
32+
</EuiButtonEmpty>
33+
</EuiFlexItem>
34+
<EuiFlexItem grow={false}>
35+
<EuiButton
36+
onClick={() => window.alert('submit')}
37+
fill={true}
38+
color="secondary"
39+
iconType="check"
40+
data-test-subj="APIKeyActionButton"
41+
>
42+
{activeApiTokenExists
43+
? i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.updateText', {
44+
defaultMessage: 'Update',
45+
})
46+
: i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.saveText', {
47+
defaultMessage: 'Save',
48+
})}
49+
</EuiButton>
50+
</EuiFlexItem>
51+
</EuiFlexGroup>
52+
</EuiFlyoutFooter>
53+
);
54+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 } from '../../../../__mocks__/kea.mock';
8+
9+
import React from 'react';
10+
import { shallow } from 'enzyme';
11+
import { EuiFlyoutHeader } from '@elastic/eui';
12+
13+
import { ApiTokenTypes } from '../constants';
14+
import { IApiToken } from '../types';
15+
16+
import { CredentialsFlyoutHeader } from './header';
17+
18+
describe('CredentialsFlyoutHeader', () => {
19+
const apiToken: IApiToken = {
20+
name: '',
21+
type: ApiTokenTypes.Private,
22+
read: true,
23+
write: true,
24+
access_all_engines: true,
25+
};
26+
const values = {
27+
activeApiToken: apiToken,
28+
};
29+
30+
beforeEach(() => {
31+
jest.clearAllMocks();
32+
setMockValues(values);
33+
});
34+
35+
it('renders', () => {
36+
const wrapper = shallow(<CredentialsFlyoutHeader />);
37+
38+
expect(wrapper.find(EuiFlyoutHeader)).toHaveLength(1);
39+
expect(wrapper.find('h2').prop('id')).toEqual('credentialsFlyoutTitle');
40+
expect(wrapper.find('h2').prop('children')).toEqual('Create a new key');
41+
});
42+
43+
it('changes the title text if editing an existing token', () => {
44+
setMockValues({
45+
activeApiToken: {
46+
...apiToken,
47+
id: 'some-id',
48+
name: 'search-key',
49+
},
50+
});
51+
const wrapper = shallow(<CredentialsFlyoutHeader />);
52+
53+
expect(wrapper.find('h2').prop('children')).toEqual('Update search-key');
54+
});
55+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 React from 'react';
8+
import { useValues } from 'kea';
9+
import { EuiFlyoutHeader, EuiTitle } from '@elastic/eui';
10+
import { i18n } from '@kbn/i18n';
11+
12+
import { CredentialsLogic } from '../credentials_logic';
13+
import { FLYOUT_ARIA_LABEL_ID } from '../constants';
14+
15+
export const CredentialsFlyoutHeader: React.FC = () => {
16+
const { activeApiToken } = useValues(CredentialsLogic);
17+
18+
return (
19+
<EuiFlyoutHeader hasBorder={true}>
20+
<EuiTitle size="m">
21+
<h2 id={FLYOUT_ARIA_LABEL_ID}>
22+
{activeApiToken.id
23+
? i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.updateTitle', {
24+
defaultMessage: 'Update {tokenName}',
25+
values: { tokenName: activeApiToken.name },
26+
})
27+
: i18n.translate('xpack.enterpriseSearch.appSearch.credentials.flyout.createTitle', {
28+
defaultMessage: 'Create a new key',
29+
})}
30+
</h2>
31+
</EuiTitle>
32+
</EuiFlyoutHeader>
33+
);
34+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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 { setMockActions } from '../../../../__mocks__/kea.mock';
8+
9+
import React from 'react';
10+
import { shallow } from 'enzyme';
11+
import { EuiFlyout } from '@elastic/eui';
12+
13+
import { CredentialsFlyout } from './';
14+
15+
describe('CredentialsFlyout', () => {
16+
const actions = {
17+
hideCredentialsForm: jest.fn(),
18+
};
19+
20+
beforeEach(() => {
21+
jest.clearAllMocks();
22+
setMockActions(actions);
23+
});
24+
25+
it('renders', () => {
26+
const wrapper = shallow(<CredentialsFlyout />);
27+
const flyout = wrapper.find(EuiFlyout);
28+
29+
expect(flyout).toHaveLength(1);
30+
expect(flyout.prop('aria-labelledby')).toEqual('credentialsFlyoutTitle');
31+
expect(flyout.prop('onClose')).toEqual(actions.hideCredentialsForm);
32+
});
33+
});

0 commit comments

Comments
 (0)