Skip to content

Commit

Permalink
Added a HiddenText component
Browse files Browse the repository at this point in the history
  • Loading branch information
JasonStoltz committed Oct 8, 2020
1 parent 44b48a2 commit a5064f3
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import React from 'react';
import { shallow } from 'enzyme';

import { CredentialsList } from './credentials_list';
import { Key } from './key';
import { EuiBasicTable, EuiCopy } from '@elastic/eui';
import { IApiToken } from '../types';
import { ApiTokenTypes } from '../constants';
import { HiddenText } from '../../../../shared/hidden_text';

describe('Credentials', () => {
const apiToken: IApiToken = {
Expand Down Expand Up @@ -149,21 +151,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 +164,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: '*******',
});
});
});

describe('column 4 (modes)', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ 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 @@ -48,19 +50,11 @@ export const CredentialsList: React.FC = () => {
})}
>
{(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={token.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: '*****',
};

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).at(0).props() as any).onClick();
expect(props.copy).toHaveBeenCalled();
});

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

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

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

it('will render the provided text', () => {
const wrapper = shallow(<Key {...props} />);
expect(wrapper.text()).toContain('*****');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 Props {
copy: () => void;
toggleIsHidden: () => void;
isHidden: boolean;
text: string;
}

export const Key: React.FC<Props> = ({ copy, toggleIsHidden, isHidden, text }) => {
const icon = isHidden ? 'eye' : 'eyeClosed';
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={icon}
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.credentials.toggleApiEndpoint',
{
defaultMessage: 'Toggle API Key visibility',
}
)}
/>
{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 replaced with stars by default', () => {
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" from stars back to the original 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 starred 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,29 @@
/*
* 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';

interface ChildrenProps {
toggle: () => void;
isHidden: boolean;
hiddenText: string;
}

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

export const HiddenText: React.FC<Props> = ({ text, children }) => {
const [isHidden, toggleIsHidden] = useState(true);
const hiddenText = isHidden ? text.replace(/./g, '*') : 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 a5064f3

Please sign in to comment.