diff --git a/web/packages/shared/hooks/useAsync.ts b/web/packages/shared/hooks/useAsync.ts index 9eeb2dca639e1..5452a9510faf2 100644 --- a/web/packages/shared/hooks/useAsync.ts +++ b/web/packages/shared/hooks/useAsync.ts @@ -91,7 +91,7 @@ export function useAsync( const isMounted = useIsMounted(); const asyncTask = useRef>(); - const run = useCallback( + const run: (...args: Args) => RunFuncReturnValue = useCallback( (...args: Args) => { setState(prevState => ({ status: 'processing', @@ -311,3 +311,5 @@ export function useDelayedRepeatedAttempt( return currentAttempt; } + +export type RunFuncReturnValue = Promise<[AttemptData, Error]>; diff --git a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx index e91917475e0ae..809b4e92e5956 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.story.tsx @@ -16,157 +16,173 @@ * along with this program. If not, see . */ +import { Meta } from '@storybook/react'; +import { ComponentProps } from 'react'; import { makeEmptyAttempt, makeProcessingAttempt, - makeErrorAttemptWithStatusText, - makeSuccessAttempt, + makeErrorAttempt, } from 'shared/hooks/useAsync'; import { makeDatabaseGateway } from 'teleterm/services/tshd/testHelpers'; -import { DocumentGateway, DocumentGatewayProps } from './DocumentGateway'; +import { OfflineGateway } from '../components/OfflineGateway'; -export default { - title: 'Teleterm/DocumentGateway', -}; +import { OnlineDocumentGateway } from './OnlineDocumentGateway'; -const gateway = makeDatabaseGateway({ - uri: '/gateways/bar', - targetName: 'sales-production', - targetUri: '/clusters/bar/dbs/foo', - targetUser: 'alice', - localAddress: 'localhost', - localPort: '1337', - protocol: 'postgres', - targetSubresourceName: 'bar', -}); -gateway.gatewayCliCommand.preview = 'connect-me-to-db-please'; - -const onlineDocumentGatewayProps: DocumentGatewayProps = { - gateway: gateway, - targetName: gateway.targetName, - defaultPort: gateway.localPort, - disconnect: async () => [undefined, null], - connected: true, - reconnect: async () => [undefined, null], - connectAttempt: makeSuccessAttempt(undefined), - disconnectAttempt: makeEmptyAttempt(), - runCliCommand: () => {}, - changeDbName: async () => [undefined, null], - changeDbNameAttempt: makeEmptyAttempt(), - changePort: async () => [undefined, null], - changePortAttempt: makeEmptyAttempt(), +type StoryProps = { + online: boolean; + // Online props. + longValues: boolean; + dbNameAttempt: 'not-started' | 'processing' | 'error'; + portAttempt: 'not-started' | 'processing' | 'error'; + disconnectAttempt: 'not-started' | 'error'; + // Offline props. + connectAttempt: 'not-started' | 'processing' | 'error'; }; -export function Online() { - return ; -} +const meta: Meta = { + title: 'Teleterm/DocumentGateway', + component: Story, + argTypes: { + // Online props. + longValues: { if: { arg: 'online' } }, + dbNameAttempt: { + if: { arg: 'online' }, + control: { type: 'radio' }, + options: ['not-started', 'processing', 'error'], + }, + portAttempt: { + if: { arg: 'online' }, + control: { type: 'radio' }, + options: ['not-started', 'processing', 'error'], + }, + disconnectAttempt: { + if: { arg: 'online' }, + control: { type: 'radio' }, + options: ['not-started', 'error'], + }, + // Offline props. + connectAttempt: { + if: { arg: 'online', truthy: false }, + control: { type: 'radio' }, + options: ['not-started', 'processing', 'error'], + }, + }, + args: { + online: true, + // Online props. + longValues: false, + dbNameAttempt: 'not-started', + portAttempt: 'not-started', + disconnectAttempt: 'not-started', + // Offline props. + connectAttempt: 'not-started', + }, +}; +export default meta; -export function OnlineWithLongValues() { - const gateway = makeDatabaseGateway({ +export function Story(props: StoryProps) { + let gateway = makeDatabaseGateway({ uri: '/gateways/bar', targetName: 'sales-production', targetUri: '/clusters/bar/dbs/foo', - targetUser: - 'quux-quuz-foo-bar-quux-quuz-foo-bar-quux-quuz-foo-bar-quux-quuz-foo-bar', + targetUser: 'alice', localAddress: 'localhost', - localPort: '13337', + localPort: '1337', protocol: 'postgres', - targetSubresourceName: - 'foo-bar-baz-quux-quuz-foo-bar-baz-quux-quuz-foo-bar-baz-quux-quuz', + targetSubresourceName: 'bar', }); - gateway.gatewayCliCommand.preview = - 'connect-me-to-db-please-baz-quux-quuz-foo-baz-quux-quuz-foo-baz-quux-quuz-foo'; - - return ( - - ); -} - -export function OnlineWithFailedDbNameAttempt() { - return ( - ( - 'Something went wrong with setting database name.' - )} - /> - ); -} - -export function OnlineWithFailedPortAttempt() { - return ( - ( - 'Something went wrong with setting port.' - )} - /> - ); -} - -export function OnlineWithFailedDbNameAndPortAttempts() { - return ( - ( - 'Something went wrong with setting database name.' - )} - changePortAttempt={makeErrorAttemptWithStatusText( - 'Something went wrong with setting port.' - )} - /> - ); -} - -export function Offline() { - return ( - - ); -} - -export function OfflineWithFailedConnectAttempt() { - return ( - ( - 'listen tcp 127.0.0.1:62414: bind: address already in use' - )} - /> - ); -} - -export function Processing() { - return ( - - ); -} + gateway.gatewayCliCommand.preview = 'connect-me-to-db-please'; + + if (!props.online) { + const offlineGatewayProps: ComponentProps = { + connectAttempt: makeEmptyAttempt(), + reconnect: () => {}, + gatewayPort: { isSupported: true, defaultPort: '1337' }, + targetName: gateway.targetName, + gatewayKind: 'database', + }; + + if (props.connectAttempt === 'error') { + offlineGatewayProps.connectAttempt = makeErrorAttempt( + new Error('listen tcp 127.0.0.1:62414: bind: address already in use') + ); + } + + if (props.connectAttempt === 'processing') { + offlineGatewayProps.connectAttempt = makeProcessingAttempt(); + } + + return ( + + ); + } + + if (props.longValues) { + gateway = makeDatabaseGateway({ + uri: '/gateways/bar', + targetName: 'sales-production', + targetUri: '/clusters/bar/dbs/foo', + targetUser: + 'quux-quuz-foo-bar-quux-quuz-foo-bar-quux-quuz-foo-bar-quux-quuz-foo-bar', + localAddress: 'localhost', + localPort: '13337', + protocol: 'postgres', + targetSubresourceName: + 'foo-bar-baz-quux-quuz-foo-bar-baz-quux-quuz-foo-bar-baz-quux-quuz', + }); + gateway.gatewayCliCommand.preview = + 'connect-me-to-db-please-baz-quux-quuz-foo-baz-quux-quuz-foo-baz-quux-quuz-foo'; + } + + const onlineDocumentGatewayProps: ComponentProps< + typeof OnlineDocumentGateway + > = { + gateway: gateway, + disconnect: async () => [undefined, null], + runCliCommand: () => {}, + changeDbName: async () => [undefined, null], + changeDbNameAttempt: makeEmptyAttempt(), + changePort: async () => [undefined, null], + changePortAttempt: makeEmptyAttempt(), + disconnectAttempt: makeEmptyAttempt(), + }; + + if (props.dbNameAttempt === 'error') { + onlineDocumentGatewayProps.changeDbNameAttempt = makeErrorAttempt( + new Error('Something went wrong with setting database name.') + ); + } + if (props.dbNameAttempt === 'processing') { + onlineDocumentGatewayProps.changeDbNameAttempt = makeProcessingAttempt(); + } + + if (props.portAttempt === 'error') { + onlineDocumentGatewayProps.changePortAttempt = makeErrorAttempt( + new Error('Something went wrong with setting port.') + ); + } + if (props.portAttempt === 'processing') { + onlineDocumentGatewayProps.changePortAttempt = makeProcessingAttempt(); + } + + if (props.disconnectAttempt === 'error') { + onlineDocumentGatewayProps.disconnectAttempt = makeErrorAttempt( + new Error('Something went wrong with closing connection.') + ); + } -export function PortProcessing() { return ( - ); } diff --git a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.tsx b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.tsx index 999c9762d44bb..47d42a3bcf731 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.tsx +++ b/web/packages/teleterm/src/ui/DocumentGateway/DocumentGateway.tsx @@ -18,53 +18,78 @@ import Document from 'teleterm/ui/Document'; import * as types from 'teleterm/ui/services/workspacesService'; +import { getCliCommandArgv0 } from 'teleterm/services/tshd/gateway'; import { OfflineGateway } from '../components/OfflineGateway'; +import { useWorkspaceContext } from '../Documents'; -import { useDocumentGateway } from './useDocumentGateway'; +import { useGateway } from './useGateway'; import { OnlineDocumentGateway } from './OnlineDocumentGateway'; -type Props = { +export function DocumentGateway(props: { visible: boolean; doc: types.DocumentGateway; -}; - -export default function Container(props: Props) { +}) { const { doc, visible } = props; - const state = useDocumentGateway(doc); - return ( - - - - ); -} + const { documentsService } = useWorkspaceContext(); -export type DocumentGatewayProps = ReturnType & { - targetName: string; -}; + const { + connected, + // Needed for OfflineGateway. + connectAttempt, + reconnect, + defaultPort, + // Needed for OnlineDocumentGateway. + gateway, + disconnect, + disconnectAttempt, + changePort, + changePortAttempt, + changeTargetSubresourceNameAttempt: changeDbNameAttempt, + changeTargetSubresourceName: changeDbName, + } = useGateway(doc); -export function DocumentGateway(props: DocumentGatewayProps) { - if (!props.connected) { + const runCliCommand = () => { + const command = getCliCommandArgv0(gateway.gatewayCliCommand); + const title = `${command} · ${doc.targetUser}@${doc.targetName}`; + + const cliDoc = documentsService.createGatewayCliDocument({ + title, + targetUri: doc.targetUri, + targetUser: doc.targetUser, + targetName: doc.targetName, + targetProtocol: gateway.protocol, + }); + documentsService.add(cliDoc); + documentsService.setLocation(cliDoc.uri); + }; + + if (!connected) { return ( - + + + ); } return ( - + + + ); } diff --git a/web/packages/teleterm/src/ui/DocumentGateway/OnlineDocumentGateway.tsx b/web/packages/teleterm/src/ui/DocumentGateway/OnlineDocumentGateway.tsx index dee93541eb9f7..a2d76b6cd1cea 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/OnlineDocumentGateway.tsx +++ b/web/packages/teleterm/src/ui/DocumentGateway/OnlineDocumentGateway.tsx @@ -18,27 +18,28 @@ import { useMemo, useRef } from 'react'; import { debounce } from 'shared/utils/highbar'; -import { Box, ButtonSecondary, Flex, H1, H2, Link, Text } from 'design'; +import { Alert, Box, ButtonSecondary, Flex, H1, H2, Link, Text } from 'design'; import Validation from 'shared/components/Validation'; import * as Alerts from 'design/Alert'; +import { Attempt, RunFuncReturnValue } from 'shared/hooks/useAsync'; + +import { Gateway } from 'gen-proto-ts/teleport/lib/teleterm/v1/gateway_pb'; + import { ConfigFieldInput, PortFieldInput } from '../components/FieldInputs'; import { CliCommand } from './CliCommand'; -import { DocumentGatewayProps } from './DocumentGateway'; -type OnlineDocumentGatewayProps = Pick< - DocumentGatewayProps, - | 'changeDbNameAttempt' - | 'changePortAttempt' - | 'disconnect' - | 'changeDbName' - | 'changePort' - | 'gateway' - | 'runCliCommand' ->; - -export function OnlineDocumentGateway(props: OnlineDocumentGatewayProps) { +export function OnlineDocumentGateway(props: { + changeDbName: (name: string) => RunFuncReturnValue; + changeDbNameAttempt: Attempt; + changePort: (port: string) => RunFuncReturnValue; + changePortAttempt: Attempt; + disconnect: () => RunFuncReturnValue; + disconnectAttempt: Attempt; + gateway: Gateway; + runCliCommand: () => void; +}) { const isPortOrDbNameProcessing = props.changeDbNameAttempt.status === 'processing' || props.changePortAttempt.status === 'processing'; @@ -85,6 +86,13 @@ export function OnlineDocumentGateway(props: OnlineDocumentGatewayProps) { Close Connection + + {props.disconnectAttempt.status === 'error' && ( + + Could not close the connection + + )} +

Connect with CLI

@@ -110,6 +118,7 @@ export function OnlineDocumentGateway(props: OnlineDocumentGatewayProps) { onButtonClick={props.runCliCommand} /> {$errors} +

Connect with GUI

diff --git a/web/packages/teleterm/src/ui/DocumentGateway/index.ts b/web/packages/teleterm/src/ui/DocumentGateway/index.ts index 824e4c702d571..8a993a80e505c 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/index.ts +++ b/web/packages/teleterm/src/ui/DocumentGateway/index.ts @@ -16,5 +16,4 @@ * along with this program. If not, see . */ -import DocumentGateway from './DocumentGateway'; -export default DocumentGateway; +export { DocumentGateway } from './DocumentGateway'; diff --git a/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.test.tsx b/web/packages/teleterm/src/ui/DocumentGateway/useGateway.test.tsx similarity index 95% rename from web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.test.tsx rename to web/packages/teleterm/src/ui/DocumentGateway/useGateway.test.tsx index 483a79e30993f..5786b67ba7b0b 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.test.tsx +++ b/web/packages/teleterm/src/ui/DocumentGateway/useGateway.test.tsx @@ -29,7 +29,7 @@ import { DatabaseUri } from 'teleterm/ui/uri'; import { WorkspaceContextProvider } from '../Documents'; import { MockAppContextProvider } from '../fixtures/MockAppContextProvider'; -import { useDocumentGateway } from './useDocumentGateway'; +import { useGateway } from './useGateway'; beforeEach(() => { jest.restoreAllMocks(); @@ -47,7 +47,7 @@ it('creates a gateway on mount if it does not exist already', async () => { return gateway; }); - const { result } = renderHook(() => useDocumentGateway(doc), { + const { result } = renderHook(() => useGateway(doc), { wrapper: $wrapper, }); @@ -71,7 +71,7 @@ it('does not create a gateway on mount if the gateway already exists', async () }); jest.spyOn(appContext.clustersService, 'createGateway'); - renderHook(() => useDocumentGateway(doc), { + renderHook(() => useGateway(doc), { wrapper: $wrapper, }); @@ -96,7 +96,7 @@ it('does not attempt to create a gateway immediately after closing it if the gat .spyOn(appContext.clustersService, 'createGateway') .mockResolvedValue(gateway); - const { result } = renderHook(() => useDocumentGateway(doc), { + const { result } = renderHook(() => useGateway(doc), { wrapper: $wrapper, }); diff --git a/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts b/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts similarity index 65% rename from web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts rename to web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts index 17374d401282c..c84a215136d2e 100644 --- a/web/packages/teleterm/src/ui/DocumentGateway/useDocumentGateway.ts +++ b/web/packages/teleterm/src/ui/DocumentGateway/useGateway.ts @@ -16,21 +16,21 @@ * along with this program. If not, see . */ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import { useAsync } from 'shared/hooks/useAsync'; +import { Gateway } from 'gen-proto-ts/teleport/lib/teleterm/v1/gateway_pb'; import { useAppContext } from 'teleterm/ui/appContextProvider'; -import * as types from 'teleterm/ui/services/workspacesService'; +import { DocumentGateway } from 'teleterm/ui/services/workspacesService'; import { useWorkspaceContext } from 'teleterm/ui/Documents'; import { retryWithRelogin } from 'teleterm/ui/utils'; -import * as tshdGateway from 'teleterm/services/tshd/gateway'; -import { Gateway } from 'teleterm/services/tshd/types'; import { isDatabaseUri, isAppUri } from 'teleterm/ui/uri'; +import { useStoreSelector } from 'teleterm/ui/hooks/useStoreSelector'; -export function useGateway(doc: types.DocumentGateway) { +export function useGateway(doc: DocumentGateway) { const ctx = useAppContext(); - const { documentsService: workspaceDocumentsService } = useWorkspaceContext(); + const { documentsService } = useWorkspaceContext(); // The port to show as default in the input field in case creating a gateway fails. // This is typically the case if someone reopens the app and the port of the gateway is already // occupied. @@ -39,11 +39,14 @@ export function useGateway(doc: types.DocumentGateway) { // input to a controlled one once `doc.port` gets set. The backend will handle converting an empty // string to '0'. const defaultPort = doc.port || ''; - const gateway = ctx.clustersService.findGateway(doc.gatewayUri); + const gateway = useStoreSelector( + 'clustersService', + useCallback(state => state.gateways.get(doc.gatewayUri), [doc.gatewayUri]) + ); const connected = !!gateway; const [connectAttempt, createGateway] = useAsync(async (port: string) => { - workspaceDocumentsService.update(doc.uri, { status: 'connecting' }); + documentsService.update(doc.uri, { status: 'connecting' }); let gw: Gateway; try { @@ -56,10 +59,10 @@ export function useGateway(doc: types.DocumentGateway) { }) ); } catch (error) { - workspaceDocumentsService.update(doc.uri, { status: 'error' }); + documentsService.update(doc.uri, { status: 'error' }); throw error; } - workspaceDocumentsService.update(doc.uri, { + documentsService.update(doc.uri, { gatewayUri: gw.uri, // Set the port on doc to match the one returned from the daemon. Teleterm doesn't let the // user provide a port for the gateway, so instead we have to let the daemon use a random @@ -90,7 +93,7 @@ export function useGateway(doc: types.DocumentGateway) { const [disconnectAttempt, disconnect] = useAsync(async () => { await ctx.clustersService.removeGateway(doc.gatewayUri); - workspaceDocumentsService.close(doc.uri); + documentsService.close(doc.uri); }); const [changeTargetSubresourceNameAttempt, changeTargetSubresourceName] = @@ -101,7 +104,7 @@ export function useGateway(doc: types.DocumentGateway) { name ); - workspaceDocumentsService.update(doc.uri, { + documentsService.update(doc.uri, { targetSubresourceName: updatedGateway.targetSubresourceName, }); }); @@ -112,7 +115,7 @@ export function useGateway(doc: types.DocumentGateway) { port ); - workspaceDocumentsService.update(doc.uri, { + documentsService.update(doc.uri, { targetSubresourceName: updatedGateway.targetSubresourceName, port: updatedGateway.localPort, }); @@ -145,57 +148,3 @@ export function useGateway(doc: types.DocumentGateway) { changePortAttempt, }; } - -//TODO(gzdunek): Refactor DocumentGateway so the hook below is no longer needed. -// We should move away from using one big hook per component. -export function useDocumentGateway(doc: types.DocumentGateway) { - const { documentsService: workspaceDocumentsService } = useWorkspaceContext(); - - const { - gateway, - reconnect, - connectAttempt, - disconnectAttempt, - disconnect, - connected, - changePort, - changePortAttempt, - changeTargetSubresourceNameAttempt, - changeTargetSubresourceName, - defaultPort, - } = useGateway(doc); - - const runCliCommand = () => { - if (!isDatabaseUri(doc.targetUri)) { - return; - } - const command = tshdGateway.getCliCommandArgv0(gateway.gatewayCliCommand); - const title = `${command} · ${doc.targetUser}@${doc.targetName}`; - - const cliDoc = workspaceDocumentsService.createGatewayCliDocument({ - title, - targetUri: doc.targetUri, - targetUser: doc.targetUser, - targetName: doc.targetName, - targetProtocol: gateway.protocol, - }); - workspaceDocumentsService.add(cliDoc); - workspaceDocumentsService.setLocation(cliDoc.uri); - }; - - return { - reconnect, - connectAttempt, - // TODO(ravicious): Show disconnectAttempt errors in UI. - disconnectAttempt, - disconnect, - connected, - gateway, - changeDbNameAttempt: changeTargetSubresourceNameAttempt, - changePort, - changePortAttempt, - changeDbName: changeTargetSubresourceName, - defaultPort, - runCliCommand, - }; -} diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx index b8fdc477ef87d..edef6c5f673ed 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/AppGateway.tsx @@ -32,10 +32,8 @@ import { import Validation from 'shared/components/Validation'; import { Attempt } from 'shared/hooks/useAsync'; import { debounce } from 'shared/utils/highbar'; - import { TextSelectCopy } from 'shared/components/TextSelectCopy'; - -import { Gateway } from 'teleterm/services/tshd/types'; +import { Gateway } from 'gen-proto-ts/teleport/lib/teleterm/v1/gateway_pb'; import { PortFieldInput } from '../components/FieldInputs'; @@ -46,6 +44,7 @@ export function AppGateway(props: { changePortAttempt: Attempt; disconnect(): void; }) { + const { gateway } = props; const formRef = useRef(); const { changePort } = props; @@ -57,26 +56,31 @@ export function AppGateway(props: { }, 1000); }, [changePort]); - const link = `${props.gateway.protocol.toLowerCase()}://${props.gateway.localAddress}:${props.gateway.localPort}`; + let address = `${gateway.localAddress}:${gateway.localPort}`; + if (gateway.protocol === 'HTTP') { + address = `http://${address}`; + } return (

App Connection

- {props.disconnectAttempt.status === 'error' && ( - - Could not close the connection - - )} Close Connection
+ + {props.disconnectAttempt.status === 'error' && ( + + Could not close the connection + + )} + handleChangePort(e.target.value)} mb={2} /> @@ -91,13 +95,16 @@ export function AppGateway(props: { /> )} + Access the app at: - + + {props.changePortAttempt.status === 'error' && ( Could not change the port number )} + The connection is made through an authenticated proxy so no extra credentials are necessary. See{' '} diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx index 8400181f316cf..bf74fa122bac2 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.story.tsx @@ -25,34 +25,66 @@ import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvi import { MockWorkspaceContextProvider } from 'teleterm/ui/fixtures/MockWorkspaceContextProvider'; import { makeAppGateway } from 'teleterm/services/tshd/testHelpers'; import { MockedUnaryCall } from 'teleterm/services/tshd/cloneableClient'; +import { Meta } from '@storybook/react'; +import { Flex } from 'design'; -export default { - title: 'Teleterm/DocumentGatewayApp', +type StoryProps = { + appType: 'web' | 'tcp'; + online: boolean; + changePort: 'succeed' | 'throw-error'; + disconnect: 'succeed' | 'throw-error'; }; -const gateway = makeAppGateway(); - -const documentGateway: types.DocumentGateway = { - kind: 'doc.gateway', - targetUri: '/clusters/bar/apps/quux', - origin: 'resource_table', - gatewayUri: gateway.uri, - uri: '/docs/123', - title: 'quux', - targetUser: '', - status: '', - targetName: 'quux', +const meta: Meta = { + title: 'Teleterm/DocumentGatewayApp', + component: Story, + argTypes: { + appType: { + control: { type: 'radio' }, + options: ['web', 'tcp'], + }, + changePort: { + if: { arg: 'online' }, + control: { type: 'radio' }, + options: ['succeed', 'throw-error'], + }, + disconnect: { + if: { arg: 'online' }, + control: { type: 'radio' }, + options: ['succeed', 'throw-error'], + }, + }, + args: { + appType: 'web', + online: true, + changePort: 'succeed', + disconnect: 'succeed', + }, }; +export default meta; -const rootClusterUri = '/clusters/bar'; +export function Story(props: StoryProps) { + const rootClusterUri = '/clusters/bar'; + const gateway = makeAppGateway(); + if (props.appType === 'tcp') { + gateway.protocol = 'TCP'; + } + const documentGateway: types.DocumentGateway = { + kind: 'doc.gateway', + targetUri: '/clusters/bar/apps/quux', + origin: 'resource_table', + gatewayUri: gateway.uri, + uri: '/docs/123', + title: 'quux', + targetUser: '', + status: '', + targetName: 'quux', + }; + if (!props.online) { + documentGateway.gatewayUri = undefined; + } -export function Online() { const appContext = new MockAppContext(); - appContext.clustersService.createGateway = () => Promise.resolve(gateway); - appContext.clustersService.setState(draftState => { - draftState.gateways.set(gateway.uri, gateway); - }); - appContext.workspacesService.setState(draftState => { draftState.rootClusterUri = rootClusterUri; draftState.workspaces[rootClusterUri] = { @@ -62,39 +94,58 @@ export function Online() { accessRequests: undefined, }; }); + if (props.online) { + appContext.clustersService.createGateway = () => Promise.resolve(gateway); + appContext.clustersService.setState(draftState => { + draftState.gateways.set(gateway.uri, gateway); + }); - appContext.tshd.setGatewayLocalPort = ({ localPort }) => - wait(800).then(() => new MockedUnaryCall({ ...gateway, localPort })); - - return ( - - - - - - ); -} - -export function Offline() { - const offlineDocumentGateway = { ...documentGateway, gatewayUri: undefined }; - const appContext = new MockAppContext(); - appContext.clustersService.createGateway = () => - Promise.reject(new Error('failed to create gateway')); - - appContext.workspacesService.setState(draftState => { - draftState.rootClusterUri = rootClusterUri; - draftState.workspaces[rootClusterUri] = { - localClusterUri: rootClusterUri, - documents: [offlineDocumentGateway], - location: offlineDocumentGateway.uri, - accessRequests: undefined, - }; - }); + appContext.tshd.setGatewayLocalPort = ({ localPort }) => + wait(1000).then( + () => + new MockedUnaryCall( + { ...gateway, localPort }, + props.changePort === 'throw-error' + ? new Error('something went wrong') + : undefined + ) + ); + appContext.tshd.removeGateway = () => + wait(50).then( + () => + new MockedUnaryCall( + {}, + props.disconnect === 'throw-error' + ? new Error('something went wrong') + : undefined + ) + ); + } else { + appContext.clustersService.createGateway = () => + Promise.reject(new Error('failed to create gateway')); + } return ( - + - + + + ); diff --git a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx index 39f8b5958bb3a..61b8ce5927c23 100644 --- a/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx +++ b/web/packages/teleterm/src/ui/DocumentGatewayApp/DocumentGatewayApp.tsx @@ -15,12 +15,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ - -import { useAppContext } from 'teleterm/ui/appContextProvider'; import { DocumentGateway } from 'teleterm/ui/services/workspacesService'; import Document from 'teleterm/ui/Document'; -import { useDocumentGateway } from '../DocumentGateway/useDocumentGateway'; +import { useGateway } from '../DocumentGateway/useGateway'; import { OfflineGateway } from '../components/OfflineGateway'; @@ -30,7 +28,7 @@ export function DocumentGatewayApp(props: { doc: DocumentGateway; visible: boolean; }) { - const ctx = useAppContext(); + const { doc } = props; const { gateway, changePort, @@ -40,9 +38,7 @@ export function DocumentGatewayApp(props: { disconnect, disconnectAttempt, reconnect, - } = useDocumentGateway(props.doc); - - ctx.clustersService.useState(); + } = useGateway(doc); return ( @@ -50,8 +46,8 @@ export function DocumentGatewayApp(props: { ) : ( diff --git a/web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx b/web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx index d1d300f0d8428..cfa686bb99696 100644 --- a/web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx +++ b/web/packages/teleterm/src/ui/Documents/DocumentsRenderer.tsx @@ -32,7 +32,7 @@ import { Workspace, } from 'teleterm/ui/services/workspacesService'; import DocumentCluster from 'teleterm/ui/DocumentCluster'; -import DocumentGateway from 'teleterm/ui/DocumentGateway'; +import { DocumentGateway } from 'teleterm/ui/DocumentGateway'; import { DocumentTerminal } from 'teleterm/ui/DocumentTerminal'; import { ConnectMyComputerContextProvider, diff --git a/web/packages/teleterm/src/ui/components/FieldInputs.tsx b/web/packages/teleterm/src/ui/components/FieldInputs.tsx index d4cc4f94b340d..e6a640e7fa9c9 100644 --- a/web/packages/teleterm/src/ui/components/FieldInputs.tsx +++ b/web/packages/teleterm/src/ui/components/FieldInputs.tsx @@ -16,32 +16,16 @@ * along with this program. If not, see . */ -import FieldInput from 'shared/components/FieldInput'; -import styled from 'styled-components'; +import FieldInput, { FieldInputProps } from 'shared/components/FieldInput'; import { forwardRef } from 'react'; -import { FieldInputProps } from 'shared/components/FieldInput'; export const ConfigFieldInput = forwardRef( (props, ref) => ); -const ConfigFieldInputWithoutStepper = styled(ConfigFieldInput)` - input { - ::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; - } - - ::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; - } - } -`; - export const PortFieldInput = forwardRef( (props, ref) => ( -