From 933a023ef1bae9fbb4387cc9c9dde3371953b89a Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Mon, 19 Oct 2020 17:06:14 -0400 Subject: [PATCH] [Enterprise Search] Added reusable HiddenText component to Credentials (#80033) --- .../credentials_list.test.tsx | 48 +++++++++++---- .../credentials_list/credentials_list.tsx | 33 ++++------ .../credentials/credentials_list/key.test.tsx | 56 +++++++++++++++++ .../credentials/credentials_list/key.tsx | 46 ++++++++++++++ .../shared/hidden_text/hidden_text.test.tsx | 61 +++++++++++++++++++ .../shared/hidden_text/hidden_text.tsx | 38 ++++++++++++ .../applications/shared/hidden_text/index.ts | 7 +++ 7 files changed, 254 insertions(+), 35 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/index.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx index b0e9b68ac839f1..97d29b9333f4b5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx @@ -8,12 +8,15 @@ import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; import { shallow } from 'enzyme'; - -import { CredentialsList } from './credentials_list'; import { EuiBasicTable, EuiCopy, EuiEmptyPrompt } from '@elastic/eui'; + import { IApiToken } from '../types'; import { ApiTokenTypes } from '../constants'; +import { HiddenText } from '../../../../shared/hidden_text'; +import { Key } from './key'; +import { CredentialsList } from './credentials_list'; + describe('Credentials', () => { const apiToken: IApiToken = { name: '', @@ -162,21 +165,11 @@ describe('Credentials', () => { }); describe('column 3 (key)', () => { - const testToken = { + const token = { ...apiToken, key: 'abc-123', }; - it('renders the credential and a button to copy it', () => { - const copyMock = jest.fn(); - const column = columns[2]; - const wrapper = shallow(
{column.render(testToken)}
); - const children = wrapper.find(EuiCopy).props().children; - const copyEl = shallow(
{children(copyMock)}
); - expect(copyEl.find('EuiButtonIcon').props().onClick).toEqual(copyMock); - expect(copyEl.text()).toContain('abc-123'); - }); - it('renders nothing if no key is present', () => { const tokenWithNoKey = { key: undefined, @@ -185,6 +178,35 @@ describe('Credentials', () => { const wrapper = shallow(
{column.render(tokenWithNoKey)}
); expect(wrapper.text()).toBe(''); }); + + it('renders an EuiCopy component with the key', () => { + const column = columns[2]; + const wrapper = shallow(
{column.render(token)}
); + expect(wrapper.find(EuiCopy).props().textToCopy).toEqual('abc-123'); + }); + + it('renders a HiddenText component with the key', () => { + const column = columns[2]; + const wrapper = shallow(
{column.render(token)}
) + .find(EuiCopy) + .dive(); + expect(wrapper.find(HiddenText).props().text).toEqual('abc-123'); + }); + + it('renders a Key component', () => { + const column = columns[2]; + const wrapper = shallow(
{column.render(token)}
) + .find(EuiCopy) + .dive() + .find(HiddenText) + .dive(); + expect(wrapper.find(Key).props()).toEqual({ + copy: expect.any(Function), + toggleIsHidden: expect.any(Function), + isHidden: expect.any(Boolean), + text: •••••••, + }); + }); }); describe('column 4 (modes)', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx index 7e754548d7a7dd..f9752dca582e1c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx @@ -5,19 +5,15 @@ */ import React, { useMemo } from 'react'; -import { - EuiBasicTable, - EuiBasicTableColumn, - EuiButtonIcon, - EuiCopy, - EuiEmptyPrompt, -} from '@elastic/eui'; +import { EuiBasicTable, EuiBasicTableColumn, EuiCopy, EuiEmptyPrompt } from '@elastic/eui'; import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table'; import { useActions, useValues } from 'kea'; import { i18n } from '@kbn/i18n'; import { CredentialsLogic } from '../credentials_logic'; +import { Key } from './key'; +import { HiddenText } from '../../../../shared/hidden_text'; import { IApiToken } from '../types'; import { TOKEN_TYPE_DISPLAY_NAMES } from '../constants'; import { apiTokenSort } from '../utils/api_token_sort'; @@ -45,28 +41,21 @@ export const CredentialsList: React.FC = () => { name: 'Key', width: '36%', render: (token: IApiToken) => { - if (!token.key) return null; + const { key } = token; + if (!key) return null; return ( {(copy) => ( - <> - - {token.key} - + + {({ hiddenText, isHidden, toggle }) => ( + + )} + )} ); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.test.tsx new file mode 100644 index 00000000000000..ec064436a3d067 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.test.tsx @@ -0,0 +1,56 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; +import { EuiButtonIcon } from '@elastic/eui'; + +import { Key } from './key'; + +describe('Key', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const props = { + copy: jest.fn(), + toggleIsHidden: jest.fn(), + isHidden: true, + text: 'some-api-key', + }; + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiButtonIcon).length).toEqual(2); + }); + + it('will call copy when the first button is clicked', () => { + const wrapper = shallow(); + wrapper.find(EuiButtonIcon).first().simulate('click'); + expect(props.copy).toHaveBeenCalled(); + }); + + it('will call hide when the second button is clicked', () => { + const wrapper = shallow(); + wrapper.find(EuiButtonIcon).last().simulate('click'); + expect(props.toggleIsHidden).toHaveBeenCalled(); + }); + + it('will render the "eye" icon when isHidden is true', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiButtonIcon).last().prop('iconType')).toBe('eye'); + }); + + it('will render the "eyeClosed" icon when isHidden is false', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiButtonIcon).last().prop('iconType')).toBe('eyeClosed'); + }); + + it('will render the provided text', () => { + const wrapper = shallow(); + expect(wrapper.text()).toContain('some-api-key'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx new file mode 100644 index 00000000000000..5c0c24ec733a47 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiButtonIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface IProps { + copy: () => void; + toggleIsHidden: () => void; + isHidden: boolean; + text: React.ReactNode; +} + +export const Key: React.FC = ({ copy, toggleIsHidden, isHidden, text }) => { + const hideIcon = isHidden ? 'eye' : 'eyeClosed'; + const hideIconLabel = isHidden + ? i18n.translate('xpack.enterpriseSearch.appSearch.credentials.showApiKey', { + defaultMessage: 'Show API Key', + }) + : i18n.translate('xpack.enterpriseSearch.appSearch.credentials.hideApiKey', { + defaultMessage: 'Hide API Key', + }); + + return ( + <> + + + {text} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.test.tsx new file mode 100644 index 00000000000000..40eb7fca49a8e3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.test.tsx @@ -0,0 +1,61 @@ +/* + * 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 React from 'react'; +import { shallow } from 'enzyme'; + +import { HiddenText } from '.'; + +describe('HiddenText', () => { + it('provides the passed "text" in a "hiddenText" field, with all characters obfuscated', () => { + const wrapper = shallow( + + {({ hiddenText, isHidden, toggle }) =>
{hiddenText}
} +
+ ); + expect(wrapper.text()).toEqual('•••••••••••'); + }); + + it('provides a "toggle" function, which when called, changes "hiddenText" to the original unobfuscated text', () => { + let toggleFn = () => {}; + + const wrapper = shallow( + + {({ hiddenText, isHidden, toggle }) => { + toggleFn = toggle; + return
{hiddenText}
; + }} +
+ ); + + expect(wrapper.text()).toEqual('•••••••••••'); + toggleFn(); + expect(wrapper.text()).toEqual('hidden_test'); + toggleFn(); + expect(wrapper.text()).toEqual('•••••••••••'); + }); + + it('provides a "hidden" boolean, which which tracks whether or not the text is obfuscated or not', () => { + let toggleFn = () => {}; + let isHiddenBool = false; + + shallow( + + {({ hiddenText, isHidden, toggle }) => { + isHiddenBool = isHidden; + toggleFn = toggle; + return
{hiddenText}
; + }} +
+ ); + + expect(isHiddenBool).toEqual(true); + toggleFn(); + expect(isHiddenBool).toEqual(false); + toggleFn(); + expect(isHiddenBool).toEqual(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx new file mode 100644 index 00000000000000..9b0833dfce5412 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx @@ -0,0 +1,38 @@ +/* + * 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 React, { useState, ReactElement } from 'react'; +import { i18n } from '@kbn/i18n'; + +interface IChildrenProps { + toggle: () => void; + isHidden: boolean; + hiddenText: React.ReactNode; +} + +interface IProps { + text: string; + children(props: IChildrenProps): ReactElement; +} + +export const HiddenText: React.FC = ({ text, children }) => { + const [isHidden, toggleIsHidden] = useState(true); + + const hiddenLabel = i18n.translate('xpack.enterpriseSearch.hiddenText', { + defaultMessage: 'Hidden text', + }); + const hiddenText = isHidden ? ( + {text.replace(/./g, '•')} + ) : ( + text + ); + + return children({ + hiddenText, + isHidden, + toggle: () => toggleIsHidden(!isHidden), + }); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/index.ts new file mode 100644 index 00000000000000..56ac36905078fa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/index.ts @@ -0,0 +1,7 @@ +/* + * 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 { HiddenText } from './hidden_text';