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

Commit

Permalink
Show database username suggestions in Teleport Connect (#754)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
ravicious and gzdunek committed Apr 26, 2022
1 parent b695ba2 commit cb3d37c
Show file tree
Hide file tree
Showing 24 changed files with 844 additions and 224 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
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 } = 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' && 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
6 changes: 5 additions & 1 deletion packages/shared/components/MenuLogin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ 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;
};

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

0 comments on commit cb3d37c

Please sign in to comment.