Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const MenuLoginWithActionMenu = ({
inputType,
}: {
/** Button text for main menu button. */
buttonText: string;
buttonText?: string;
/**
* Handles select or click in main menu items.
* If isExternalUrl item returned by getLoginItems is true, a button with <a> tag is rendered
Expand Down
55 changes: 38 additions & 17 deletions web/packages/teleterm/src/ui/DocumentCluster/ActionButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
MenuLogin,
MenuLoginProps,
} from 'shared/components/MenuLogin';
import { MenuLoginWithActionMenu } from 'shared/components/MenuLoginWithActionMenu';

import {
formatPortRange,
Expand All @@ -57,12 +58,25 @@ import {
import { IAppContext } from 'teleterm/ui/types';
import { DatabaseUri, routing } from 'teleterm/ui/uri';
import { retryWithRelogin } from 'teleterm/ui/utils';
import { useVnetAppLauncher, useVnetContext } from 'teleterm/ui/Vnet';
import { useVnetContext, useVnetLauncher } from 'teleterm/ui/Vnet';

export function ConnectServerActionButton(props: {
server: Server;
}): React.JSX.Element {
const ctx = useAppContext();
const { isSupported: isVnetSupported } = useVnetContext();
const { launchVnet } = useVnetLauncher();

function connectWithVnet(): void {
const hostname = props.server.hostname;
const cluster = ctx.clustersService.findClusterByResource(props.server.uri);
const clusterName = cluster?.name || '<cluster>';
const addr = `${hostname}.${clusterName}`;
launchVnet({
addrToCopy: addr,
resourceUri: props.server.uri,
});
}

function getSshLogins(): string[] {
const cluster = ctx.clustersService.findClusterByResource(props.server.uri);
Expand All @@ -80,21 +94,28 @@ export function ConnectServerActionButton(props: {
);
}

const commonProps = {
inputType: MenuInputType.FILTER,
textTransform: 'none',
getLoginItems: () => getSshLogins().map(login => ({ login, url: '' })),
onSelect: (e, login) => connect(login),
transformOrigin: {
vertical: 'top',
horizontal: 'right',
},
anchorOrigin: {
vertical: 'bottom',
horizontal: 'right',
},
};

if (!isVnetSupported) {
return <MenuLogin {...commonProps} />;
}
return (
<MenuLogin
inputType={MenuInputType.FILTER}
textTransform="none"
getLoginItems={() => getSshLogins().map(login => ({ login, url: '' }))}
onSelect={(e, login) => connect(login)}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
/>
<MenuLoginWithActionMenu size="small" {...commonProps}>
<MenuItem onClick={connectWithVnet}>Connect with VNet</MenuItem>
</MenuLoginWithActionMenu>
);
}

Expand All @@ -121,13 +142,13 @@ export function ConnectKubeActionButton(props: {
export function ConnectAppActionButton(props: { app: App }): React.JSX.Element {
const appContext = useAppContext();
const { isSupported: isVnetSupported } = useVnetContext();
const { launchVnet } = useVnetAppLauncher();
const { launchVnet } = useVnetLauncher();

function connectWithVnet(targetPort?: number): void {
void launchVnet({
addrToCopy: appToAddrToCopy(props.app, targetPort),
resourceUri: props.app.uri,
isMultiPort: !!props.app.tcpPorts.length,
isMultiPortApp: !!props.app.tcpPorts.length,
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ import { MockAppContextProvider } from 'teleterm/ui/fixtures/MockAppContextProvi
import { MockAppContext } from 'teleterm/ui/fixtures/mocks';
import { MockWorkspaceContextProvider } from 'teleterm/ui/fixtures/MockWorkspaceContextProvider';
import { makeDocumentCluster } from 'teleterm/ui/services/workspacesService/documentsService/testHelpers';
import { ConnectionsContextProvider } from 'teleterm/ui/TopBar/Connections/connectionsContext';
import * as uri from 'teleterm/ui/uri';
import { VnetContextProvider } from 'teleterm/ui/Vnet';

const mio = mockIntersectionObserver();

Expand Down Expand Up @@ -318,12 +320,16 @@ test.each([
<MockWorkspaceContextProvider>
<ResourcesContextProvider>
<ConnectMyComputerContextProvider rootClusterUri={rootCluster.uri}>
<Refresher ref={ref} rootClusterUri={rootCluster.uri} />
<UnifiedResources
clusterUri={doc.clusterUri}
docUri={doc.uri}
queryParams={doc.queryParams}
/>
<ConnectionsContextProvider>
<VnetContextProvider>
<Refresher ref={ref} rootClusterUri={rootCluster.uri} />
<UnifiedResources
clusterUri={doc.clusterUri}
docUri={doc.uri}
queryParams={doc.queryParams}
/>
</VnetContextProvider>
</ConnectionsContextProvider>
</ConnectMyComputerContextProvider>
</ResourcesContextProvider>
</MockWorkspaceContextProvider>
Expand Down Expand Up @@ -398,7 +404,9 @@ it('passes props with stable identity to <Resources>', async () => {
<MockWorkspaceContextProvider>
<ResourcesContextProvider>
<ConnectMyComputerContextProvider rootClusterUri={doc.clusterUri}>
{children}
<ConnectionsContextProvider>
<VnetContextProvider>{children}</VnetContextProvider>
</ConnectionsContextProvider>
</ConnectMyComputerContextProvider>
</ResourcesContextProvider>
</MockWorkspaceContextProvider>
Expand Down
4 changes: 2 additions & 2 deletions web/packages/teleterm/src/ui/Search/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { ResourceRequest } from 'teleterm/ui/services/workspacesService/accessRe
import { IAppContext } from 'teleterm/ui/types';
import { routing } from 'teleterm/ui/uri';
import { assertUnreachable, retryWithRelogin } from 'teleterm/ui/utils';
import { VnetAppLauncher } from 'teleterm/ui/Vnet';
import { VnetLauncher } from 'teleterm/ui/Vnet';

export interface SimpleAction {
type: 'simple-action';
Expand Down Expand Up @@ -73,7 +73,7 @@ export type SearchAction = SimpleAction | ParametrizedAction;

export function mapToAction(
ctx: IAppContext,
launchVnet: VnetAppLauncher,
launchVnet: VnetLauncher,
searchContext: SearchContext,
result: SearchResult
): SearchAction {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
} from 'teleterm/ui/Search/useSearch';
import { routing } from 'teleterm/ui/uri';
import { isRetryable } from 'teleterm/ui/utils/retryWithRelogin';
import { useVnetAppLauncher, useVnetContext } from 'teleterm/ui/Vnet';
import { useVnetContext, useVnetLauncher } from 'teleterm/ui/Vnet';

import { useDisplayResults } from './useDisplayResults';

Expand All @@ -48,7 +48,7 @@ export function useActionAttempts() {
const searchContext = useSearchContext();
const { inputValue, filters, pauseUserInteraction } = searchContext;
const { isSupported: isVnetSupported } = useVnetContext();
const vnetLauncher = useVnetAppLauncher();
const vnetLauncher = useVnetLauncher();
const launchVnet = isVnetSupported ? vnetLauncher.launchVnet : undefined;

const [resourceSearchAttempt, runResourceSearch, setResourceSearchAttempt] =
Expand Down
15 changes: 6 additions & 9 deletions web/packages/teleterm/src/ui/Vnet/DocumentVnetInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import type * as docTypes from 'teleterm/ui/services/workspacesService';
import { routing } from 'teleterm/ui/uri';

import appAccessPng from './app-access.png';
import { useVnetAppLauncher } from './useVnetAppLauncher';
import { useVnetLauncher } from './useVnetLauncher';
import { useVnetContext } from './vnetContext';

export function DocumentVnetInfo(props: {
Expand All @@ -46,7 +46,7 @@ export function DocumentVnetInfo(props: {
stopAttempt,
status,
} = useVnetContext();
const { launchVnetWithoutFirstTimeCheck } = useVnetAppLauncher();
const { launchVnetWithoutFirstTimeCheck } = useVnetLauncher();
const userAtHost = useMemo(() => {
const { hostname, username } = mainProcessClient.getRuntimeSettings();
return `${username}@${hostname}`;
Expand All @@ -55,13 +55,10 @@ export function DocumentVnetInfo(props: {
const proxyHostname = routing.parseClusterName(rootClusterUri);

const startVnet = async () => {
await launchVnetWithoutFirstTimeCheck({
addrToCopy: doc.app?.targetAddress,
isMultiPort: doc.app?.isMultiPort,
});
// Remove targetAddress so that subsequent launches of VNet from this specific doc won't copy
// the stale app address to the clipboard.
documentsService.update(doc.uri, { app: undefined });
await launchVnetWithoutFirstTimeCheck(doc.launcherArgs);
// Remove launcherArgs so that subsequent launches of VNet from this
// specific doc won't copy the stale address to the clipboard.
documentsService.update(doc.uri, { launcherArgs: undefined });
};

return (
Expand Down
2 changes: 1 addition & 1 deletion web/packages/teleterm/src/ui/Vnet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
export * from './VnetSliderStep';
export * from './vnetContext';
export { VnetConnectionItem } from './VnetConnectionItem';
export * from './useVnetAppLauncher';
export * from './useVnetLauncher';
Original file line number Diff line number Diff line change
Expand Up @@ -19,48 +19,31 @@
import { useCallback, useMemo } from 'react';

import { useAppContext } from 'teleterm/ui/appContextProvider';
import { VnetLauncherArgs } from 'teleterm/ui/services/workspacesService/documentsService/types';
import { useConnectionsContext } from 'teleterm/ui/TopBar/Connections/connectionsContext';
import { ResourceUri, routing } from 'teleterm/ui/uri';
import { routing } from 'teleterm/ui/uri';

import { useVnetContext } from './vnetContext';

export type VnetAppLauncher = (args: VnetAppLauncherArgs) => Promise<void>;
export type VnetLauncher = (args: VnetLauncherArgs) => Promise<void>;

// NOTE: Almost every field added to VnetAppLauncherArgs will need to be added to DocumentVnetInfo.
//
// This is because during the first launch of VNet through useVnetAppLauncher, the act of launching
// VNet is split into two parts. When a user clicks the "Connect" button next to a TCP app or opens
// one from the search bar, a DocumentVnetInfo is opened first. Then the user can start VNet from
// there, which should carry out the launch of that particular app. Hence the need to copy some
// arguments from the app to the doc.
type VnetAppLauncherArgs = {
addrToCopy: string | undefined;
/**
* resourceUri lets the VNet launcher establish which workspace to open the info doc in if
* there's a need to do it.
*/
resourceUri: ResourceUri;
isMultiPort: boolean;
};

export const useVnetAppLauncher = (): {
export const useVnetLauncher = (): {
/**
* launchVnet is a function that manages VNet start when:
*
* - The user clicks "Connect" next to a TCP app or selects one of the ports from the menu.
* - The user selects a TCP app through the search bar.
* - The user clicks "Connect with VNet" on an SSH server.
*
* If the user is yet to start VNet, it opens the info doc. If they already started it in the past,
* it starts VNet and then copies the address of the app to the clipboard.
* it starts VNet and then copies the address of the resource to the clipboard.
*/
launchVnet: VnetAppLauncher;
launchVnet: VnetLauncher;
/**
* launchVnetWithoutFirstTimeCheck never opens the info doc, it starts VNet and then copies the
* address of the app to the clipboard.
* address of the resource to the clipboard.
*/
launchVnetWithoutFirstTimeCheck: (
args: Pick<VnetAppLauncherArgs, 'addrToCopy' | 'isMultiPort'>
) => Promise<void>;
launchVnetWithoutFirstTimeCheck: (args?: VnetLauncherArgs) => Promise<void>;
} => {
const { notificationsService, workspacesService } = useAppContext();
const { start, status, startAttempt, hasEverStarted } = useVnetContext();
Expand All @@ -82,8 +65,8 @@ export const useVnetAppLauncher = (): {
}, [status.value, startAttempt.status, open, start]);

const openInfoDoc = useCallback(
async ({ addrToCopy, resourceUri, isMultiPort }: VnetAppLauncherArgs) => {
const rootClusterUri = routing.ensureRootClusterUri(resourceUri);
async (args: VnetLauncherArgs) => {
const rootClusterUri = routing.ensureRootClusterUri(args.resourceUri);
// Since VNet app launcher might be called from the search bar, we have to account for the
// user being in a different workspace than the selected app.
const { isAtDesiredWorkspace } =
Expand All @@ -105,35 +88,39 @@ export const useVnetAppLauncher = (): {
// Update targetAddress so that clicking "Start VNet" from the info doc is going to copy that
// address to clipboard.
docsService.update(docUri, {
app: { targetAddress: addrToCopy, isMultiPort },
launcherArgs: args,
});
},
[workspacesService]
);

const launchVnetAndCopyAddr = useCallback(
async ({
addrToCopy,
isMultiPort,
}: Pick<VnetAppLauncherArgs, 'addrToCopy' | 'isMultiPort'>) => {
async (args?: VnetLauncherArgs) => {
if (!(await launchVnet())) {
return;
}

if (!addrToCopy) {
if (!args) {
// args are optional, if unset don't copy anything to the clipboard.
return;
}

const copiedToClipboard = copyAddrToClipboard(addrToCopy);
notificationsService.notifyInfo(
[
`Connect via VNet by using ${addrToCopy}`,
copiedToClipboard && '(copied to clipboard)',
!isMultiPort && 'and any port',
]
.filter(Boolean)
.join(' ') + '.'
);
const { addrToCopy, isMultiPortApp, resourceUri } = args;
const isApp = !!routing.parseAppUri(resourceUri)?.params?.appId;
const isServer = !!routing.parseServerUri(resourceUri)?.params?.serverId;
let msgParts = [];
if (isApp) {
msgParts.push(`Connect via VNet by using ${addrToCopy}`);
} else if (isServer) {
msgParts.push(`Connect with any SSH client to ${addrToCopy}`);
} else {
msgParts.push(`Connect via VNet to ${addrToCopy}`);
}
if (copyAddrToClipboard(addrToCopy)) {
msgParts.push('(copied to clipboard)');
}
if (isApp && !isMultiPortApp) {
msgParts.push('and any port');
}
notificationsService.notifyInfo(msgParts.join(' ') + '.');
},
[launchVnet, notificationsService]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
import { appToAddrToCopy } from 'teleterm/services/vnet/app';
import { IAppContext } from 'teleterm/ui/types';
import { AppUri, routing } from 'teleterm/ui/uri';
import { VnetAppLauncher } from 'teleterm/ui/Vnet';
import { VnetLauncher } from 'teleterm/ui/Vnet';

import { DocumentOrigin } from './types';

Expand All @@ -46,7 +46,7 @@ export async function connectToApp(
* launchVnet is supposed to be provided if VNet is supported. If so, connectToApp is going to use
* this function when targeting a TCP app. Otherwise it'll create an app gateway.
*/
launchVnet: null | VnetAppLauncher,
launchVnet: null | VnetLauncher,
target: App,
telemetry: { origin: DocumentOrigin },
options?: {
Expand Down Expand Up @@ -111,7 +111,7 @@ export async function connectToApp(
await launchVnet({
addrToCopy: appToAddrToCopy(target),
resourceUri: target.uri,
isMultiPort: !!target.tcpPorts.length,
isMultiPortApp: !!target.tcpPorts.length,
});
return;
}
Expand Down
Loading
Loading