Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Enterprise Search] Added reusable HiddenText component to Credentials #80033

Merged
merged 11 commits into from
Oct 19, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -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 } 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: '',
Expand Down Expand Up @@ -149,21 +152,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(<div>{column.render(testToken)}</div>);
const children = wrapper.find(EuiCopy).props().children;
const copyEl = shallow(<div>{children(copyMock)}</div>);
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,
Expand All @@ -172,6 +165,35 @@ describe('Credentials', () => {
const wrapper = shallow(<div>{column.render(tokenWithNoKey)}</div>);
expect(wrapper.text()).toBe('');
});

it('renders an EuiCopy component with the key', () => {
const column = columns[2];
const wrapper = shallow(<div>{column.render(token)}</div>);
expect(wrapper.find(EuiCopy).props().textToCopy).toEqual('abc-123');
});

it('renders a HiddenText component with the key', () => {
const column = columns[2];
const wrapper = shallow(<div>{column.render(token)}</div>)
.find(EuiCopy)
.dive();
expect(wrapper.find(HiddenText).props().text).toEqual('abc-123');
});

it('renders a Key component', () => {
const column = columns[2];
const wrapper = shallow(<div>{column.render(token)}</div>)
.find(EuiCopy)
.dive()
.find(HiddenText)
.dive();
JasonStoltz marked this conversation as resolved.
Show resolved Hide resolved
expect(wrapper.find(Key).props()).toEqual({
copy: expect.any(Function),
toggleIsHidden: expect.any(Function),
isHidden: expect.any(Boolean),
text: <span aria-label="Hidden text">•••••••</span>,
});
});
});

describe('column 4 (modes)', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
*/

import React, { useMemo } from 'react';
import { EuiBasicTable, EuiBasicTableColumn, EuiButtonIcon, EuiCopy } from '@elastic/eui';
import { EuiBasicTable, EuiBasicTableColumn, EuiCopy } 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';
Expand Down Expand Up @@ -39,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 (
<EuiCopy
textToCopy={token.key}
textToCopy={key}
afterMessage={i18n.translate('xpack.enterpriseSearch.appSearch.credentials.copied', {
defaultMessage: 'Copied',
})}
>
{(copy) => (
<>
<EuiButtonIcon
onClick={copy}
iconType="copyClipboard"
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.credentials.copyApiKey',
{
defaultMessage: 'Copy API Key to clipboard',
}
)}
/>
{token.key}
</>
<HiddenText text={key}>
{({ hiddenText, isHidden, toggle }) => (
<Key copy={copy} toggleIsHidden={toggle} isHidden={isHidden} text={hiddenText} />
)}
</HiddenText>
)}
</EuiCopy>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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: '*****',
JasonStoltz marked this conversation as resolved.
Show resolved Hide resolved
};

it('renders', () => {
const wrapper = shallow(<Key {...props} />);
expect(wrapper.find(EuiButtonIcon).length).toEqual(2);
});

it('will call copy when the first button is clicked', () => {
const wrapper = shallow(<Key {...props} />);
wrapper.find(EuiButtonIcon).first().simulate('click');
expect(props.copy).toHaveBeenCalled();
});

it('will call hide when the second button is clicked', () => {
const wrapper = shallow(<Key {...props} />);
wrapper.find(EuiButtonIcon).last().simulate('click');
expect(props.toggleIsHidden).toHaveBeenCalled();
});

it('will render the "eye" icon when isHidden is true', () => {
const wrapper = shallow(<Key {...props} />);
expect(wrapper.find(EuiButtonIcon).last().prop('iconType')).toBe('eye');
});

it('will render the "eyeClosed" icon when isHidden is false', () => {
const wrapper = shallow(<Key {...{ ...props, isHidden: false }} />);
expect(wrapper.find(EuiButtonIcon).last().prop('iconType')).toBe('eyeClosed');
});

it('will render the provided text', () => {
const wrapper = shallow(<Key {...props} />);
expect(wrapper.text()).toContain('*****');
JasonStoltz marked this conversation as resolved.
Show resolved Hide resolved
});
});
Original file line number Diff line number Diff line change
@@ -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<IProps> = ({ 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 (
<>
<EuiButtonIcon
onClick={copy}
iconType="copyClipboard"
aria-label={i18n.translate('xpack.enterpriseSearch.appSearch.credentials.copyApiKey', {
defaultMessage: 'Copy API Key to clipboard',
})}
/>
<EuiButtonIcon
onClick={toggleIsHidden}
iconType={hideIcon}
aria-label={hideIconLabel}
aria-pressed={!isHidden}
/>
{text}
</>
);
};
Original file line number Diff line number Diff line change
@@ -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', () => {
JasonStoltz marked this conversation as resolved.
Show resolved Hide resolved
it('provides the passed "text" in a "hiddenText" field, with all characters obfuscated', () => {
const wrapper = shallow(
<HiddenText text="hidden_test">
{({ hiddenText, isHidden, toggle }) => <div>{hiddenText}</div>}
</HiddenText>
);
expect(wrapper.text()).toEqual('•••••••••••');
});

it('provides a "toggle" function, which when called, changes "hiddenText" to the original unobfuscated text', () => {
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
let toggleFn = () => {};

const wrapper = shallow(
<HiddenText text="hidden_test">
{({ hiddenText, isHidden, toggle }) => {
toggleFn = toggle;
return <div>{hiddenText}</div>;
}}
</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 text="hidden_test">
{({ hiddenText, isHidden, toggle }) => {
isHiddenBool = isHidden;
toggleFn = toggle;
return <div>{hiddenText}</div>;
}}
</HiddenText>
);

expect(isHiddenBool).toEqual(true);
toggleFn();
expect(isHiddenBool).toEqual(false);
toggleFn();
expect(isHiddenBool).toEqual(true);
});
});
Original file line number Diff line number Diff line change
@@ -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 ChildrenProps {
toggle: () => void;
isHidden: boolean;
hiddenText: React.ReactNode;
}

interface Props {
text: string;
children(props: ChildrenProps): ReactElement;
}

export const HiddenText: React.FC<Props> = ({ text, children }) => {
JasonStoltz marked this conversation as resolved.
Show resolved Hide resolved
const [isHidden, toggleIsHidden] = useState(true);

const hiddenLabel = i18n.translate('xpack.enterpriseSearch.hiddenText', {
defaultMessage: 'Hidden text',
});
const hiddenText = isHidden ? (
<span aria-label={hiddenLabel}>{text.replace(/./g, '•')}</span>
) : (
text
);

return children({
hiddenText,
isHidden,
toggle: () => toggleIsHidden(!isHidden),
});
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
};
Original file line number Diff line number Diff line change
@@ -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';