diff --git a/packages/shared/components/MenuLogin/MenuLogin.story.tsx b/packages/shared/components/MenuLogin/MenuLogin.story.tsx index bc80e45be..103027c33 100644 --- a/packages/shared/components/MenuLogin/MenuLogin.story.tsx +++ b/packages/shared/components/MenuLogin/MenuLogin.story.tsx @@ -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', () => ( - - []} - onSelect={() => null} - placeholder="Please provide user name…" - /> - - -)); +storiesOf('Shared/MenuLogin', module).add('MenuLogin', () => { + return ; +}); + +storiesOf('Shared/MenuLogin', module).add( + 'MenuLogin in Teleport Connect', + () => { + return ( + + + + ); + } +); + +function MenuLoginExamples() { + return ( + + []} + onSelect={() => null} + placeholder="Please provide user name…" + /> + new Promise(() => {})} + placeholder="MenuLogin in processing state" + onSelect={() => null} + /> + + + ); +} class SampleMenu extends React.Component { - menuRef = React.createRef(); + menuRef = React.createRef(); componentDidMount() { - this.menuRef.current.onOpen(); + this.menuRef.current.open(); } render() { return ( - - loginItems} - onSelect={() => null} - /> - + loginItems} + onSelect={() => null} + /> ); } } diff --git a/packages/shared/components/MenuLogin/MenuLogin.test.tsx b/packages/shared/components/MenuLogin/MenuLogin.test.tsx new file mode 100644 index 000000000..5d42c7898 --- /dev/null +++ b/packages/shared/components/MenuLogin/MenuLogin.test.tsx @@ -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( + []} + 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( + []} + onSelect={() => onSelect()} + /> + ); + + fireEvent.click(await findByText('CONNECT')); + await waitFor(async () => + fireEvent.keyPress(await findByPlaceholderText('MenuLogin input'), { + key: 'Enter', + keyCode: 13, + }) + ); + + expect(onSelect).toHaveBeenCalledTimes(1); +}); diff --git a/packages/shared/components/MenuLogin/MenuLogin.tsx b/packages/shared/components/MenuLogin/MenuLogin.tsx index ec794db31..6ed26911f 100644 --- a/packages/shared/components/MenuLogin/MenuLogin.tsx +++ b/packages/shared/components/MenuLogin/MenuLogin.tsx @@ -14,61 +14,62 @@ 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( + (props, ref) => { + const { onSelect, anchorOrigin, transformOrigin, required = true } = props; + const anchorRef = useRef(); + 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, + login: string + ) => { + onClose(); + onSelect(e, login); + }; + const onKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && (!required || e.currentTarget.value)) { + onClose(); + onSelect(e, e.currentTarget.value); + } + }; + + useImperativeHandle(ref, () => ({ + open: () => { + onOpen(); + }, + })); -export class MenuLogin extends React.Component { - 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, login: string) => { - this.onClose(); - this.props.onSelect(e, login); - }; - - onClose = () => { - this.setState({ open: false }); - }; - - onKeyPress = (e: React.KeyboardEvent) => { - 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 ( (this.anchorEl = e)} - onClick={this.onOpen} + setRef={anchorRef} + onClick={onOpen} > CONNECT @@ -76,42 +77,35 @@ export class MenuLogin extends React.Component { ); } -} +); -export const LoginItemList = ({ logins, onClick, onKeyPress, placeholder }) => { - logins = logins || []; - const $menuItems = logins.map((item, key) => { - const { login, url } = item; - return ( - { - onClick(e, login); - }} - > - {login} - - ); - }); +const LoginItemList = ({ + getLoginItemsAttempt, + onClick, + onKeyPress, + placeholder, +}: { + getLoginItemsAttempt: Attempt; + onClick: (e: React.MouseEvent, login: string) => void; + onKeyPress: (e: React.KeyboardEvent) => void; + placeholder: string; +}) => { + const content = getLoginItemListContent(getLoginItemsAttempt, onClick); return ( @@ -124,11 +118,53 @@ export const LoginItemList = ({ logins, onClick, onKeyPress, placeholder }) => { placeholder={placeholder} autoComplete="off" /> - {$menuItems} + {content} ); }; +function getLoginItemListContent( + getLoginItemsAttempt: Attempt, + onClick: (e: React.MouseEvent, login: string) => void +) { + switch (getLoginItemsAttempt.status) { + case '': + case 'processing': + return ( + ` + 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 ( + ) => { + onClick(e, login); + }} + > + {login} + + ); + }); + } +} + const StyledButton = styled.button` color: inherit; border: none; diff --git a/packages/shared/components/MenuLogin/types.ts b/packages/shared/components/MenuLogin/types.ts index 99d22e180..6f98471a8 100644 --- a/packages/shared/components/MenuLogin/types.ts +++ b/packages/shared/components/MenuLogin/types.ts @@ -20,9 +20,14 @@ export type LoginItem = { }; export type MenuLoginProps = { - getLoginItems: () => LoginItem[]; + getLoginItems: () => LoginItem[] | Promise; onSelect: (e: React.SyntheticEvent, login: string) => void; anchorOrigin?: any; transformOrigin?: any; placeholder?: string; + required?: boolean; +}; + +export type MenuLoginHandle = { + open: () => void; }; diff --git a/packages/shared/hooks/useAsync.ts b/packages/shared/hooks/useAsync.ts new file mode 100644 index 000000000..2ae8f1d7c --- /dev/null +++ b/packages/shared/hooks/useAsync.ts @@ -0,0 +1,185 @@ +/* +Copyright 2019 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* eslint-disable @typescript-eslint/ban-types */ + +import React from 'react'; + +/** + * `useAsync` lets you represent the state of an async operation as data. It accepts an async function + * that you want to execute. Calling the hook returns an array of three elements: + * + * * The first element is the representation of the attempt to run that async function as data, the + * so called attempt object. + * * The second element is a function which when called starts to execute the async function. + * * The third element is a function that lets you directly update the attempt object if needed. + * + * + * @example + * export function useUserProfile(userId) { + * const [fetchUserProfileAttempt, fetchUserProfile] = useAsync(async () => { + * return await fetch(`/users/${userId}`); + * }) + * + * return { fetchUserProfileAttempt, fetchUserProfile }; + * } + * + * + * @example In the view layer you can use it like this: + * function UserProfile(props) { + * const { fetchUserProfileAttempt, fetchUserProfile } = useUserProfile(props.id); + * + * useEffect(() => { + * if (!fetchUserProfileAttempt.status) { + * fetchUserProfile() + * } + * }, [fetchUserProfileAttempt]) + * + * switch (fetchUserProfileAttempt.status) { + * case '': + * case 'processing': + * return ; + * case 'error': + * return ; + * case 'success': + * return ; + * } + * } + */ +export function useAsync(cb?: AsyncCb) { + const [state, setState] = React.useState>(() => + makeEmptyAttempt() + ); + + const run = (...p: Parameters>) => + Promise.resolve() + .then(() => { + setState({ + ...state, + status: 'processing', + }); + + return cb.call(null, ...p) as R; + }) + .then( + data => { + setState({ + ...state, + status: 'success', + data, + }); + + return [data, null] as [R, Error]; + }, + err => { + setState({ + ...state, + status: 'error', + statusText: err.message, + data: null, + }); + + return [null, err] as [R, Error]; + } + ); + + function setAttempt(attempt: Attempt) { + setState(attempt); + } + + return [state, run, setAttempt] as const; +} + +export type Attempt = { + data?: T; + status: 'processing' | 'success' | 'error' | ''; + statusText: string; +}; + +export function makeEmptyAttempt(): Attempt { + return { + data: null, + status: '', + statusText: '', + }; +} + +export function makeSuccessAttempt(data: T): Attempt { + return { + data, + status: 'success', + statusText: '', + }; +} + +export function makeProcessingAttempt(): Attempt { + return { + data: null, + status: 'processing', + statusText: '', + }; +} + +export function makeErrorAttempt(statusText: string): Attempt { + return { + data: null, + status: 'error', + statusText, + }; +} + +type IsValidArg = T extends object + ? keyof T extends never + ? false + : true + : true; + +type AsyncCb = T extends (...args: any[]) => Promise + ? T + : T extends ( + a: infer A, + b: infer B, + c: infer C, + d: infer D, + e: infer E, + f: infer F, + g: infer G, + h: infer H, + i: infer I, + j: infer J + ) => Promise + ? IsValidArg extends true + ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J) => Promise + : IsValidArg extends true + ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I) => Promise + : IsValidArg extends true + ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => Promise + : IsValidArg extends true + ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => Promise + : IsValidArg extends true + ? (a: A, b: B, c: C, d: D, e: E, f: F) => Promise + : IsValidArg extends true + ? (a: A, b: B, c: C, d: D, e: E) => Promise + : IsValidArg extends true + ? (a: A, b: B, c: C, d: D) => Promise + : IsValidArg extends true + ? (a: A, b: B, c: C) => Promise + : IsValidArg extends true + ? (a: A, b: B) => Promise + : IsValidArg extends true + ? (a: A) => Promise + : () => Promise + : never; diff --git a/packages/teleterm/README.md b/packages/teleterm/README.md index e5c73ca7b..37161c16d 100644 --- a/packages/teleterm/README.md +++ b/packages/teleterm/README.md @@ -1,10 +1,10 @@ -## Teleport Terminal +## Teleport Connect -Teleport Terminal (teleterm) is a desktop application that allows easy access to Teleport resources. +Teleport Connect (previously Teleport Terminal, package name `teleterm`) is a desktop application that allows easy access to Teleport resources. ### Building and Packaging -Teleport Terminal consists of two main components: the `tsh` tool and the Electron app. Our build +Teleport Connect consists of two main components: the `tsh` tool and the Electron app. Our build scripts assume that the `webapps` repo and the `teleport` repo are in the same folder. To get started, first we need to build `tsh` that resides in the `teleport` repo. diff --git a/packages/teleterm/package.json b/packages/teleterm/package.json index 89fe281cf..d3d8fd2f0 100644 --- a/packages/teleterm/package.json +++ b/packages/teleterm/package.json @@ -1,7 +1,7 @@ { "name": "@gravitational/teleterm", "version": "1.0.0", - "description": "Teleport Terminal", + "description": "Teleport Connect", "main": "build/app/dist/main/main.js", "author": { "name": "Teleport", @@ -17,7 +17,7 @@ "build-main": "webpack build --config webpack.main.config.js --progress --mode=production", "build-renderer": "webpack build --config webpack.renderer.prod.config.js --progress", "build-natives": "electron-builder install-app-deps", - "package": "electron-builder build --publish never -c.extraMetadata.name=teleterm" + "package": "electron-builder build --publish never -c.extraMetadata.name=teleconnect" }, "repository": { "type": "git", @@ -52,10 +52,10 @@ "xterm": "^4.15.0", "xterm-addon-fit": "^0.5.0" }, + "productName": "Teleport Connect Preview", "build": { "electronVersion": "13.2.3", - "productName": "teleterm", - "appId": "gravitational.teleport.terminal", + "appId": "gravitational.teleport.connect", "asar": true, "asarUnpack": "**\\*.{node,dll}", "afterSign": "notarize.js", diff --git a/packages/teleterm/src/mainProcess/runtimeSettings.ts b/packages/teleterm/src/mainProcess/runtimeSettings.ts index 392329b5d..99af31272 100644 --- a/packages/teleterm/src/mainProcess/runtimeSettings.ts +++ b/packages/teleterm/src/mainProcess/runtimeSettings.ts @@ -14,7 +14,7 @@ const RESOURCES_PATH = app.isPackaged const dev = env.NODE_ENV === 'development' || env.DEBUG_PROD === 'true'; // Allows running tsh in insecure mode (development) -const isInsecure = dev || argv.slice(2).indexOf('--insecure') !== -1; +const isInsecure = dev || argv.includes('--insecure'); function getRuntimeSettings(): RuntimeSettings { const userDataDir = app.getPath('userData'); diff --git a/packages/teleterm/src/mainProcess/windowsManager.ts b/packages/teleterm/src/mainProcess/windowsManager.ts index c641b879c..0f1edef17 100644 --- a/packages/teleterm/src/mainProcess/windowsManager.ts +++ b/packages/teleterm/src/mainProcess/windowsManager.ts @@ -22,10 +22,10 @@ export class WindowsManager { y: windowState.y, width: windowState.width, height: windowState.height, - backgroundColor: theme.colors.primary.main, + backgroundColor: theme.colors.primary.darker, minWidth: 400, minHeight: 300, - title: 'Teleport Terminal', + title: 'Teleport Connect Preview', icon: getAssetPath('icon.png'), webPreferences: { contextIsolation: true, diff --git a/packages/teleterm/src/services/config/providers/keyboardShortcutsConfigProvider.ts b/packages/teleterm/src/services/config/providers/keyboardShortcutsConfigProvider.ts index aeffe787c..9e65fc8c6 100644 --- a/packages/teleterm/src/services/config/providers/keyboardShortcutsConfigProvider.ts +++ b/packages/teleterm/src/services/config/providers/keyboardShortcutsConfigProvider.ts @@ -45,7 +45,7 @@ export const keyboardShortcutsConfigProvider: ConfigServiceProvider { // -p: PID // -d: only include the file descriptor, cwd // -F: fields to output (the n character outputs 3 things, the last one is cwd) - const { stdout, stderr } = await asyncExec( - `lsof -a -p ${pid} -d cwd -F n` - ); - if (stderr) { - throw new Error(stderr); - } + const { stdout } = await asyncExec(`lsof -a -p ${pid} -d cwd -F n`); return stdout.split('\n').filter(Boolean).reverse()[0].substring(1); case 'linux': const asyncReadlink = promisify(readlink); diff --git a/packages/teleterm/src/services/tshd/createClient.ts b/packages/teleterm/src/services/tshd/createClient.ts index 9e72465c9..8fa16919c 100644 --- a/packages/teleterm/src/services/tshd/createClient.ts +++ b/packages/teleterm/src/services/tshd/createClient.ts @@ -109,6 +109,19 @@ export default function createClient(addr: string) { }); }, + async listDatabaseUsers(dbUri: string) { + const req = new api.ListDatabaseUsersRequest().setDbUri(dbUri); + return new Promise((resolve, reject) => { + tshd.listDatabaseUsers(req, (err, response) => { + if (err) { + reject(err); + } else { + resolve(response.toObject().usersList); + } + }); + }); + }, + async listServers(clusterUri: string) { const req = new api.ListServersRequest().setClusterUri(clusterUri); return new Promise((resolve, reject) => { @@ -202,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((resolve, reject) => { tshd.createGateway(req, (err, response) => { if (err) { diff --git a/packages/teleterm/src/services/tshd/fixtures/mocks.ts b/packages/teleterm/src/services/tshd/fixtures/mocks.ts index d239a8f45..d1512e926 100644 --- a/packages/teleterm/src/services/tshd/fixtures/mocks.ts +++ b/packages/teleterm/src/services/tshd/fixtures/mocks.ts @@ -5,6 +5,7 @@ export class MockTshClient implements types.TshClient { listRootClusters: () => Promise; listLeafClusters: (clusterUri: string) => Promise; listDatabases: (clusterUri: string) => Promise; + listDatabaseUsers: (dbUri: string) => Promise; listKubes: (clusterUri: string) => Promise; listApps: (clusterUri: string) => Promise; listServers: (clusterUri: string) => Promise; diff --git a/packages/teleterm/src/services/tshd/types.ts b/packages/teleterm/src/services/tshd/types.ts index 97ed5dd4e..f2bc294d5 100644 --- a/packages/teleterm/src/services/tshd/types.ts +++ b/packages/teleterm/src/services/tshd/types.ts @@ -34,6 +34,7 @@ export type TshClient = { listApps: (clusterUri: string) => Promise; listKubes: (clusterUri: string) => Promise; listDatabases: (clusterUri: string) => Promise; + listDatabaseUsers: (dbUri: string) => Promise; listServers: (clusterUri: string) => Promise; createAbortController: () => TshAbortController; addRootCluster: (addr: string) => Promise; @@ -73,4 +74,5 @@ export type CreateGatewayParams = { targetUri: string; port?: string; user?: string; + subresource_name?: string; }; diff --git a/packages/teleterm/src/services/tshd/v1/gateway_pb.d.ts b/packages/teleterm/src/services/tshd/v1/gateway_pb.d.ts index d9a2f919c..0476575eb 100644 --- a/packages/teleterm/src/services/tshd/v1/gateway_pb.d.ts +++ b/packages/teleterm/src/services/tshd/v1/gateway_pb.d.ts @@ -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; @@ -52,5 +55,6 @@ export namespace Gateway { localPort: string, protocol: string, cliCommand: string, + targetSubresourceName: string, } } diff --git a/packages/teleterm/src/services/tshd/v1/gateway_pb.js b/packages/teleterm/src/services/tshd/v1/gateway_pb.js index d8cdbc84c..004daf858 100644 --- a/packages/teleterm/src/services/tshd/v1/gateway_pb.js +++ b/packages/teleterm/src/services/tshd/v1/gateway_pb.js @@ -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) { @@ -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; @@ -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 + ); + } }; @@ -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); diff --git a/packages/teleterm/src/services/tshd/v1/service_grpc_pb.d.ts b/packages/teleterm/src/services/tshd/v1/service_grpc_pb.d.ts index 644778016..73b1cff58 100644 --- a/packages/teleterm/src/services/tshd/v1/service_grpc_pb.d.ts +++ b/packages/teleterm/src/services/tshd/v1/service_grpc_pb.d.ts @@ -19,6 +19,7 @@ interface ITerminalServiceService extends grpc.ServiceDefinition; responseDeserialize: grpc.deserialize; } +interface ITerminalServiceService_IListDatabaseUsers extends grpc.MethodDefinition { + path: "/teleport.terminal.v1.TerminalService/ListDatabaseUsers"; + requestStream: false; + responseStream: false; + requestSerialize: grpc.serialize; + requestDeserialize: grpc.deserialize; + responseSerialize: grpc.serialize; + responseDeserialize: grpc.deserialize; +} interface ITerminalServiceService_IListGateways extends grpc.MethodDefinition { path: "/teleport.terminal.v1.TerminalService/ListGateways"; requestStream: false; @@ -175,6 +185,7 @@ export interface ITerminalServiceServer { listRootClusters: grpc.handleUnaryCall; listLeafClusters: grpc.handleUnaryCall; listDatabases: grpc.handleUnaryCall; + listDatabaseUsers: grpc.handleUnaryCall; listGateways: grpc.handleUnaryCall; listServers: grpc.handleUnaryCall; listKubes: grpc.handleUnaryCall; @@ -199,6 +210,9 @@ export interface ITerminalServiceClient { listDatabases(request: v1_service_pb.ListDatabasesRequest, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListDatabasesResponse) => void): grpc.ClientUnaryCall; listDatabases(request: v1_service_pb.ListDatabasesRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListDatabasesResponse) => void): grpc.ClientUnaryCall; listDatabases(request: v1_service_pb.ListDatabasesRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListDatabasesResponse) => void): grpc.ClientUnaryCall; + listDatabaseUsers(request: v1_service_pb.ListDatabaseUsersRequest, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListDatabaseUsersResponse) => void): grpc.ClientUnaryCall; + listDatabaseUsers(request: v1_service_pb.ListDatabaseUsersRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListDatabaseUsersResponse) => void): grpc.ClientUnaryCall; + listDatabaseUsers(request: v1_service_pb.ListDatabaseUsersRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListDatabaseUsersResponse) => void): grpc.ClientUnaryCall; listGateways(request: v1_service_pb.ListGatewaysRequest, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListGatewaysResponse) => void): grpc.ClientUnaryCall; listGateways(request: v1_service_pb.ListGatewaysRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListGatewaysResponse) => void): grpc.ClientUnaryCall; listGateways(request: v1_service_pb.ListGatewaysRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListGatewaysResponse) => void): grpc.ClientUnaryCall; @@ -248,6 +262,9 @@ export class TerminalServiceClient extends grpc.Client implements ITerminalServi public listDatabases(request: v1_service_pb.ListDatabasesRequest, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListDatabasesResponse) => void): grpc.ClientUnaryCall; public listDatabases(request: v1_service_pb.ListDatabasesRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListDatabasesResponse) => void): grpc.ClientUnaryCall; public listDatabases(request: v1_service_pb.ListDatabasesRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListDatabasesResponse) => void): grpc.ClientUnaryCall; + public listDatabaseUsers(request: v1_service_pb.ListDatabaseUsersRequest, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListDatabaseUsersResponse) => void): grpc.ClientUnaryCall; + public listDatabaseUsers(request: v1_service_pb.ListDatabaseUsersRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListDatabaseUsersResponse) => void): grpc.ClientUnaryCall; + public listDatabaseUsers(request: v1_service_pb.ListDatabaseUsersRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListDatabaseUsersResponse) => void): grpc.ClientUnaryCall; public listGateways(request: v1_service_pb.ListGatewaysRequest, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListGatewaysResponse) => void): grpc.ClientUnaryCall; public listGateways(request: v1_service_pb.ListGatewaysRequest, metadata: grpc.Metadata, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListGatewaysResponse) => void): grpc.ClientUnaryCall; public listGateways(request: v1_service_pb.ListGatewaysRequest, metadata: grpc.Metadata, options: Partial, callback: (error: grpc.ServiceError | null, response: v1_service_pb.ListGatewaysResponse) => void): grpc.ClientUnaryCall; diff --git a/packages/teleterm/src/services/tshd/v1/service_grpc_pb.js b/packages/teleterm/src/services/tshd/v1/service_grpc_pb.js index 32469d2d1..2c765ff0f 100644 --- a/packages/teleterm/src/services/tshd/v1/service_grpc_pb.js +++ b/packages/teleterm/src/services/tshd/v1/service_grpc_pb.js @@ -159,6 +159,28 @@ function deserialize_teleport_terminal_v1_ListClustersResponse(buffer_arg) { return v1_service_pb.ListClustersResponse.deserializeBinary(new Uint8Array(buffer_arg)); } +function serialize_teleport_terminal_v1_ListDatabaseUsersRequest(arg) { + if (!(arg instanceof v1_service_pb.ListDatabaseUsersRequest)) { + throw new Error('Expected argument of type teleport.terminal.v1.ListDatabaseUsersRequest'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_teleport_terminal_v1_ListDatabaseUsersRequest(buffer_arg) { + return v1_service_pb.ListDatabaseUsersRequest.deserializeBinary(new Uint8Array(buffer_arg)); +} + +function serialize_teleport_terminal_v1_ListDatabaseUsersResponse(arg) { + if (!(arg instanceof v1_service_pb.ListDatabaseUsersResponse)) { + throw new Error('Expected argument of type teleport.terminal.v1.ListDatabaseUsersResponse'); + } + return Buffer.from(arg.serializeBinary()); +} + +function deserialize_teleport_terminal_v1_ListDatabaseUsersResponse(buffer_arg) { + return v1_service_pb.ListDatabaseUsersResponse.deserializeBinary(new Uint8Array(buffer_arg)); +} + function serialize_teleport_terminal_v1_ListDatabasesRequest(arg) { if (!(arg instanceof v1_service_pb.ListDatabasesRequest)) { throw new Error('Expected argument of type teleport.terminal.v1.ListDatabasesRequest'); @@ -303,7 +325,7 @@ function deserialize_teleport_terminal_v1_RemoveGatewayRequest(buffer_arg) { } -// TerminalService desribes teleterm service +// TerminalService describes Teleterm service var TerminalServiceService = exports.TerminalServiceService = { // ListRootClusters lists root clusters listRootClusters: { @@ -341,6 +363,18 @@ listDatabases: { responseSerialize: serialize_teleport_terminal_v1_ListDatabasesResponse, responseDeserialize: deserialize_teleport_terminal_v1_ListDatabasesResponse, }, + // ListDatabaseUsers lists allowed users for the given database based on the role set. +listDatabaseUsers: { + path: '/teleport.terminal.v1.TerminalService/ListDatabaseUsers', + requestStream: false, + responseStream: false, + requestType: v1_service_pb.ListDatabaseUsersRequest, + responseType: v1_service_pb.ListDatabaseUsersResponse, + requestSerialize: serialize_teleport_terminal_v1_ListDatabaseUsersRequest, + requestDeserialize: deserialize_teleport_terminal_v1_ListDatabaseUsersRequest, + responseSerialize: serialize_teleport_terminal_v1_ListDatabaseUsersResponse, + responseDeserialize: deserialize_teleport_terminal_v1_ListDatabaseUsersResponse, + }, // ListGateways lists gateways listGateways: { path: '/teleport.terminal.v1.TerminalService/ListGateways', diff --git a/packages/teleterm/src/services/tshd/v1/service_pb.d.ts b/packages/teleterm/src/services/tshd/v1/service_pb.d.ts index c5ae913d8..a3dbe0892 100644 --- a/packages/teleterm/src/services/tshd/v1/service_pb.d.ts +++ b/packages/teleterm/src/services/tshd/v1/service_pb.d.ts @@ -348,6 +348,50 @@ export namespace ListDatabasesResponse { } } +export class ListDatabaseUsersRequest extends jspb.Message { + getDbUri(): string; + setDbUri(value: string): ListDatabaseUsersRequest; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): ListDatabaseUsersRequest.AsObject; + static toObject(includeInstance: boolean, msg: ListDatabaseUsersRequest): ListDatabaseUsersRequest.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: ListDatabaseUsersRequest, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): ListDatabaseUsersRequest; + static deserializeBinaryFromReader(message: ListDatabaseUsersRequest, reader: jspb.BinaryReader): ListDatabaseUsersRequest; +} + +export namespace ListDatabaseUsersRequest { + export type AsObject = { + dbUri: string, + } +} + +export class ListDatabaseUsersResponse extends jspb.Message { + clearUsersList(): void; + getUsersList(): Array; + setUsersList(value: Array): ListDatabaseUsersResponse; + addUsers(value: string, index?: number): string; + + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): ListDatabaseUsersResponse.AsObject; + static toObject(includeInstance: boolean, msg: ListDatabaseUsersResponse): ListDatabaseUsersResponse.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: ListDatabaseUsersResponse, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): ListDatabaseUsersResponse; + static deserializeBinaryFromReader(message: ListDatabaseUsersResponse, reader: jspb.BinaryReader): ListDatabaseUsersResponse; +} + +export namespace ListDatabaseUsersResponse { + export type AsObject = { + usersList: Array, + } +} + export class CreateGatewayRequest extends jspb.Message { getTargetUri(): string; setTargetUri(value: string): CreateGatewayRequest; @@ -358,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; @@ -374,6 +421,7 @@ export namespace CreateGatewayRequest { targetUri: string, targetUser: string, localPort: string, + targetSubresourceName: string, } } diff --git a/packages/teleterm/src/services/tshd/v1/service_pb.js b/packages/teleterm/src/services/tshd/v1/service_pb.js index 04996fb15..2c069ebbb 100644 --- a/packages/teleterm/src/services/tshd/v1/service_pb.js +++ b/packages/teleterm/src/services/tshd/v1/service_pb.js @@ -37,6 +37,8 @@ goog.exportSymbol('proto.teleport.terminal.v1.ListAppsRequest', null, global); goog.exportSymbol('proto.teleport.terminal.v1.ListAppsResponse', null, global); goog.exportSymbol('proto.teleport.terminal.v1.ListClustersRequest', null, global); goog.exportSymbol('proto.teleport.terminal.v1.ListClustersResponse', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.ListDatabaseUsersRequest', null, global); +goog.exportSymbol('proto.teleport.terminal.v1.ListDatabaseUsersResponse', null, global); goog.exportSymbol('proto.teleport.terminal.v1.ListDatabasesRequest', null, global); goog.exportSymbol('proto.teleport.terminal.v1.ListDatabasesResponse', null, global); goog.exportSymbol('proto.teleport.terminal.v1.ListGatewaysRequest', null, global); @@ -347,6 +349,48 @@ if (goog.DEBUG && !COMPILED) { */ proto.teleport.terminal.v1.ListDatabasesResponse.displayName = 'proto.teleport.terminal.v1.ListDatabasesResponse'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.teleport.terminal.v1.ListDatabaseUsersRequest = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.teleport.terminal.v1.ListDatabaseUsersRequest, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.teleport.terminal.v1.ListDatabaseUsersRequest.displayName = 'proto.teleport.terminal.v1.ListDatabaseUsersRequest'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.teleport.terminal.v1.ListDatabaseUsersResponse = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, proto.teleport.terminal.v1.ListDatabaseUsersResponse.repeatedFields_, null); +}; +goog.inherits(proto.teleport.terminal.v1.ListDatabaseUsersResponse, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.teleport.terminal.v1.ListDatabaseUsersResponse.displayName = 'proto.teleport.terminal.v1.ListDatabaseUsersResponse'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -2629,6 +2673,292 @@ proto.teleport.terminal.v1.ListDatabasesResponse.prototype.clearDatabasesList = +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.teleport.terminal.v1.ListDatabaseUsersRequest.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.terminal.v1.ListDatabaseUsersRequest.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.teleport.terminal.v1.ListDatabaseUsersRequest} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.ListDatabaseUsersRequest.toObject = function(includeInstance, msg) { + var f, obj = { + dbUri: jspb.Message.getFieldWithDefault(msg, 1, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.teleport.terminal.v1.ListDatabaseUsersRequest} + */ +proto.teleport.terminal.v1.ListDatabaseUsersRequest.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.teleport.terminal.v1.ListDatabaseUsersRequest; + return proto.teleport.terminal.v1.ListDatabaseUsersRequest.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.teleport.terminal.v1.ListDatabaseUsersRequest} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.teleport.terminal.v1.ListDatabaseUsersRequest} + */ +proto.teleport.terminal.v1.ListDatabaseUsersRequest.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.setDbUri(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.teleport.terminal.v1.ListDatabaseUsersRequest.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.teleport.terminal.v1.ListDatabaseUsersRequest.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.teleport.terminal.v1.ListDatabaseUsersRequest} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.ListDatabaseUsersRequest.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getDbUri(); + if (f.length > 0) { + writer.writeString( + 1, + f + ); + } +}; + + +/** + * optional string db_uri = 1; + * @return {string} + */ +proto.teleport.terminal.v1.ListDatabaseUsersRequest.prototype.getDbUri = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 1, "")); +}; + + +/** + * @param {string} value + * @return {!proto.teleport.terminal.v1.ListDatabaseUsersRequest} returns this + */ +proto.teleport.terminal.v1.ListDatabaseUsersRequest.prototype.setDbUri = function(value) { + return jspb.Message.setProto3StringField(this, 1, value); +}; + + + +/** + * List of repeated fields within this message type. + * @private {!Array} + * @const + */ +proto.teleport.terminal.v1.ListDatabaseUsersResponse.repeatedFields_ = [1]; + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.teleport.terminal.v1.ListDatabaseUsersResponse.prototype.toObject = function(opt_includeInstance) { + return proto.teleport.terminal.v1.ListDatabaseUsersResponse.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.teleport.terminal.v1.ListDatabaseUsersResponse} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.ListDatabaseUsersResponse.toObject = function(includeInstance, msg) { + var f, obj = { + usersList: (f = jspb.Message.getRepeatedField(msg, 1)) == null ? undefined : f + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.teleport.terminal.v1.ListDatabaseUsersResponse} + */ +proto.teleport.terminal.v1.ListDatabaseUsersResponse.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.teleport.terminal.v1.ListDatabaseUsersResponse; + return proto.teleport.terminal.v1.ListDatabaseUsersResponse.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.teleport.terminal.v1.ListDatabaseUsersResponse} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.teleport.terminal.v1.ListDatabaseUsersResponse} + */ +proto.teleport.terminal.v1.ListDatabaseUsersResponse.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {string} */ (reader.readString()); + msg.addUsers(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.teleport.terminal.v1.ListDatabaseUsersResponse.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.teleport.terminal.v1.ListDatabaseUsersResponse.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.teleport.terminal.v1.ListDatabaseUsersResponse} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.teleport.terminal.v1.ListDatabaseUsersResponse.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getUsersList(); + if (f.length > 0) { + writer.writeRepeatedString( + 1, + f + ); + } +}; + + +/** + * repeated string users = 1; + * @return {!Array} + */ +proto.teleport.terminal.v1.ListDatabaseUsersResponse.prototype.getUsersList = function() { + return /** @type {!Array} */ (jspb.Message.getRepeatedField(this, 1)); +}; + + +/** + * @param {!Array} value + * @return {!proto.teleport.terminal.v1.ListDatabaseUsersResponse} returns this + */ +proto.teleport.terminal.v1.ListDatabaseUsersResponse.prototype.setUsersList = function(value) { + return jspb.Message.setField(this, 1, value || []); +}; + + +/** + * @param {string} value + * @param {number=} opt_index + * @return {!proto.teleport.terminal.v1.ListDatabaseUsersResponse} returns this + */ +proto.teleport.terminal.v1.ListDatabaseUsersResponse.prototype.addUsers = function(value, opt_index) { + return jspb.Message.addToRepeatedField(this, 1, value, opt_index); +}; + + +/** + * Clears the list making it empty but non-null. + * @return {!proto.teleport.terminal.v1.ListDatabaseUsersResponse} returns this + */ +proto.teleport.terminal.v1.ListDatabaseUsersResponse.prototype.clearUsersList = function() { + return this.setUsersList([]); +}; + + + + + if (jspb.Message.GENERATE_TO_OBJECT) { /** * Creates an object representation of this proto. @@ -2660,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) { @@ -2709,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; @@ -2759,6 +3094,13 @@ proto.teleport.terminal.v1.CreateGatewayRequest.serializeBinaryToWriter = functi f ); } + f = message.getTargetSubresourceName(); + if (f.length > 0) { + writer.writeString( + 4, + f + ); + } }; @@ -2816,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. diff --git a/packages/teleterm/src/ui/AppInitializer.tsx b/packages/teleterm/src/ui/AppInitializer.tsx index 825a8fada..23913f082 100644 --- a/packages/teleterm/src/ui/AppInitializer.tsx +++ b/packages/teleterm/src/ui/AppInitializer.tsx @@ -1,6 +1,6 @@ import React, { FC, useEffect } from 'react'; import { useAppContext } from 'teleterm/ui/appContextProvider'; -import useAsync from 'teleterm/ui/useAsync'; +import { useAsync } from 'shared/hooks/useAsync'; import styled from 'styled-components'; export const AppInitializer: FC = props => { diff --git a/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/ClusterAdd.tsx b/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/ClusterAdd.tsx index 4e71339d8..2c7d0508e 100644 --- a/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/ClusterAdd.tsx +++ b/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/ClusterAdd.tsx @@ -22,7 +22,7 @@ import Validation from 'shared/components/Validation'; import { requiredField } from 'shared/components/Validation/rules'; import { DialogContent, DialogHeader } from 'design/Dialog'; import { useClusterAdd } from 'teleterm/ui/ClusterConnect/ClusterAdd/useClusterAdd'; -import { Attempt } from 'teleterm/ui/useAsync'; +import { Attempt } from 'shared/hooks/useAsync'; export function ClusterAdd(props: ClusterAddProps) { const clusterAdd = useClusterAdd(props); diff --git a/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/useClusterAdd.tsx b/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/useClusterAdd.tsx index 6e91c47c6..6aa67a742 100644 --- a/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/useClusterAdd.tsx +++ b/packages/teleterm/src/ui/ClusterConnect/ClusterAdd/useClusterAdd.tsx @@ -1,5 +1,5 @@ import { useAppContext } from 'teleterm/ui/appContextProvider'; -import useAsync from 'teleterm/ui/useAsync'; +import { useAsync } from 'shared/hooks/useAsync'; import { ClusterAddProps, ClusterAddPresentationProps } from './ClusterAdd'; export function useClusterAdd( diff --git a/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx b/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx index 3f1fd8fab..510dbce6b 100644 --- a/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx +++ b/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/ClusterLogin.story.tsx @@ -16,7 +16,7 @@ limitations under the License. import React from 'react'; import * as types from 'teleterm/ui/services/clusters/types'; -import { Attempt } from 'teleterm/ui/useAsync'; +import { Attempt } from 'shared/hooks/useAsync'; import { ClusterLoginPresentation } from './ClusterLogin'; export default { diff --git a/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLogin.tsx b/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLogin.tsx index c54de103b..a635e2817 100644 --- a/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLogin.tsx +++ b/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/FormLogin/FormLogin.tsx @@ -25,7 +25,7 @@ import { requiredToken, requiredField, } from 'shared/components/Validation/rules'; -import { Attempt } from 'teleterm/ui/useAsync'; +import { Attempt } from 'shared/hooks/useAsync'; import createMfaOptions, { MfaOption } from 'shared/utils/createMfaOptions'; import * as types from 'teleterm/ui/services/clusters/types'; import SSOButtonList from './SsoButtons'; diff --git a/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/useClusterLogin.ts b/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/useClusterLogin.ts index 759812d13..1fed4c20b 100644 --- a/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/useClusterLogin.ts +++ b/packages/teleterm/src/ui/ClusterConnect/ClusterLogin/useClusterLogin.ts @@ -17,7 +17,7 @@ import { useState, useEffect, useRef } from 'react'; import * as types from 'teleterm/ui/services/clusters/types'; import { useAppContext } from 'teleterm/ui/appContextProvider'; -import useAsync from 'teleterm/ui/useAsync'; +import { useAsync } from 'shared/hooks/useAsync'; export default function useClusterLogin(props: Props) { const { onSuccess, clusterUri } = props; diff --git a/packages/teleterm/src/ui/ClusterLogout/useClusterLogout.ts b/packages/teleterm/src/ui/ClusterLogout/useClusterLogout.ts index c0ee5da27..2f994db73 100644 --- a/packages/teleterm/src/ui/ClusterLogout/useClusterLogout.ts +++ b/packages/teleterm/src/ui/ClusterLogout/useClusterLogout.ts @@ -1,5 +1,5 @@ import { useAppContext } from '../appContextProvider'; -import useAsync from '../useAsync'; +import { useAsync } from 'shared/hooks/useAsync'; import { useEffect } from 'react'; export function useClusterLogout({ clusterUri, onClose, clusterTitle }: Props) { @@ -18,6 +18,7 @@ export function useClusterLogout({ clusterUri, onClose, clusterTitle }: Props) { await ctx.workspacesService.setActiveWorkspace(null); } } + ctx.workspacesService.removeWorkspace(clusterUri); }); useEffect(() => { diff --git a/packages/teleterm/src/ui/Document/Document.tsx b/packages/teleterm/src/ui/Document/Document.tsx index c223f752b..5470faae2 100644 --- a/packages/teleterm/src/ui/Document/Document.tsx +++ b/packages/teleterm/src/ui/Document/Document.tsx @@ -24,7 +24,7 @@ const Document: React.FC<{ }> = ({ visible, children, onContextMenu, ...styles }) => ( { border: 'none', backgroundColor: 'inherit', flexShrink: '0', + borderRadius: '4px', '&:hover, &:focus': { - background: props.theme.colors.primary.light, + background: props.theme.colors.primary.main, }, ...space(props), ...width(props), diff --git a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/Applications.tsx b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/Applications.tsx index 7bb5b0800..8add6b417 100644 --- a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/Applications.tsx +++ b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Applications/Applications.tsx @@ -29,7 +29,8 @@ import { deepOrange, blueGrey, } from 'design/theme/palette'; -import Table, { Cell } from 'design/DataTable'; +import { Table } from 'teleterm/ui/components/Table'; +import { Cell } from 'design/DataTable'; import * as types from 'teleterm/ui/services/clusters/types'; import { useApps, State } from './useApps'; import { renderLabelCell } from '../renderLabelCell'; diff --git a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/ClusterSearch.tsx b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/ClusterSearch.tsx index 1cf821792..a470e6368 100644 --- a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/ClusterSearch.tsx +++ b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/ClusterSearch.tsx @@ -38,21 +38,25 @@ export default function ClusterSearch(props: Props) { const Input = styled.input(props => { const { theme } = props; return { - background: theme.colors.primary.light, + background: theme.colors.primary.main, boxSizing: 'border-box', color: theme.colors.text.primary, width: '100%', minHeight: '30px', minWidth: '300px', - border: 'none', outline: 'none', borderRadius: '4px', + border: '1px solid transparent', padding: '2px 12px', '&:hover, &:focus': { color: theme.colors.primary.contrastText, - background: theme.colors.primary.lighter, + borderColor: theme.colors.primary.lighter, opacity: 1, }, + '::placeholder': { + opacity: 1, + color: theme.colors.text.secondary, + }, ...space(props), ...width(props), diff --git a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/Databases.tsx b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/Databases.tsx index 9636619f3..9c1f6b4c9 100644 --- a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/Databases.tsx +++ b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/Databases.tsx @@ -14,13 +14,18 @@ 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, { Cell } from 'design/DataTable'; +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'; +import { NotificationsService } from 'teleterm/ui/services/notifications'; export default function Container() { const state = useDatabases(); @@ -48,8 +53,14 @@ function DatabaseList(props: State) { }, { altKey: 'connect-btn', - render: db => - renderConnectButton(user => props.connect(db.uri, user)), + render: db => ( + + props.connect(db.uri, dbUser, dbName) + } + /> + ), }, ]} pagination={{ pageSize: 100, pagerPosition: 'bottom' }} @@ -59,24 +70,87 @@ function DatabaseList(props: State) { ); } -function renderConnectButton(onConnect: (user: string) => void) { +function ConnectButton({ + dbUri, + onConnect, +}: { + dbUri: string; + onConnect: (dbUser: string, dbName: string) => void; +}) { + const { clustersService, notificationsService } = useAppContext(); + const dbNameMenuLoginRef = useRef(); + const [dbUser, setDbUser] = useState(); + return ( - []} - onSelect={(_, user) => onConnect(user)} - transformOrigin={{ - vertical: 'top', - horizontal: 'right', - }} - anchorOrigin={{ - vertical: 'center', - horizontal: 'right', - }} - /> + + {/* The db name MenuLogin will be overlayed by the db username MenuLogin, which the user + should interact with first. */} + []} + onSelect={(_, dbName) => onConnect(dbUser, dbName)} + transformOrigin={transformOrigin} + anchorOrigin={anchorOrigin} + /> + + getDatabaseUsers(dbUri, clustersService, notificationsService) + } + onSelect={(_, user) => { + setDbUser(user); + dbNameMenuLoginRef.current.open(); + }} + transformOrigin={transformOrigin} + anchorOrigin={anchorOrigin} + /> + ); } + +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, + notificationsService: NotificationsService +) { + try { + const dbUsers = await clustersService.getDbUsers(dbUri); + return dbUsers.map(user => ({ login: user, url: '' })); + } catch (e) { + // Emitting a warning instead of an error here because fetching those username suggestions is + // not the most important part of the app. + notificationsService.notifyWarning({ + title: 'Could not fetch database usernames', + description: e.message, + }); + + throw e; + } +} diff --git a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/useDatabases.ts b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/useDatabases.ts index f8fa04ced..13f44a397 100644 --- a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/useDatabases.ts +++ b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Databases/useDatabases.ts @@ -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 = @@ -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); diff --git a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Kubes/Kubes.tsx b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Kubes/Kubes.tsx index d292ef9b1..a3beac763 100644 --- a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Kubes/Kubes.tsx +++ b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Kubes/Kubes.tsx @@ -16,7 +16,8 @@ limitations under the License. import React from 'react'; import { useKubes, State } from './useKubes'; -import Table, { Cell } from 'design/DataTable'; +import { Table } from 'teleterm/ui/components/Table'; +import { Cell } from 'design/DataTable'; import { ButtonBorder } from 'design'; import { renderLabelCell } from '../renderLabelCell'; import { Danger } from 'design/Alert'; diff --git a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/MenuLoginTheme.tsx b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/MenuLoginTheme.tsx index bbedd7fdd..76abe6179 100644 --- a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/MenuLoginTheme.tsx +++ b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/MenuLoginTheme.tsx @@ -6,11 +6,20 @@ const menuLoginTheme = { ...theme, colors: { ...theme.colors, - subtle:theme.colors.primary.lighter, - light: theme.colors.primary.dark, + subtle: theme.colors.primary.lighter, + light: theme.colors.primary.light, + primary: { + ...theme.colors.primary, + lighter: theme.colors.primary.darker, + }, + secondary: { + ...theme.colors.secondary, + dark: theme.colors.text.primary, + }, grey: { [50]: 'rgba(255,255,255,0.05)', [900]: theme.colors.text.primary, + [100]: theme.colors.text.secondary, }, link: theme.colors.text.primary, }, diff --git a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Servers/Servers.tsx b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Servers/Servers.tsx index 100211aa5..adad34918 100644 --- a/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Servers/Servers.tsx +++ b/packages/teleterm/src/ui/DocumentCluster/ClusterResources/Servers/Servers.tsx @@ -17,7 +17,8 @@ limitations under the License. import React from 'react'; import { useServers, State } from './useServers'; import * as types from 'teleterm/ui/services/clusters/types'; -import Table, { Cell } from 'design/DataTable'; +import { Table } from 'teleterm/ui/components/Table'; +import { Cell } from 'design/DataTable'; import { renderLabelCell } from '../renderLabelCell'; import { MenuLogin } from 'shared/components/MenuLogin'; import { MenuLoginTheme } from '../MenuLoginTheme'; diff --git a/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts b/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts index cfc1a5444..82474601e 100644 --- a/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts +++ b/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts @@ -17,7 +17,7 @@ limitations under the License. import React, { useEffect } from 'react'; import { useAppContext } from 'teleterm/ui/appContextProvider'; import * as types from 'teleterm/ui/services/workspacesService'; -import useAsync from 'teleterm/ui/useAsync'; +import { useAsync } from 'shared/hooks/useAsync'; import { useWorkspaceDocumentsService } from 'teleterm/ui/Documents'; export default function useGateway(doc: types.DocumentGateway) { @@ -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, { diff --git a/packages/teleterm/src/ui/DocumentTerminal/Terminal/Terminal.tsx b/packages/teleterm/src/ui/DocumentTerminal/Terminal/Terminal.tsx index 602b3b283..040784834 100644 --- a/packages/teleterm/src/ui/DocumentTerminal/Terminal/Terminal.tsx +++ b/packages/teleterm/src/ui/DocumentTerminal/Terminal/Terminal.tsx @@ -79,13 +79,8 @@ type Props = { onEnterKey?(): void; }; -const StyledXterm = styled(Box)( - props => ` - height: 100%; - width: 100%; - background-color: ${props.theme.colors.bgTerminal}; - .terminal .xterm-viewport { - background-color: ${props.theme.bgTerminal}; - } - }` -); +const StyledXterm = styled(Box)` + height: 100%; + width: 100%; + background-color: ${props => props.theme.colors.primary.darker}; +`; diff --git a/packages/teleterm/src/ui/DocumentTerminal/Terminal/ctrl.ts b/packages/teleterm/src/ui/DocumentTerminal/Terminal/ctrl.ts index 8892dbb90..82d30281b 100644 --- a/packages/teleterm/src/ui/DocumentTerminal/Terminal/ctrl.ts +++ b/packages/teleterm/src/ui/DocumentTerminal/Terminal/ctrl.ts @@ -52,7 +52,7 @@ export default class TtyTerminal { cursorBlink: false, fontFamily: this.options.fontFamily, theme: { - background: theme.colors.bgTerminal, + background: theme.colors.primary.darker, }, windowOptions: { setWinSizeChars: true, diff --git a/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts b/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts index 1b67df223..0fe9da623 100644 --- a/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts +++ b/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts @@ -20,7 +20,7 @@ import { IAppContext } from 'teleterm/ui/types'; import * as types from 'teleterm/ui/services/workspacesService'; import { DocumentsService } from 'teleterm/ui/services/workspacesService'; import { PtyCommand, PtyProcess } from 'teleterm/services/pty/types'; -import useAsync from 'teleterm/ui/useAsync'; +import { useAsync } from 'shared/hooks/useAsync'; import { useWorkspaceDocumentsService } from 'teleterm/ui/Documents'; export default function useDocumentTerminal(doc: Doc) { diff --git a/packages/teleterm/src/ui/Documents/KeyboardShortcutsPanel.tsx b/packages/teleterm/src/ui/Documents/KeyboardShortcutsPanel.tsx index 72cda6b62..5aa2bef78 100644 --- a/packages/teleterm/src/ui/Documents/KeyboardShortcutsPanel.tsx +++ b/packages/teleterm/src/ui/Documents/KeyboardShortcutsPanel.tsx @@ -56,7 +56,7 @@ function Entry(props: { title: string; shortcut: string }) { {props.title} - + {props.shortcut} diff --git a/packages/teleterm/src/ui/QuickInput/QuickInput.tsx b/packages/teleterm/src/ui/QuickInput/QuickInput.tsx index 6940c9921..1cd92fed6 100644 --- a/packages/teleterm/src/ui/QuickInput/QuickInput.tsx +++ b/packages/teleterm/src/ui/QuickInput/QuickInput.tsx @@ -198,6 +198,7 @@ const Input = styled.input(props => { }, '&:focus': { borderColor: theme.colors.secondary.main, + backgroundColor: theme.colors.primary.darker, '::placeholder': { color: theme.colors.text.placeholder, }, diff --git a/packages/teleterm/src/ui/QuickInput/QuickInputList/QuickInputList.tsx b/packages/teleterm/src/ui/QuickInput/QuickInputList/QuickInputList.tsx index 9d79524fc..2c540775a 100644 --- a/packages/teleterm/src/ui/QuickInput/QuickInputList/QuickInputList.tsx +++ b/packages/teleterm/src/ui/QuickInput/QuickInputList/QuickInputList.tsx @@ -158,8 +158,8 @@ const StyledItem = styled.div(({ theme, $active }) => { padding: '2px 8px', color: theme.colors.primary.contrastText, background: $active - ? theme.colors.primary.lighter - : theme.colors.primary.dark, + ? theme.colors.primary.main + : theme.colors.primary.darker, }; }); diff --git a/packages/teleterm/src/ui/TabHost/TabHost.test.tsx b/packages/teleterm/src/ui/TabHost/TabHost.test.tsx index 672ca9b5e..02a7d0b4e 100644 --- a/packages/teleterm/src/ui/TabHost/TabHost.test.tsx +++ b/packages/teleterm/src/ui/TabHost/TabHost.test.tsx @@ -14,6 +14,7 @@ import { TabContextMenuOptions, } from 'teleterm/mainProcess/types'; import { ClustersService } from 'teleterm/ui/services/clusters'; +import AppContext from 'teleterm/ui/appContext'; function getMockDocuments(): Document[] { return [ @@ -94,20 +95,20 @@ function getTestSetup({ documents }: { documents: Document[] }) { }, }; + const appContext: AppContext = { + // @ts-expect-error - using mocks + keyboardShortcutsService, + // @ts-expect-error - using mocks + mainProcessClient, + // @ts-expect-error - using mocks + clustersService, + // @ts-expect-error - using mocks + workspacesService, + }; + const utils = render( - - + + ); diff --git a/packages/teleterm/src/ui/TabHost/TabHost.tsx b/packages/teleterm/src/ui/TabHost/TabHost.tsx index 3a1704f88..3820c17ba 100644 --- a/packages/teleterm/src/ui/TabHost/TabHost.tsx +++ b/packages/teleterm/src/ui/TabHost/TabHost.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useMemo } from 'react'; +import React from 'react'; import styled from 'styled-components'; import { Flex } from 'design'; import { useAppContext } from 'teleterm/ui/appContextProvider'; @@ -24,31 +24,36 @@ import { useTabShortcuts } from './useTabShortcuts'; import { DocumentsRenderer } from 'teleterm/ui/Documents'; import { useNewTabOpener } from './useNewTabOpener'; import { ClusterConnectPanel } from './ClusterConnectPanel/ClusterConnectPanel'; +import AppContext from 'teleterm/ui/appContext'; export function TabHostContainer() { const ctx = useAppContext(); ctx.workspacesService.useState(); - const isRootClusterSelected = !!ctx.workspacesService.getRootClusterUri(); - return useMemo(() => { - if (isRootClusterSelected) { - return ; - } - return ; - }, [isRootClusterSelected]); + if (isRootClusterSelected) { + return ; + } + return ; } -export function TabHost() { - const ctx = useAppContext(); +export function TabHost({ ctx }: { ctx: AppContext }) { const documentsService = ctx.workspacesService.getActiveWorkspaceDocumentService(); - const activeDocument = documentsService.getActive(); - const { openClusterTab } = useNewTabOpener(); - ctx.workspacesService.useState(); - - // enable keyboard shortcuts - useTabShortcuts(); + const activeDocument = documentsService?.getActive(); + + // TODO(gzdunek): make workspace refactor - it'd be helpful to have a single object that fully represents a workspace + const { openClusterTab } = useNewTabOpener({ + documentsService, + localClusterUri: + ctx.workspacesService.getActiveWorkspace()?.localClusterUri, + }); + + useTabShortcuts({ + documentsService, + localClusterUri: + ctx.workspacesService.getActiveWorkspace()?.localClusterUri, + }); function handleTabClick(doc: types.Document) { documentsService.open(doc.uri); diff --git a/packages/teleterm/src/ui/TabHost/useNewTabOpener.ts b/packages/teleterm/src/ui/TabHost/useNewTabOpener.ts index 0d8b933b9..ef1390dee 100644 --- a/packages/teleterm/src/ui/TabHost/useNewTabOpener.ts +++ b/packages/teleterm/src/ui/TabHost/useNewTabOpener.ts @@ -1,15 +1,13 @@ -import { useAppContext } from 'teleterm/ui/appContextProvider'; import { useCallback } from 'react'; +import { DocumentsService } from 'teleterm/ui/services/workspacesService'; -export function useNewTabOpener() { - const ctx = useAppContext(); - - const documentsService = - ctx.workspacesService.getActiveWorkspaceDocumentService(); - - const localClusterUri = - ctx.workspacesService.getActiveWorkspace()?.localClusterUri; - +export function useNewTabOpener({ + documentsService, + localClusterUri, +}: { + documentsService: DocumentsService; + localClusterUri: string; +}) { const openClusterTab = useCallback(() => { if (localClusterUri) { const clusterDocument = documentsService.createClusterDocument({ diff --git a/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx b/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx index 3d50443ff..2e63adb66 100644 --- a/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx +++ b/packages/teleterm/src/ui/TabHost/useTabShortcuts.test.tsx @@ -13,6 +13,7 @@ import { } from 'teleterm/ui/services/keyboardShortcuts'; import AppContextProvider from 'teleterm/ui/appContextProvider'; import { WorkspacesService } from 'teleterm/ui/services/workspacesService'; +import AppContext from 'teleterm/ui/appContext'; function getMockDocuments(): Document[] { return [ @@ -37,6 +38,7 @@ function getMockDocuments(): Document[] { title: 'Test 4', gatewayUri: '', targetUri: '', + targetName: 'foobar', targetUser: 'foo', }, { @@ -45,6 +47,7 @@ function getMockDocuments(): Document[] { title: 'Test 5', gatewayUri: '', targetUri: '', + targetName: 'foobar', targetUser: 'bar', }, { @@ -85,7 +88,8 @@ function getTestSetup({ documents }: { documents: Document[] }) { }, }; - const docsService: Partial = { + // @ts-expect-error - using mocks + const docsService: DocumentsService = { getDocuments(): Document[] { return documents; }, @@ -103,7 +107,6 @@ function getTestSetup({ documents }: { documents: Document[] }) { }; const workspacesService: Partial = { - // @ts-expect-error - using mocks getActiveWorkspaceDocumentService() { return docsService; }, @@ -121,16 +124,26 @@ function getTestSetup({ documents }: { documents: Document[] }) { }, }; - renderHook(() => useTabShortcuts(), { - wrapper: props => ( - - {props.children} - - ), - }); + const appContext: AppContext = { + // @ts-expect-error - using mocks + keyboardShortcutsService, + // @ts-expect-error - using mocks + workspacesService, + }; + renderHook( + () => + useTabShortcuts({ + documentsService: docsService, + localClusterUri: workspacesService.getActiveWorkspace().localClusterUri, + }), + { + wrapper: props => ( + + {props.children} + + ), + } + ); return { emitKeyboardShortcutEvent: eventEmitter, diff --git a/packages/teleterm/src/ui/TabHost/useTabShortcuts.ts b/packages/teleterm/src/ui/TabHost/useTabShortcuts.ts index b738ce623..2c22da1c5 100644 --- a/packages/teleterm/src/ui/TabHost/useTabShortcuts.ts +++ b/packages/teleterm/src/ui/TabHost/useTabShortcuts.ts @@ -19,18 +19,23 @@ import { KeyboardShortcutHandlers, useKeyboardShortcuts, } from 'teleterm/ui/services/keyboardShortcuts'; -import { useAppContext } from 'teleterm/ui/appContextProvider'; import { DocumentsService } from 'teleterm/ui/services/workspacesService'; import { useNewTabOpener } from 'teleterm/ui/TabHost/useNewTabOpener'; -export function useTabShortcuts() { - const { workspacesService } = useAppContext(); - workspacesService.useState(); - const documentService = workspacesService.getActiveWorkspaceDocumentService(); - const { openClusterTab } = useNewTabOpener(); +export function useTabShortcuts({ + documentsService, + localClusterUri, +}: { + documentsService: DocumentsService; + localClusterUri: string; +}) { + const { openClusterTab } = useNewTabOpener({ + documentsService, + localClusterUri, + }); const tabsShortcuts = useMemo( - () => buildTabsShortcuts(documentService, openClusterTab), - [documentService] + () => buildTabsShortcuts(documentsService, openClusterTab), + [documentsService, openClusterTab] ); useKeyboardShortcuts(tabsShortcuts); } diff --git a/packages/teleterm/src/ui/Tabs/TabItem.tsx b/packages/teleterm/src/ui/Tabs/TabItem.tsx index 25475f6f0..50132cce6 100644 --- a/packages/teleterm/src/ui/Tabs/TabItem.tsx +++ b/packages/teleterm/src/ui/Tabs/TabItem.tsx @@ -48,28 +48,32 @@ export function TabItem(props: TabItemProps) { }; return ( - <> - - - - {name} - - - {onClose && ( - - - - )} - - + + + {name} + + {onClose && ( + + + + )} + ); } @@ -79,6 +83,7 @@ const StyledTabItem = styled.div(({ theme, active, dragging, canDrag }) => { flexBasis: '0', flexGrow: '1', opacity: '1', + color: theme.colors.text.secondary, alignItems: 'center', minWidth: '0', height: '100%', @@ -91,7 +96,7 @@ const StyledTabItem = styled.div(({ theme, active, dragging, canDrag }) => { }; if (active) { - styles['backgroundColor'] = theme.colors.terminalDark; + styles['backgroundColor'] = theme.colors.primary.darker; styles['color'] = theme.colors.secondary.contrastText; styles['transition'] = 'none'; } @@ -107,8 +112,8 @@ const StyledTabItem = styled.div(({ theme, active, dragging, canDrag }) => { return styles; }); -const StyledTabButton = styled.button` - display: flex; +const Title = styled(Text)` + display: block; cursor: pointer; outline: none; color: inherit; @@ -116,7 +121,7 @@ const StyledTabButton = styled.button` line-height: 32px; background-color: transparent; white-space: nowrap; - padding: 0 12px; + padding-left: 12px; border: none; min-width: 0; width: 100%; diff --git a/packages/teleterm/src/ui/Tabs/Tabs.tsx b/packages/teleterm/src/ui/Tabs/Tabs.tsx index a83f9d142..756c554fc 100644 --- a/packages/teleterm/src/ui/Tabs/Tabs.tsx +++ b/packages/teleterm/src/ui/Tabs/Tabs.tsx @@ -63,16 +63,11 @@ export function Tabs(props: Props) { : $emptyTab; return ( - + {$items} props.theme.colors.primary.main}; min-height: 32px; border-radius: 4px; display: flex; diff --git a/packages/teleterm/src/ui/ThemeProvider/globals.ts b/packages/teleterm/src/ui/ThemeProvider/globals.ts index 9bac829ec..0eb53d47e 100644 --- a/packages/teleterm/src/ui/ThemeProvider/globals.ts +++ b/packages/teleterm/src/ui/ThemeProvider/globals.ts @@ -25,7 +25,7 @@ const GlobalStyle = createGlobalStyle` body { margin: 0; - background-color: ${props => props.theme.colors.primary.main}; + background-color: ${props => props.theme.colors.primary.darker}; color: ${props => props.theme.colors.light}; padding: 0; } @@ -34,20 +34,6 @@ const GlobalStyle = createGlobalStyle` font-family: ${props => props.theme.font}; } - // custom scrollbars - ::-webkit-scrollbar { - width: 8px; - height: 8px; - } - - ::-webkit-scrollbar-thumb { - background: #757575; - } - - ::-webkit-scrollbar-corner { - background: rgba(0,0,0,0.5); - } - // remove dotted Firefox outline button, a { outline: 0; diff --git a/packages/teleterm/src/ui/ThemeProvider/theme.ts b/packages/teleterm/src/ui/ThemeProvider/theme.ts index 50a75e231..2feed3ae1 100644 --- a/packages/teleterm/src/ui/ThemeProvider/theme.ts +++ b/packages/teleterm/src/ui/ThemeProvider/theme.ts @@ -42,11 +42,12 @@ const colors = { light: '#FFFFFF', primary: { - main: '#01172C', - light: '#091E32', - lighter: '#0E2337', - dark: '#011223', - darker: '#010e1a', + darker: '#0C143D', + dark: '#131B43', + main: '#182047', + light: '#222C59', + lighter: '#2D3761', + contrastText: '#FFFFFF', }, secondary: { @@ -94,9 +95,6 @@ const colors = { subtle: '#CFD8DC', link: '#039BE5', - terminal: '#28FE14', - terminalDark: '#011223', - bgTerminal: '#01172C', danger: pink.A400, highlight: yellow[50], diff --git a/packages/teleterm/src/ui/TopBar/Clusters/Clusters.tsx b/packages/teleterm/src/ui/TopBar/Clusters/Clusters.tsx index aa4a33a73..66fe84b26 100644 --- a/packages/teleterm/src/ui/TopBar/Clusters/Clusters.tsx +++ b/packages/teleterm/src/ui/TopBar/Clusters/Clusters.tsx @@ -64,5 +64,5 @@ export function Clusters() { } const Container = styled(Box)` - background: ${props => props.theme.colors.primary.dark}; + background: ${props => props.theme.colors.primary.light}; `; diff --git a/packages/teleterm/src/ui/TopBar/Clusters/ClustersFilterableList/ClusterItem.tsx b/packages/teleterm/src/ui/TopBar/Clusters/ClustersFilterableList/ClusterItem.tsx index a41d0aa6a..551f38b28 100644 --- a/packages/teleterm/src/ui/TopBar/Clusters/ClustersFilterableList/ClusterItem.tsx +++ b/packages/teleterm/src/ui/TopBar/Clusters/ClustersFilterableList/ClusterItem.tsx @@ -73,7 +73,7 @@ function getBackgroundColor(props) { return props.theme.colors.secondary.main; } if (props.isActive) { - return 'rgba(255, 255, 255, 0.05)'; + return props.theme.colors.secondary.lighter; } } @@ -82,6 +82,6 @@ function getHoverBackgroundColor(props) { return props.theme.colors.secondary.light; } if (props.isActive) { - return 'rgba(255, 255, 255, 0.05)'; + return props.theme.colors.secondary.lighter; } } diff --git a/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts b/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts index e45c2b3c6..1b5c4e8d0 100644 --- a/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts +++ b/packages/teleterm/src/ui/TopBar/Clusters/useClusters.ts @@ -16,12 +16,9 @@ export function useClusters() { const rootClusterUri = workspacesService.getRootClusterUri(); const localClusterUri = workspacesService.getActiveWorkspace()?.localClusterUri; - const items = rootClusterUri - ? [ - clustersService.findCluster(rootClusterUri), - ...findLeaves(rootClusterUri), - ] - : []; + const rootCluster = clustersService.findCluster(rootClusterUri); + const items = + (rootCluster && [rootCluster, ...findLeaves(rootClusterUri)]) || []; return { hasLeaves: items.some(i => i.leaf), diff --git a/packages/teleterm/src/ui/TopBar/Connections/Connections.story.tsx b/packages/teleterm/src/ui/TopBar/Connections/Connections.story.tsx index d1a3e025e..c0d234158 100644 --- a/packages/teleterm/src/ui/TopBar/Connections/Connections.story.tsx +++ b/packages/teleterm/src/ui/TopBar/Connections/Connections.story.tsx @@ -24,6 +24,7 @@ export function ExpanderConnections() { connected: true, kind: 'connection.gateway', title: 'graves', + targetName: 'graves', id: '68b6a281', targetUri: 'brock', port: '22', diff --git a/packages/teleterm/src/ui/TopBar/Connections/Connections.tsx b/packages/teleterm/src/ui/TopBar/Connections/Connections.tsx index 65bd0d627..1b863b9c4 100644 --- a/packages/teleterm/src/ui/TopBar/Connections/Connections.tsx +++ b/packages/teleterm/src/ui/TopBar/Connections/Connections.tsx @@ -60,5 +60,5 @@ export function Connections() { } const Container = styled(Box)` - background: ${props => props.theme.colors.primary.dark}; + background: ${props => props.theme.colors.primary.light}; `; diff --git a/packages/teleterm/src/ui/TopBar/Connections/ConnectionsFilterableList/ConnectionItem.tsx b/packages/teleterm/src/ui/TopBar/Connections/ConnectionsFilterableList/ConnectionItem.tsx index c6514df8e..feae9ade7 100644 --- a/packages/teleterm/src/ui/TopBar/Connections/ConnectionsFilterableList/ConnectionItem.tsx +++ b/packages/teleterm/src/ui/TopBar/Connections/ConnectionsFilterableList/ConnectionItem.tsx @@ -48,15 +48,24 @@ export function ConnectionItem(props: ConnectionItemProps) { height: unset; `} > - + - +
{props.item.title} @@ -72,13 +80,11 @@ export function ConnectionItem(props: ConnectionItemProps) { {props.item.clusterName} - +
props.theme.colors.primary.lighter}; + background: ${props => props.theme.colors.primary.light}; padding: 9px; width: 30px; height: 30px; diff --git a/packages/teleterm/src/ui/TopBar/Identity/EmptyIdentityList/EmptyIdentityList.tsx b/packages/teleterm/src/ui/TopBar/Identity/EmptyIdentityList/EmptyIdentityList.tsx index 5a4ab8624..7a26c6bf1 100644 --- a/packages/teleterm/src/ui/TopBar/Identity/EmptyIdentityList/EmptyIdentityList.tsx +++ b/packages/teleterm/src/ui/TopBar/Identity/EmptyIdentityList/EmptyIdentityList.tsx @@ -1,16 +1,13 @@ import React from 'react'; import { ButtonPrimary, Flex, Text } from 'design'; -import { useAppContext } from 'teleterm/ui/appContextProvider'; import Image from 'design/Image'; import clusterPng from './clusters.png'; -export function EmptyIdentityList() { - const ctx = useAppContext(); - - function handleConnect() { - ctx.commandLauncher.executeCommand('cluster-connect', {}); - } +interface EmptyIdentityListProps { + onConnect(): void; +} +export function EmptyIdentityList(props: EmptyIdentityListProps) { return ( Lorem ipsum dolor sit amet, consectetur adipiscing elit - + Connect diff --git a/packages/teleterm/src/ui/TopBar/Identity/Identity.tsx b/packages/teleterm/src/ui/TopBar/Identity/Identity.tsx index 2e587e466..32f2404e0 100644 --- a/packages/teleterm/src/ui/TopBar/Identity/Identity.tsx +++ b/packages/teleterm/src/ui/TopBar/Identity/Identity.tsx @@ -23,6 +23,15 @@ export function Identity() { setIsPopoverOpened(wasOpened => !wasOpened); }, [setIsPopoverOpened]); + function withClose any>( + fn: T + ): (...args: Parameters) => ReturnType { + return (...args) => { + setIsPopoverOpened(false); + return fn(...args); + }; + } + useKeyboardShortcuts( useMemo( () => ({ @@ -55,12 +64,12 @@ export function Identity() { ) : ( - + )} @@ -69,5 +78,5 @@ export function Identity() { } const Container = styled(Box)` - background: ${props => props.theme.colors.primary.dark}; + background: ${props => props.theme.colors.primary.light}; `; diff --git a/packages/teleterm/src/ui/TopBar/Identity/IdentityList/AddNewClusterItem.tsx b/packages/teleterm/src/ui/TopBar/Identity/IdentityList/AddNewClusterItem.tsx index 70308583c..6032a703b 100644 --- a/packages/teleterm/src/ui/TopBar/Identity/IdentityList/AddNewClusterItem.tsx +++ b/packages/teleterm/src/ui/TopBar/Identity/IdentityList/AddNewClusterItem.tsx @@ -25,6 +25,8 @@ export function AddNewClusterItem(props: AddNewClusterItemProps) { } const StyledListItem = styled(ListItem)` + border-radius: 0; + height: 38px; justify-content: center; color: ${props => props.theme.colors.text.secondary}; `; diff --git a/packages/teleterm/src/ui/TopBar/Identity/IdentityList/IdentityList.tsx b/packages/teleterm/src/ui/TopBar/Identity/IdentityList/IdentityList.tsx index 460a616a3..8c72555ff 100644 --- a/packages/teleterm/src/ui/TopBar/Identity/IdentityList/IdentityList.tsx +++ b/packages/teleterm/src/ui/TopBar/Identity/IdentityList/IdentityList.tsx @@ -20,10 +20,10 @@ interface IdentityListProps { export function IdentityList(props: IdentityListProps) { return ( - + {props.loggedInUser && ( <> - + {props.loggedInUser.name} @@ -36,7 +36,7 @@ export function IdentityList(props: IdentityListProps) { )} {focusGrabber} - + {props.clusters.map((i, index) => ( - + props.theme.colors.primary.lighter}; - margin: 0 16px; height: 1px; `; diff --git a/packages/teleterm/src/ui/TopBar/Identity/IdentityList/IdentityListItem.tsx b/packages/teleterm/src/ui/TopBar/Identity/IdentityList/IdentityListItem.tsx index 1f3a7fddc..d861f88c8 100644 --- a/packages/teleterm/src/ui/TopBar/Identity/IdentityList/IdentityListItem.tsx +++ b/packages/teleterm/src/ui/TopBar/Identity/IdentityList/IdentityListItem.tsx @@ -32,6 +32,7 @@ export function IdentityListItem(props: IdentityListItemProps) { return ( props.theme.colors.primary.main}; display: grid; grid-template-columns: 1fr 4fr 2fr; width: 100%; diff --git a/packages/teleterm/src/ui/appContext.ts b/packages/teleterm/src/ui/appContext.ts index 4e95accdf..1560658ff 100644 --- a/packages/teleterm/src/ui/appContext.ts +++ b/packages/teleterm/src/ui/appContext.ts @@ -55,7 +55,7 @@ export default class AppContext { this.modalsService, this.clustersService, this.notificationsService, - this.statePersistenceService, + this.statePersistenceService ); this.terminalsService = new TerminalsService(ptyServiceClient); @@ -81,9 +81,6 @@ export default class AppContext { async init(): Promise { await this.clustersService.syncRootClusters(); - const { rootClusterUri } = this.statePersistenceService.getWorkspaces(); - if (rootClusterUri) { - this.workspacesService.setActiveWorkspace(rootClusterUri); - } + this.workspacesService.restorePersistedState(); } } diff --git a/packages/teleterm/src/ui/components/FilterableList/FilterableList.tsx b/packages/teleterm/src/ui/components/FilterableList/FilterableList.tsx index 33107dd19..84c879636 100644 --- a/packages/teleterm/src/ui/components/FilterableList/FilterableList.tsx +++ b/packages/teleterm/src/ui/components/FilterableList/FilterableList.tsx @@ -66,22 +66,22 @@ const DarkInput = styled(Input)` background: inherit; border: 1px ${props => props.theme.colors.action.disabledBackground} solid; border-radius: 51px; - color: ${props => props.theme.colors.light}; + color: ${props => props.theme.colors.text.primary}; margin-bottom: 10px; font-size: 14px; - opacity: 0.6; height: 34px; + transition : border 300ms ease-out; ::placeholder { opacity: 1; + color: ${props => props.theme.colors.text.secondary}; } - & :hover { - border-color: ${props => props.theme.colors.action.active}; + &:hover { + border-color: ${props => props.theme.colors.text.secondary}; } &:focus { border-color: ${props => props.theme.colors.secondary.main}; - opacity: 1; } `; diff --git a/packages/teleterm/src/ui/components/Notifcations/Notification.tsx b/packages/teleterm/src/ui/components/Notifcations/Notification.tsx index a2450e030..909af2c26 100644 --- a/packages/teleterm/src/ui/components/Notifcations/Notification.tsx +++ b/packages/teleterm/src/ui/components/Notifcations/Notification.tsx @@ -111,7 +111,7 @@ function getRenderedContent( if (typeof content === 'string') { return ( - + props.theme.colors.primary.darker}; + background: ${props => props.theme.colors.primary.light}; min-height: 40px; width: 320px; margin-bottom: 15px; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.24); color: ${props => props.theme.colors.text.primary}; - opacity: 0.95; border-radius: 4px; - - &:hover { - opacity: 1; - cursor: pointer; - } + cursor: pointer; `; diff --git a/packages/teleterm/src/ui/components/Notifcations/Notifications.tsx b/packages/teleterm/src/ui/components/Notifcations/Notifications.tsx index 3601ae544..5d4f2c900 100644 --- a/packages/teleterm/src/ui/components/Notifcations/Notifications.tsx +++ b/packages/teleterm/src/ui/components/Notifcations/Notifications.tsx @@ -27,4 +27,5 @@ const Container = styled.div` position: fixed; bottom: 12px; right: 12px; + z-index: 10; `; diff --git a/packages/teleterm/src/ui/components/Table.tsx b/packages/teleterm/src/ui/components/Table.tsx new file mode 100644 index 000000000..963e34832 --- /dev/null +++ b/packages/teleterm/src/ui/components/Table.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { ThemeProvider, useTheme } from 'styled-components'; +import DesignTable from 'design/DataTable/Table'; + +export const Table: typeof DesignTable = props => { + const theme = useTheme(); + + return ( + + + + ); +}; + +function getTableTheme(theme) { + return { + ...theme, + colors: { + ...theme.colors, + primary: { + ...theme.colors.primary, + dark: 'rgba(255, 255, 255, 0.05)', + light: theme.colors.primary.dark, + lighter: theme.colors.primary.light, + main: theme.colors.primary.darker, + }, + link: theme.colors.text.primary, + }, + }; +} diff --git a/packages/teleterm/src/ui/services/clusters/clustersService.test.ts b/packages/teleterm/src/ui/services/clusters/clustersService.test.ts index 02c2bde39..a3ac86d22 100644 --- a/packages/teleterm/src/ui/services/clusters/clustersService.test.ts +++ b/packages/teleterm/src/ui/services/clusters/clustersService.test.ts @@ -22,6 +22,7 @@ const gatewayMock: tsh.Gateway = { localPort: '2000', protocol: 'https', targetName: 'Test', + targetSubresourceName: '', targetUser: '', targetUri: 'clusters/xxx/', cliCommand: 'psql postgres://postgres@localhost:5432/postgres', diff --git a/packages/teleterm/src/ui/services/clusters/clustersService.ts b/packages/teleterm/src/ui/services/clusters/clustersService.ts index 85f21c853..50b3b5135 100644 --- a/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -278,12 +278,28 @@ export class ClustersService extends ImmutableStore { } } + /** + * Removes cluster and its leaf clusters (if any) + */ async removeCluster(clusterUri: string) { await this.client.removeCluster(clusterUri); + const leafClustersUris = this.getClusters() + .filter( + item => + item.leaf && routing.ensureRootClusterUri(item.uri) === clusterUri + ) + .map(cluster => cluster.uri); this.setState(draft => { draft.clusters.delete(clusterUri); + leafClustersUris.forEach(leafClusterUri => { + draft.clusters.delete(leafClusterUri); + }); }); + this.removeResources(clusterUri); + leafClustersUris.forEach(leafClusterUri => { + this.removeResources(leafClusterUri); + }); } async getAuthSettings(clusterUri: string) { @@ -413,6 +429,10 @@ export class ClustersService extends ImmutableStore { return [...this.state.dbs.values()]; } + async getDbUsers(dbUri: string): Promise { + return await this.client.listDatabaseUsers(dbUri); + } + useState() { return useStore(this).state; } diff --git a/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts b/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts index 93e00c378..c669f2bc8 100644 --- a/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts +++ b/packages/teleterm/src/ui/services/connectionTracker/connectionTrackerService.ts @@ -102,6 +102,12 @@ export class ConnectionTrackerService extends ImmutableStore { this.setState(draft => { + // filter out connections from removed clusters + draft.connections = draft.connections.filter(i => { + const uri = i.kind === 'connection.gateway' ? i.targetUri : i.serverUri; + return !!this._clusterService.findClusterByResource(uri); + }); + // assign default "connected" values draft.connections.forEach(i => { if (i.kind === 'connection.gateway') { @@ -134,6 +140,9 @@ export class ConnectionTrackerService extends ImmutableStore('state'); - if (restored) { - this.state = restored; - } + saveConnectionTrackerState(connectionTracker: ConnectionTrackerState): void { + const newState: StatePersistenceState = { + ...this.getState(), + connectionTracker, + }; + this.putState(newState); } - saveConnectionTrackerState(navigatorState: ConnectionTrackerState): void { - this.state.connectionTracker = navigatorState; - this._fileStorage.put('state', this.state); + getConnectionTrackerState(): ConnectionTrackerState { + return this.getState().connectionTracker; } - getConnectionTrackerState(): ConnectionTrackerState { - return this.state.connectionTracker; + saveWorkspacesState(workspacesState: WorkspacesState): void { + const newState: StatePersistenceState = { + ...this.getState(), + workspacesState, + }; + this.putState(newState); + } + + getWorkspacesState(): WorkspacesState { + return this.getState().workspacesState; } - saveWorkspaces(workspacesState: WorkspacesState): void { - this.state.workspacesState.rootClusterUri = workspacesState.rootClusterUri; - for (let w in workspacesState.workspaces) { - if (workspacesState.workspaces[w]) { - this.state.workspacesState.workspaces[w] = { - location: workspacesState.workspaces[w].location, - localClusterUri: workspacesState.workspaces[w].localClusterUri, - documents: workspacesState.workspaces[w].documents, - }; - } - } - this._fileStorage.put('state', this.state); + private getState(): StatePersistenceState { + const defaultState: StatePersistenceState = { + connectionTracker: { + connections: [], + }, + workspacesState: { + workspaces: {}, + }, + }; + return this._fileStorage.get('state') || defaultState; } - getWorkspaces(): WorkspacesState { - return this.state.workspacesState; + private putState(state: StatePersistenceState): void { + this._fileStorage.put('state', state); } } diff --git a/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts b/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts index 3532a8d03..514ea2ab8 100644 --- a/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts +++ b/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.test.ts @@ -58,6 +58,7 @@ describe('document should be added', () => { kind: 'doc.gateway', gatewayUri: '', targetUri: '', + targetName: '', targetUser: 'foo', }; @@ -129,6 +130,7 @@ test('only gateway documents should be returned', () => { title: 'gw', gatewayUri: '', targetUri: '', + targetName: '', targetUser: 'foo', }; diff --git a/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts b/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts index a956c2e77..fb99f8f3c 100644 --- a/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts +++ b/packages/teleterm/src/ui/services/workspacesService/documentsService/documentsService.ts @@ -88,15 +88,39 @@ export class DocumentsService { }; } + /** + * If title is not present in opts, createGatewayDocument will create one based on opts. + */ createGatewayDocument(opts: CreateGatewayDocumentOpts): DocumentGateway { - const { targetUri, title, targetUser } = opts; + const { + targetUri, + targetUser, + targetName, + targetSubresourceName, + port, + gatewayUri, + } = opts; const uri = routing.getDocUri({ docId: unique() }); + let title = opts.title; + + if (!title) { + title = `${targetUser}@${targetName}`; + + if (targetSubresourceName) { + title += `/${targetSubresourceName}`; + } + } + return { uri, kind: 'doc.gateway', targetUri, targetUser, + targetName, + targetSubresourceName, + gatewayUri, title, + port, }; } diff --git a/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts b/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts index d6c407a4d..626b28c8d 100644 --- a/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts +++ b/packages/teleterm/src/ui/services/workspacesService/documentsService/types.ts @@ -56,6 +56,8 @@ export interface DocumentGateway extends DocumentBase { gatewayUri?: string; targetUri: string; targetUser: string; + targetName: string; + targetSubresourceName?: string; port?: string; } @@ -84,8 +86,10 @@ export type Document = export type CreateGatewayDocumentOpts = { gatewayUri?: string; targetUri: string; + targetName: string; targetUser: string; - title: string; + targetSubresourceName?: string; + title?: string; port?: string; }; diff --git a/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts b/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts index 65c4c8b3e..166cd8824 100644 --- a/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts +++ b/packages/teleterm/src/ui/services/workspacesService/workspacesService.ts @@ -6,6 +6,7 @@ import { ClustersService } from 'teleterm/ui/services/clusters'; import { StatePersistenceService } from 'teleterm/ui/services/statePersistence'; import { isEqual } from 'lodash'; import { NotificationsService } from 'teleterm/ui/services/notifications'; +import { routing } from 'teleterm/ui/uri'; export interface WorkspacesState { rootClusterUri?: string; @@ -107,36 +108,29 @@ export class WorkspacesService extends ImmutableStore { setState(nextState: (draftState: WorkspacesState) => WorkspacesState | void) { super.setState(nextState); - this.statePersistenceService.saveWorkspaces(this.state); + this.persistState(); } setActiveWorkspace(clusterUri: string): Promise { const setWorkspace = () => { this.setState(draftState => { - // clusterUri can be undefined - we don't want to create a workspace for it - if (clusterUri && !draftState.workspaces[clusterUri]) { - const persistedWorkspace = - this.statePersistenceService.getWorkspaces().workspaces[clusterUri]; - const defaultDocument = this.getWorkspaceDocumentService( - clusterUri - ).createClusterDocument({ clusterUri }); - - draftState.workspaces[clusterUri] = { - localClusterUri: persistedWorkspace?.localClusterUri || clusterUri, - location: defaultDocument.uri, - documents: [defaultDocument], - previous: persistedWorkspace?.documents - ? { - documents: persistedWorkspace.documents, - location: persistedWorkspace.location, - } - : undefined, - }; + // adding a new workspace + if (!draftState.workspaces[clusterUri]) { + draftState.workspaces[clusterUri] = + this.getWorkspaceDefaultState(clusterUri); } draftState.rootClusterUri = clusterUri; }); }; + // empty cluster URI - no cluster selected + if (!clusterUri) { + this.setState(draftState => { + draftState.rootClusterUri = undefined; + }); + return Promise.resolve(); + } + const cluster = this.clustersService.findCluster(clusterUri); if (!cluster) { this.notificationsService.notifyError({ @@ -167,7 +161,7 @@ export class WorkspacesService extends ImmutableStore { }) .then(() => { return new Promise(resolve => { - if (!this.canReopenPreviousDocuments(this.getWorkspace(clusterUri))) { + if (!this.getWorkspace(clusterUri)?.previous) { return resolve(); } this.modalsService.openDocumentsReopenDialog({ @@ -197,6 +191,41 @@ export class WorkspacesService extends ImmutableStore { ); } + restorePersistedState(): void { + const persistedState = this.statePersistenceService.getWorkspacesState(); + const restoredWorkspaces = this.clustersService + .getClusters() + .reduce((workspaces, cluster) => { + const persistedWorkspace = persistedState.workspaces[cluster.uri]; + const workspaceDefaultState = this.getWorkspaceDefaultState( + persistedWorkspace?.localClusterUri || cluster.uri + ); + const persistedWorkspaceDocuments = persistedWorkspace.documents; + + workspaces[cluster.uri] = { + ...workspaceDefaultState, + previous: this.canReopenPreviousDocuments({ + previousDocuments: persistedWorkspaceDocuments, + currentDocuments: workspaceDefaultState.documents, + }) + ? { + location: persistedWorkspace.location, + documents: persistedWorkspaceDocuments, + } + : undefined, + }; + return workspaces; + }, {}); + + this.setState(draftState => { + draftState.workspaces = restoredWorkspaces; + }); + + if (persistedState.rootClusterUri) { + this.setActiveWorkspace(persistedState.rootClusterUri); + } + } + private reopenPreviousDocuments(clusterUri: string): void { this.setState(draftState => { const workspace = draftState.workspaces[clusterUri]; @@ -213,17 +242,47 @@ export class WorkspacesService extends ImmutableStore { }); } - private canReopenPreviousDocuments(workspace: Workspace): boolean { + private canReopenPreviousDocuments({ + previousDocuments, + currentDocuments, + }: { + previousDocuments: Document[]; + currentDocuments: Document[]; + }): boolean { const removeUri = (documents: Document[]) => documents.map(d => ({ ...d, uri: undefined })); return ( - workspace.previous && - workspace.previous.documents?.length && - !isEqual( - removeUri(workspace.previous.documents), - removeUri(workspace.documents) - ) + previousDocuments?.length && + !isEqual(removeUri(previousDocuments), removeUri(currentDocuments)) ); } + + private getWorkspaceDefaultState(localClusterUri: string): Workspace { + const rootClusterUri = routing.ensureRootClusterUri(localClusterUri); + const defaultDocument = this.getWorkspaceDocumentService( + rootClusterUri + ).createClusterDocument({ clusterUri: localClusterUri }); + return { + localClusterUri, + location: defaultDocument.uri, + documents: [defaultDocument], + }; + } + + private persistState(): void { + const stateToSave: WorkspacesState = { + rootClusterUri: this.state.rootClusterUri, + workspaces: {}, + }; + for (let w in this.state.workspaces) { + const workspace = this.state.workspaces[w]; + stateToSave.workspaces[w] = { + localClusterUri: workspace.localClusterUri, + location: workspace.previous?.location || workspace.location, + documents: workspace.previous?.documents || workspace.documents, + }; + } + this.statePersistenceService.saveWorkspacesState(stateToSave); + } } diff --git a/packages/teleterm/src/ui/useAsync.ts b/packages/teleterm/src/ui/useAsync.ts deleted file mode 100644 index 1f753c564..000000000 --- a/packages/teleterm/src/ui/useAsync.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* -Copyright 2019 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/* eslint-disable @typescript-eslint/ban-types */ - -import React from 'react'; - -export default function useAsync(cb?: AsyncCb) { - const [state, setState] = React.useState>(() => ({ - data: null, - status: '', - statusText: '', - })); - - const run = async (...p: Parameters>) => { - try { - setState({ - ...state, - status: 'processing', - }); - - const data = (await cb.call(null, ...p)) as R; - - setState({ - ...state, - status: 'success', - data, - }); - - return [data, null] as [R, Error]; - } catch (err) { - setState({ - ...state, - status: 'error', - statusText: err.message, - data: null, - }); - - return [null, err] as [R, Error]; - } - }; - - function setAttempt(attempt: Attempt) { - setState(attempt); - } - - return [state, run, setAttempt] as const; -} - -export type Attempt = { - data?: T; - status: 'processing' | 'success' | 'error' | ''; - statusText: string; -}; - -type IsValidArg = T extends object - ? keyof T extends never - ? false - : true - : true; - -type AsyncCb = T extends (...args: any[]) => Promise - ? T - : T extends ( - a: infer A, - b: infer B, - c: infer C, - d: infer D, - e: infer E, - f: infer F, - g: infer G, - h: infer H, - i: infer I, - j: infer J - ) => Promise - ? IsValidArg extends true - ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J) => Promise - : IsValidArg extends true - ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I) => Promise - : IsValidArg extends true - ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H) => Promise - : IsValidArg extends true - ? (a: A, b: B, c: C, d: D, e: E, f: F, g: G) => Promise - : IsValidArg extends true - ? (a: A, b: B, c: C, d: D, e: E, f: F) => Promise - : IsValidArg extends true - ? (a: A, b: B, c: C, d: D, e: E) => Promise - : IsValidArg extends true - ? (a: A, b: B, c: C, d: D) => Promise - : IsValidArg extends true - ? (a: A, b: B, c: C) => Promise - : IsValidArg extends true - ? (a: A, b: B) => Promise - : IsValidArg
extends true - ? (a: A) => Promise - : () => Promise - : never;