Skip to content

Commit

Permalink
[Enterprise Search] Added reusable HiddenText component to Credentials (
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonStoltz committed Oct 19, 2020
1 parent 40afb87 commit 933a023
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 35 deletions.
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, 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: '',
Expand Down Expand Up @@ -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(<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 @@ -185,6 +178,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();
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,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';
Expand Down Expand Up @@ -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 (
<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: 'some-api-key',
};

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('some-api-key');
});
});
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', () => {
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', () => {
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 IChildrenProps {
toggle: () => void;
isHidden: boolean;
hiddenText: React.ReactNode;
}

interface IProps {
text: string;
children(props: IChildrenProps): ReactElement;
}

export const HiddenText: React.FC<IProps> = ({ text, children }) => {
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),
});
};
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';

0 comments on commit 933a023

Please sign in to comment.