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

[7.x] [Enterprise Search] Added reusable HiddenText component to Credentials (#80033) #81070

Merged
merged 1 commit into from
Oct 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';