Skip to content
This repository has been archived by the owner on Feb 8, 2024. It is now read-only.

Commit

Permalink
[v9] Backport Teleport Connect (5 of 5) (#766)
Browse files Browse the repository at this point in the history
* Bring back native scrollbar as the styled one causes content to jump when it becomes visible

* Use new colors for theme

* Fix not clickable notifications when displayed over xterm

* Close `Identity` popover after selecting an option (#741)

* Fix getting cwd in presence of lsof warnings (#745)

lsof uses stderr to print out warnings. This caused the previous version
of ptyProcess to throw an error, even though the exit code of lsof was 0.

The existing code already handles non-zero exit codes (asyncExec will
reject the promise). Since we don't know why stderr was inspected in
the first place, let's just remove the check for it.

* Change connections shortcut to `Command/Ctrl-P` (#747)

* Resolve issues on logout (#740)

* Change app name to `Teleport Connect` (#753)

* Show database username suggestions in Teleport Connect (#754)

* Move useAsync to the shared package, add docs

* Add async variant of MenuLogin

* Add Teleport Connect version of MenuLogin to its story

* Update gRPC files

* Show database username suggestions

* Use JSdoc in useAsync

Co-authored-by: Grzegorz Zdunek <[email protected]>

* Convert useAsync to a named export

* Refactor MenuLogin to use useAsync underneath

* Rewrite useAsync to use a promise instead of async

The Web UI currently doesn't support `async`. To do this, we'd need to
configure polyfills there, but we don't have time for that right now.

* Replace async with promise in MenuLogin

Co-authored-by: Grzegorz Zdunek <[email protected]>

* Fix check for the --insecure flag (#758)

Fixes gravitational/webapps.e#147.

* Remove state related to a cluster when removing it (#755)

* Teleport Connect: Add dropdown for database name (#757)

* Add required prop to MenuLogin

This will be needed for the db name dropdown, where the value is optional.

* Update proto files

* Include targetSubresourceName when creating a gateway

* Set better title for gateway tab

* Fix long document titles breaking connection tracker's UI

Co-authored-by: Grzegorz Zdunek <[email protected]>
Co-authored-by: Grzegorz Zdunek <[email protected]>
  • Loading branch information
3 people authored Apr 27, 2022
1 parent 3d58fd8 commit 2aa1d3c
Show file tree
Hide file tree
Showing 82 changed files with 1,466 additions and 521 deletions.
74 changes: 47 additions & 27 deletions packages/shared/components/MenuLogin/MenuLogin.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,62 @@ import React from 'react';
import { storiesOf } from '@storybook/react';
import { Flex } from 'design';
import { MenuLogin } from './MenuLogin';
import { Router } from 'react-router';
import { createMemoryHistory } from 'history';
import { MenuLoginHandle } from './types';
import { MenuLoginTheme } from 'teleterm/ui/DocumentCluster/ClusterResources/MenuLoginTheme';

storiesOf('Shared/MenuLogin', module).add('MenuLogin', () => (
<Flex
width="400px"
height="100px"
alignItems="center"
justifyContent="space-around"
bg="primary.light"
>
<MenuLogin
getLoginItems={() => []}
onSelect={() => null}
placeholder="Please provide user name…"
/>
<SampleMenu />
</Flex>
));
storiesOf('Shared/MenuLogin', module).add('MenuLogin', () => {
return <MenuLoginExamples />;
});

storiesOf('Shared/MenuLogin', module).add(
'MenuLogin in Teleport Connect',
() => {
return (
<MenuLoginTheme>
<MenuLoginExamples />
</MenuLoginTheme>
);
}
);

function MenuLoginExamples() {
return (
<Flex
width="400px"
height="100px"
alignItems="center"
justifyContent="space-around"
bg="primary.light"
>
<MenuLogin
getLoginItems={() => []}
onSelect={() => null}
placeholder="Please provide user name…"
/>
<MenuLogin
getLoginItems={() => new Promise(() => {})}
placeholder="MenuLogin in processing state"
onSelect={() => null}
/>
<SampleMenu />
</Flex>
);
}

class SampleMenu extends React.Component {
menuRef = React.createRef<MenuLogin>();
menuRef = React.createRef<MenuLoginHandle>();

componentDidMount() {
this.menuRef.current.onOpen();
this.menuRef.current.open();
}

render() {
return (
<Router history={createMemoryHistory()}>
<MenuLogin
ref={this.menuRef}
getLoginItems={() => loginItems}
onSelect={() => null}
/>
</Router>
<MenuLogin
ref={this.menuRef}
getLoginItems={() => loginItems}
onSelect={() => null}
/>
);
}
}
Expand Down
47 changes: 47 additions & 0 deletions packages/shared/components/MenuLogin/MenuLogin.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import { render, fireEvent, waitFor } from 'design/utils/testing';
import { MenuLogin } from './MenuLogin';

test('does not accept an empty value when required is set to true', async () => {
const onSelect = jest.fn();
const { findByText, findByPlaceholderText } = render(
<MenuLogin
placeholder="MenuLogin input"
required={true}
getLoginItems={() => []}
onSelect={() => onSelect()}
/>
);

fireEvent.click(await findByText('CONNECT'));
await waitFor(async () =>
fireEvent.keyPress(await findByPlaceholderText('MenuLogin input'), {
key: 'Enter',
keyCode: 13,
})
);

expect(onSelect).toHaveBeenCalledTimes(0);
});

test('accepts an empty value when required is set to false', async () => {
const onSelect = jest.fn();
const { findByText, findByPlaceholderText } = render(
<MenuLogin
placeholder="MenuLogin input"
required={false}
getLoginItems={() => []}
onSelect={() => onSelect()}
/>
);

fireEvent.click(await findByText('CONNECT'));
await waitFor(async () =>
fireEvent.keyPress(await findByPlaceholderText('MenuLogin input'), {
key: 'Enter',
keyCode: 13,
})
);

expect(onSelect).toHaveBeenCalledTimes(1);
});
178 changes: 107 additions & 71 deletions packages/shared/components/MenuLogin/MenuLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,104 +14,98 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import React, { useImperativeHandle, useRef, useState } from 'react';
import styled from 'styled-components';
import { NavLink } from 'react-router-dom';
import Menu, { MenuItem } from 'design/Menu';
import { space } from 'design/system';
import { MenuLoginProps } from './types';
import { ButtonBorder, Flex } from 'design';
import { MenuLoginProps, LoginItem, MenuLoginHandle } from './types';
import { ButtonBorder, Flex, Indicator } from 'design';
import { CarrotDown } from 'design/Icon';
import { useAsync, Attempt } from 'shared/hooks/useAsync';

export const MenuLogin = React.forwardRef<MenuLoginHandle, MenuLoginProps>(
(props, ref) => {
const { onSelect, anchorOrigin, transformOrigin, required = true } = props;
const anchorRef = useRef<HTMLElement>();
const [isOpen, setIsOpen] = useState(false);
const [getLoginItemsAttempt, runGetLoginItems] = useAsync(() =>
Promise.resolve().then(() => props.getLoginItems())
);

const placeholder = props.placeholder || 'Enter login name…';
const onOpen = () => {
if (!getLoginItemsAttempt.status) {
runGetLoginItems();
}
setIsOpen(true);
};
const onClose = () => {
setIsOpen(false);
};
const onItemClick = (
e: React.MouseEvent<HTMLAnchorElement>,
login: string
) => {
onClose();
onSelect(e, login);
};
const onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && (!required || e.currentTarget.value)) {
onClose();
onSelect(e, e.currentTarget.value);
}
};

useImperativeHandle(ref, () => ({
open: () => {
onOpen();
},
}));

export class MenuLogin extends React.Component<MenuLoginProps> {
static displayName = 'MenuLogin';

anchorEl = React.createRef();

state = {
logins: [],
open: false,
anchorEl: null,
};

onOpen = () => {
const logins = this.props.getLoginItems();
this.setState({
logins,
open: true,
});
};

onItemClick = (e: React.MouseEvent<HTMLAnchorElement>, login: string) => {
this.onClose();
this.props.onSelect(e, login);
};

onClose = () => {
this.setState({ open: false });
};

onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && e.currentTarget.value) {
this.onClose();
this.props.onSelect(e, e.currentTarget.value);
}
};

render() {
const { anchorOrigin, transformOrigin } = this.props;
const placeholder = this.props.placeholder || 'Enter login name…';
const { open, logins } = this.state;
return (
<React.Fragment>
<ButtonBorder
height="24px"
size="small"
setRef={e => (this.anchorEl = e)}
onClick={this.onOpen}
setRef={anchorRef}
onClick={onOpen}
>
CONNECT
<CarrotDown ml={2} mr={-2} fontSize="2" color="text.secondary" />
</ButtonBorder>
<Menu
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
anchorEl={this.anchorEl}
open={open}
onClose={this.onClose}
anchorEl={anchorRef.current}
open={isOpen}
onClose={onClose}
getContentAnchorEl={null}
>
<LoginItemList
logins={logins}
onKeyPress={this.onKeyPress}
onClick={this.onItemClick}
getLoginItemsAttempt={getLoginItemsAttempt}
onKeyPress={onKeyPress}
onClick={onItemClick}
placeholder={placeholder}
/>
</Menu>
</React.Fragment>
);
}
}
);

export const LoginItemList = ({ logins, onClick, onKeyPress, placeholder }) => {
logins = logins || [];
const $menuItems = logins.map((item, key) => {
const { login, url } = item;
return (
<StyledMenuItem
key={key}
px="2"
mx="2"
as={url ? NavLink : StyledButton}
to={url}
onClick={(e: Event) => {
onClick(e, login);
}}
>
{login}
</StyledMenuItem>
);
});
const LoginItemList = ({
getLoginItemsAttempt,
onClick,
onKeyPress,
placeholder,
}: {
getLoginItemsAttempt: Attempt<LoginItem[]>;
onClick: (e: React.MouseEvent<HTMLAnchorElement>, login: string) => void;
onKeyPress: (e: React.KeyboardEvent<HTMLInputElement>) => void;
placeholder: string;
}) => {
const content = getLoginItemListContent(getLoginItemsAttempt, onClick);

return (
<Flex flexDirection="column">
Expand All @@ -124,11 +118,53 @@ export const LoginItemList = ({ logins, onClick, onKeyPress, placeholder }) => {
placeholder={placeholder}
autoComplete="off"
/>
{$menuItems}
{content}
</Flex>
);
};

function getLoginItemListContent(
getLoginItemsAttempt: Attempt<LoginItem[]>,
onClick: (e: React.MouseEvent<HTMLAnchorElement>, login: string) => void
) {
switch (getLoginItemsAttempt.status) {
case '':
case 'processing':
return (
<Indicator
css={({ theme }) => `
align-self: center;
color: ${theme.colors.secondary.dark}
`}
/>
);
case 'error':
// Ignore errors and let the caller handle them outside of this component. There's little
// space to show the error inside the menu.
return null;
case 'success':
const logins = getLoginItemsAttempt.data;

return logins.map((item, key) => {
const { login, url } = item;
return (
<StyledMenuItem
key={key}
px="2"
mx="2"
as={url ? NavLink : StyledButton}
to={url}
onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
onClick(e, login);
}}
>
{login}
</StyledMenuItem>
);
});
}
}

const StyledButton = styled.button`
color: inherit;
border: none;
Expand Down
7 changes: 6 additions & 1 deletion packages/shared/components/MenuLogin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ export type LoginItem = {
};

export type MenuLoginProps = {
getLoginItems: () => LoginItem[];
getLoginItems: () => LoginItem[] | Promise<LoginItem[]>;
onSelect: (e: React.SyntheticEvent, login: string) => void;
anchorOrigin?: any;
transformOrigin?: any;
placeholder?: string;
required?: boolean;
};

export type MenuLoginHandle = {
open: () => void;
};
Loading

0 comments on commit 2aa1d3c

Please sign in to comment.