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

Commit

Permalink
Teleport Connect: Add dropdown for database name (#757)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ravicious committed Apr 27, 2022
1 parent d7915dc commit 17a367e
Show file tree
Hide file tree
Showing 22 changed files with 238 additions and 38 deletions.
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);
});
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 @@ -31,6 +31,9 @@ export class Gateway extends jspb.Message {
getCliCommand(): string;
setCliCommand(value: string): Gateway;

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


serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): Gateway.AsObject;
Expand All @@ -52,5 +55,6 @@ export namespace Gateway {
localPort: string,
protocol: string,
cliCommand: string,
targetSubresourceName: string,
}
}
32 changes: 31 additions & 1 deletion packages/teleterm/src/services/tshd/v1/gateway_pb.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ proto.teleport.terminal.v1.Gateway.toObject = function(includeInstance, msg) {
localAddress: jspb.Message.getFieldWithDefault(msg, 5, ""),
localPort: jspb.Message.getFieldWithDefault(msg, 6, ""),
protocol: jspb.Message.getFieldWithDefault(msg, 7, ""),
cliCommand: jspb.Message.getFieldWithDefault(msg, 8, "")
cliCommand: jspb.Message.getFieldWithDefault(msg, 8, ""),
targetSubresourceName: jspb.Message.getFieldWithDefault(msg, 9, "")
};

if (includeInstance) {
Expand Down Expand Up @@ -142,6 +143,10 @@ proto.teleport.terminal.v1.Gateway.deserializeBinaryFromReader = function(msg, r
var value = /** @type {string} */ (reader.readString());
msg.setCliCommand(value);
break;
case 9:
var value = /** @type {string} */ (reader.readString());
msg.setTargetSubresourceName(value);
break;
default:
reader.skipField();
break;
Expand Down Expand Up @@ -227,6 +232,13 @@ proto.teleport.terminal.v1.Gateway.serializeBinaryToWriter = function(message, w
f
);
}
f = message.getTargetSubresourceName();
if (f.length > 0) {
writer.writeString(
9,
f
);
}
};


Expand Down Expand Up @@ -374,4 +386,22 @@ proto.teleport.terminal.v1.Gateway.prototype.setCliCommand = 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);
};


goog.object.extend(exports, proto.teleport.terminal.v1);
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,66 @@ 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={transformOrigin}
anchorOrigin={anchorOrigin}
/>
<MenuLogin
placeholder="Enter username"
getLoginItems={() =>
getDatabaseUsers(dbUri, clustersService, notificationsService)
}
onSelect={(_, user) => {
setDbUser(user);
dbNameMenuLoginRef.current.open();
}}
transformOrigin={transformOrigin}
anchorOrigin={anchorOrigin}
/>
</OverlayGrid>
</MenuLoginTheme>
</Cell>
);
}

const transformOrigin = {
vertical: 'top',
horizontal: 'right',
};
const anchorOrigin = {
vertical: 'center',
horizontal: 'right',
};

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 {
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
2 changes: 2 additions & 0 deletions packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function getMockDocuments(): Document[] {
title: 'Test 4',
gatewayUri: '',
targetUri: '',
targetName: 'foobar',
targetUser: 'foo',
},
{
Expand All @@ -46,6 +47,7 @@ function getMockDocuments(): Document[] {
title: 'Test 5',
gatewayUri: '',
targetUri: '',
targetName: 'foobar',
targetUser: 'bar',
},
{
Expand Down
Loading

0 comments on commit 17a367e

Please sign in to comment.