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

Teleport Connect: Add dropdown for database name #757

Merged
merged 10 commits into from
Apr 26, 2022
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);
Comment on lines +17 to +24
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure whether to put waitFor on fireEvent vs expect().toHaveBeenCalledTimes(0).

My understanding was that in order to verify that something was not at all, waitFor would have to wait until its default timeout runs out. To verify if fireEvent was called, it only needs to check if the event was fired.

Turns out this is not how waitFor works at all. If the callback doesn't return a promise, waitFor just continuously calls the callback until it stops throwing an error or the timeout runs out.

Anyway, I guess it's more readable to have expectations without waitFor.

});

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);
});
4 changes: 2 additions & 2 deletions packages/shared/components/MenuLogin/MenuLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { useAsync, Attempt } from 'shared/hooks/useAsync';

export const MenuLogin = React.forwardRef<MenuLoginHandle, MenuLoginProps>(
(props, ref) => {
const { onSelect, anchorOrigin, transformOrigin } = props;
const { onSelect, anchorOrigin, transformOrigin, required = true } = props;
const anchorRef = useRef<HTMLElement>();
const [isOpen, setIsOpen] = useState(false);
const [getLoginItemsAttempt, runGetLoginItems] = useAsync(() =>
Expand All @@ -51,7 +51,7 @@ export const MenuLogin = React.forwardRef<MenuLoginHandle, MenuLoginProps>(
onSelect(e, login);
};
const onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && e.currentTarget.value) {
if (e.key === 'Enter' && (!required || e.currentTarget.value)) {
onClose();
onSelect(e, e.currentTarget.value);
}
Expand Down
1 change: 1 addition & 0 deletions packages/shared/components/MenuLogin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type MenuLoginProps = {
anchorOrigin?: any;
transformOrigin?: any;
placeholder?: string;
required?: boolean;
};

export type MenuLoginHandle = {
Expand Down
3 changes: 2 additions & 1 deletion packages/teleterm/src/services/tshd/createClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ export default function createClient(addr: string) {
const req = new api.CreateGatewayRequest()
.setTargetUri(params.targetUri)
.setTargetUser(params.user)
.setLocalPort(params.port);
.setLocalPort(params.port)
.setTargetSubresourceName(params.subresource_name);
return new Promise<types.Gateway>((resolve, reject) => {
tshd.createGateway(req, (err, response) => {
if (err) {
Expand Down
1 change: 1 addition & 0 deletions packages/teleterm/src/services/tshd/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,5 @@ export type CreateGatewayParams = {
targetUri: string;
port?: string;
user?: string;
subresource_name?: string;
};
4 changes: 4 additions & 0 deletions packages/teleterm/src/services/tshd/v1/gateway_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export class Gateway extends jspb.Message {
getTargetUser(): string;
setTargetUser(value: string): Gateway;

getTargetSubresourceName(): string;
setTargetSubresourceName(value: string): Gateway;

getLocalAddress(): string;
setLocalAddress(value: string): Gateway;

Expand Down Expand Up @@ -48,6 +51,7 @@ export namespace Gateway {
targetName: string,
targetUri: string,
targetUser: string,
targetSubresourceName: string,
localAddress: string,
localPort: string,
protocol: string,
Expand Down
30 changes: 30 additions & 0 deletions packages/teleterm/src/services/tshd/v1/gateway_pb.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ proto.teleport.terminal.v1.Gateway.toObject = function(includeInstance, msg) {
targetName: jspb.Message.getFieldWithDefault(msg, 2, ""),
targetUri: jspb.Message.getFieldWithDefault(msg, 3, ""),
targetUser: jspb.Message.getFieldWithDefault(msg, 4, ""),
targetSubresourceName: jspb.Message.getFieldWithDefault(msg, 9, ""),
localAddress: jspb.Message.getFieldWithDefault(msg, 5, ""),
localPort: jspb.Message.getFieldWithDefault(msg, 6, ""),
protocol: jspb.Message.getFieldWithDefault(msg, 7, ""),
Expand Down Expand Up @@ -126,6 +127,10 @@ proto.teleport.terminal.v1.Gateway.deserializeBinaryFromReader = function(msg, r
var value = /** @type {string} */ (reader.readString());
msg.setTargetUser(value);
break;
case 9:
var value = /** @type {string} */ (reader.readString());
msg.setTargetSubresourceName(value);
break;
case 5:
var value = /** @type {string} */ (reader.readString());
msg.setLocalAddress(value);
Expand Down Expand Up @@ -199,6 +204,13 @@ proto.teleport.terminal.v1.Gateway.serializeBinaryToWriter = function(message, w
f
);
}
f = message.getTargetSubresourceName();
if (f.length > 0) {
writer.writeString(
9,
f
);
}
f = message.getLocalAddress();
if (f.length > 0) {
writer.writeString(
Expand Down Expand Up @@ -302,6 +314,24 @@ proto.teleport.terminal.v1.Gateway.prototype.setTargetUser = function(value) {
};


/**
* optional string target_subresource_name = 9;
* @return {string}
*/
proto.teleport.terminal.v1.Gateway.prototype.getTargetSubresourceName = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 9, ""));
};


/**
* @param {string} value
* @return {!proto.teleport.terminal.v1.Gateway} returns this
*/
proto.teleport.terminal.v1.Gateway.prototype.setTargetSubresourceName = function(value) {
return jspb.Message.setProto3StringField(this, 9, value);
};


/**
* optional string local_address = 5;
* @return {string}
Expand Down
4 changes: 4 additions & 0 deletions packages/teleterm/src/services/tshd/v1/service_pb.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,9 @@ export class CreateGatewayRequest extends jspb.Message {
getLocalPort(): string;
setLocalPort(value: string): CreateGatewayRequest;

getTargetSubresourceName(): string;
setTargetSubresourceName(value: string): CreateGatewayRequest;


serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): CreateGatewayRequest.AsObject;
Expand All @@ -418,6 +421,7 @@ export namespace CreateGatewayRequest {
targetUri: string,
targetUser: string,
localPort: string,
targetSubresourceName: string,
}
}

Expand Down
32 changes: 31 additions & 1 deletion packages/teleterm/src/services/tshd/v1/service_pb.js
Original file line number Diff line number Diff line change
Expand Up @@ -2990,7 +2990,8 @@ proto.teleport.terminal.v1.CreateGatewayRequest.toObject = function(includeInsta
var f, obj = {
targetUri: jspb.Message.getFieldWithDefault(msg, 1, ""),
targetUser: jspb.Message.getFieldWithDefault(msg, 2, ""),
localPort: jspb.Message.getFieldWithDefault(msg, 3, "")
localPort: jspb.Message.getFieldWithDefault(msg, 3, ""),
targetSubresourceName: jspb.Message.getFieldWithDefault(msg, 4, "")
};

if (includeInstance) {
Expand Down Expand Up @@ -3039,6 +3040,10 @@ proto.teleport.terminal.v1.CreateGatewayRequest.deserializeBinaryFromReader = fu
var value = /** @type {string} */ (reader.readString());
msg.setLocalPort(value);
break;
case 4:
var value = /** @type {string} */ (reader.readString());
msg.setTargetSubresourceName(value);
break;
default:
reader.skipField();
break;
Expand Down Expand Up @@ -3089,6 +3094,13 @@ proto.teleport.terminal.v1.CreateGatewayRequest.serializeBinaryToWriter = functi
f
);
}
f = message.getTargetSubresourceName();
if (f.length > 0) {
writer.writeString(
4,
f
);
}
};


Expand Down Expand Up @@ -3146,6 +3158,24 @@ proto.teleport.terminal.v1.CreateGatewayRequest.prototype.setLocalPort = functio
};


/**
* optional string target_subresource_name = 4;
* @return {string}
*/
proto.teleport.terminal.v1.CreateGatewayRequest.prototype.getTargetSubresourceName = function() {
return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, ""));
};


/**
* @param {string} value
* @return {!proto.teleport.terminal.v1.CreateGatewayRequest} returns this
*/
proto.teleport.terminal.v1.CreateGatewayRequest.prototype.setTargetSubresourceName = function(value) {
return jspb.Message.setProto3StringField(this, 4, value);
};



/**
* List of repeated fields within this message type.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import React from 'react';
import React, { useRef, useState } from 'react';
import styled from 'styled-components';
import { useDatabases, State } from './useDatabases';
import { Table } from 'teleterm/ui/components/Table';
import { Cell } from 'design/DataTable';
import { renderLabelCell } from '../renderLabelCell';
import { Danger } from 'design/Alert';
import { MenuLogin } from 'shared/components/MenuLogin';
import { MenuLogin, MenuLoginHandle } from 'shared/components/MenuLogin';
import { MenuLoginTheme } from '../MenuLoginTheme';
import { useAppContext } from 'teleterm/ui/appContextProvider';
import { ClustersService } from 'teleterm/ui/services/clusters';
Expand Down Expand Up @@ -55,7 +56,9 @@ function DatabaseList(props: State) {
render: db => (
<ConnectButton
dbUri={db.uri}
onConnect={user => props.connect(db.uri, user)}
onConnect={(dbUser, dbName) =>
props.connect(db.uri, dbUser, dbName)
}
/>
),
},
Expand All @@ -72,33 +75,69 @@ function ConnectButton({
onConnect,
}: {
dbUri: string;
onConnect: (user: string) => void;
onConnect: (dbUser: string, dbName: string) => void;
}) {
const { clustersService, notificationsService } = useAppContext();
const dbNameMenuLoginRef = useRef<MenuLoginHandle>();
const [dbUser, setDbUser] = useState<string>();

return (
<Cell align="right">
<MenuLoginTheme>
<MenuLogin
placeholder="Enter username…"
getLoginItems={() =>
getDatabaseUsers(dbUri, clustersService, notificationsService)
}
onSelect={(_, user) => onConnect(user)}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
anchorOrigin={{
vertical: 'center',
horizontal: 'right',
}}
/>
<OverlayGrid>
{/* The db name MenuLogin will be overlayed by the db username MenuLogin, which the user
should interact with first. */}
<MenuLogin
ref={dbNameMenuLoginRef}
placeholder="Enter optional db name"
required={false}
getLoginItems={() => []}
onSelect={(_, dbName) => onConnect(dbUser, dbName)}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
anchorOrigin={{
vertical: 'center',
horizontal: 'right',
}}
/>
<MenuLogin
placeholder="Enter username"
getLoginItems={() =>
getDatabaseUsers(dbUri, clustersService, notificationsService)
}
onSelect={(_, user) => {
setDbUser(user);
dbNameMenuLoginRef.current.open();
}}
transformOrigin={{
vertical: 'top',
ravicious marked this conversation as resolved.
Show resolved Hide resolved
horizontal: 'right',
}}
anchorOrigin={{
vertical: 'center',
horizontal: 'right',
}}
/>
</OverlayGrid>
</MenuLoginTheme>
</Cell>
);
}

const OverlayGrid = styled.div`
display: inline-grid;

& > button {
grid-area: 1 / 1;
}

& button:first-child {
visibility: hidden;
}
`;

async function getDatabaseUsers(
dbUri: string,
clustersService: ClustersService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function useDatabases() {
const dbs = clusterContext.getDbs();
const syncStatus = clusterContext.getSyncStatus().dbs;

function connect(dbUri: string, user: string): void {
function connect(dbUri: string, dbUser: string, dbName: string): void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When having 2+ params, it's better to convert them to object

const db = appContext.clustersService.findDb(dbUri);
const rootClusterUri = routing.ensureRootClusterUri(db.uri);
const documentsService =
Expand All @@ -33,9 +33,10 @@ export function useDatabases() {
const doc = documentsService.createGatewayDocument({
// Not passing the `gatewayUri` field here, as at this point the gateway doesn't exist yet.
// `port` is not passed as well, we'll let the tsh daemon pick a random one.
title: db.name,
targetUri: db.uri,
targetUser: user,
targetName: db.name,
targetUser: dbUser,
targetSubresourceName: dbName,
});
documentsService.add(doc);
documentsService.open(doc.uri);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default function useGateway(doc: types.DocumentGateway) {
targetUri: doc.targetUri,
port: doc.port,
user: doc.targetUser,
subresource_name: doc.targetSubresourceName,
});

workspaceDocumentsService.update(doc.uri, {
Expand Down
Loading