Skip to content

Commit

Permalink
feat: show tunneling information notification when starting bot with …
Browse files Browse the repository at this point in the history
…remote skills (#7611)

* add util to get device OS

* add port to local publish result

* add port to botEndpointsState

* show ngrok notification when starting a bot with remote skills

* only show the remote skills notification onces per session per bot

* update l10n file

* address feedback

* update locale file after merge

* fix failing test

Co-authored-by: Soroush <[email protected]>
  • Loading branch information
a-b-r-o-w-n and hatpick authored May 5, 2021
1 parent a326776 commit 0c914b2
Show file tree
Hide file tree
Showing 16 changed files with 229 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const OpenEmulatorButton: React.FC<OpenEmulatorButtonProps> = ({ projectI
const { openBotInEmulator } = useRecoilValue(dispatcherState);
const currentBotStatus = useRecoilValue(botStatusState(projectId));
const botEndpoints = useRecoilValue(botEndpointsState);
const endpoint = botEndpoints[projectId];
const endpoint = botEndpoints[projectId]?.url;

const handleClick = () => {
openBotInEmulator(projectId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const cardContainer = (show: boolean, ref?: HTMLDivElement | null) => () => {
border-left: 4px solid #0078d4;
background: white;
box-shadow: 0 6.4px 14.4px 0 rgba(0, 0, 0, 0.132), 0 1.2px 3.6px 0 rgba(0, 0, 0, 0.108);
width: 340px;
min-width: 340px;
border-radius: 2px;
display: flex;
flex-direction: column;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import React from 'react';
import formatMessage from 'format-message';
import { IconButton, IButtonStyles } from 'office-ui-fabric-react/lib/Button';
import { NeutralColors, FontSizes, FluentTheme } from '@uifabric/fluent-theme';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { FontWeights } from '@uifabric/styling';

import { platform, OS } from '../../utils/os';

import { CardProps } from './NotificationCard';

const container = css`
padding: 0 16px 16px 40px;
position: relative;
`;

const commandContainer = css`
display: flex;
flex-flow: row nowrap;
position: relative;
padding: 4px 28px 4px 8px;
background-color: ${NeutralColors.gray20};
line-height: 22px;
margin: 1rem 0;
`;

const copyContainer = css`
margin: 0;
margin-bottom: 4px;
font-size: ${FontSizes.size16};
font-weight: ${FontWeights.semibold};
`;

const copyIconColor = FluentTheme.palette.themeDark;
const copyIconStyles: IButtonStyles = {
root: { position: 'absolute', right: 0, color: copyIconColor, height: '22px' },
rootHovered: { backgroundColor: 'transparent', color: copyIconColor },
rootPressed: { backgroundColor: 'transparent', color: copyIconColor },
};

const linkContainer = css`
margin: 0;
`;

const getNgrok = () => {
const os = platform();
if (os === OS.Windows) {
return 'ngrok.exe';
}

return 'ngrok';
};

export const TunnelingSetupNotification: React.FC<CardProps> = (props) => {
const { title, data } = props;
const port = data?.port;
const command = `${getNgrok()} http ${port} --host-header=localhost`;

const copyLocationToClipboard = async () => {
try {
await window.navigator.clipboard.writeText(command);
} catch (e) {
// eslint-disable-next-line no-console
console.error('Something went wrong when trying to copy the command to clipboard.', e);
}
};

return (
<div css={container}>
<h2 css={copyContainer}>{title}</h2>
<p css={linkContainer}>
{formatMessage.rich('<a>Install ngrok</a> and run the following command to continue', {
a: ({ children }) => (
<Link key="ngrok-download" href="https://ngrok.com/download" rel="noopener noreferrer" target="_blank">
{children}
</Link>
),
})}
</p>
<div css={commandContainer}>
{command}
<IconButton
ariaLabel={formatMessage('Copy command to clipboard')}
iconProps={{ iconName: 'Copy' }}
styles={copyIconStyles}
title={formatMessage('Copy command to clipboard')}
onClick={copyLocationToClipboard}
/>
</div>
<p css={linkContainer}>
<Link
href="https://docs.microsoft.com/en-us/composer/how-to-connect-to-a-skill"
rel="noopener noreferrer"
target="_blank"
>
{formatMessage('Learn more')}
</Link>
</p>
</div>
);
};
3 changes: 3 additions & 0 deletions Composer/packages/client/src/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,6 @@ export const defaultTeamsManifest: TeamsManifest = {
permissions: ['identity', 'messageTeamMembers'],
validDomains: ['token.botframework.com'],
};

export const defaultBotPort = 3979;
export const defaultBotEndpoint = `http://localhost:${defaultBotPort}/api/messages`;
2 changes: 1 addition & 1 deletion Composer/packages/client/src/recoilModel/atoms/appState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export const runtimeSettingsState = atom<{
},
});

export const botEndpointsState = atom<Record<string, string>>({
export const botEndpointsState = atom<Record<string, { url: string; port: number }>>({
key: getFullyQualifiedKey('botEndpoints'),
default: {},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,8 @@ describe('skill dispatcher', () => {
it('should update setting.skill on local skills with "Composer Local" chosen as endpoint', async () => {
await act(async () => {
const botEndpoints = {};
botEndpoints[`${skillIds[0]}`] = 'http://localhost:3978/api/messages';
botEndpoints[`${skillIds[1]}`] = 'http://localhost:3979/api/messages';
botEndpoints[`${skillIds[0]}`] = { url: 'http://localhost:3978/api/messages', port: 3978 };
botEndpoints[`${skillIds[1]}`] = { url: 'http://localhost:3979/api/messages', port: 3979 };
renderedComponent.current.setters.setBotEndpoints(botEndpoints);
renderedComponent.current.setters.setTodoSkillData({
location: '/Users/tester/Desktop/LoadedBotProject/Todo-Skill',
Expand Down
41 changes: 35 additions & 6 deletions Composer/packages/client/src/recoilModel/dispatchers/publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
filePersistenceState,
settingsState,
runtimeStandardOutputDataState,
botProjectFileState,
} from '../atoms/botState';
import { openInEmulator } from '../../utils/navigation';
import { botEndpointsState } from '../atoms';
Expand All @@ -30,11 +31,13 @@ import { ClientStorage } from '../../utils/storage';
import { RuntimeOutputData } from '../types';
import { checkIfFunctionsMissing, missingFunctionsError } from '../../utils/runtimeErrors';
import TelemetryClient from '../../telemetry/TelemetryClient';
import { TunnelingSetupNotification } from '../../components/Notifications/TunnelingSetupNotification';

import { BotStatus, Text } from './../../constants';
import { BotStatus, Text, defaultBotEndpoint, defaultBotPort } from './../../constants';
import httpClient from './../../utils/httpUtil';
import { logMessage, setError } from './shared';
import { setRootBotSettingState } from './setting';
import { createNotification, addNotificationInternal } from './notification';

const PUBLISH_SUCCESS = 200;
const PUBLISH_PENDING = 202;
Expand Down Expand Up @@ -62,11 +65,14 @@ export const publisherDispatcher = () => {

const publishSuccess = async ({ set }: CallbackInterface, projectId: string, data: PublishResult, target) => {
TelemetryClient.track('PublishSuccess');
const { endpointURL, status } = data;
const { endpointURL, status, port } = data;
if (target.name === defaultPublishConfig.name) {
if (status === PUBLISH_SUCCESS && endpointURL) {
set(botStatusState(projectId), BotStatus.connected);
set(botEndpointsState, (botEndpoints) => ({ ...botEndpoints, [projectId]: `${endpointURL}/api/messages` }));
set(botEndpointsState, (botEndpoints) => ({
...botEndpoints,
[projectId]: { url: `${endpointURL}/api/messages`, port: port || defaultBotPort },
}));
} else {
set(botStatusState(projectId), BotStatus.starting);
}
Expand Down Expand Up @@ -95,7 +101,7 @@ export const publisherDispatcher = () => {
) => {
if (data == null) return;
const { set, snapshot } = callbackHelpers;
const { endpointURL, status } = data;
const { endpointURL, status, port } = data;

// remove job id in publish storage if published
if (status === PUBLISH_SUCCESS || status === PUBLISH_FAILED) {
Expand All @@ -119,11 +125,34 @@ export const publisherDispatcher = () => {
};
setRootBotSettingState(callbackHelpers, projectId, updatedSettings);
}

// display a notification for bots with remote skills the first time they are published
// for a given session.
const rootBotProjectFile = await snapshot.getPromise(botProjectFileState(rootBotId));
const notificationCache = publishStorage.get('notifications') || {};
if (
!notificationCache[rootBotId] &&
Object.values(rootBotProjectFile?.content?.skills ?? []).some((s) => s.remote)
) {
const notification = createNotification({
type: 'info',
title: formatMessage('Setup tunneling software to test your remote skill'),
onRenderCardContent: TunnelingSetupNotification,
data: {
port,
},
});
addNotificationInternal(callbackHelpers, notification);
publishStorage.set('notifications', {
...notificationCache,
[rootBotId]: true,
});
}
}
set(botStatusState(projectId), BotStatus.connected);
set(botEndpointsState, (botEndpoints) => ({
...botEndpoints,
[projectId]: `${endpointURL}/api/messages`,
[projectId]: { url: `${endpointURL}/api/messages`, port: port || defaultBotPort },
}));
} else if (status === PUBLISH_PENDING) {
set(botStatusState(projectId), BotStatus.starting);
Expand Down Expand Up @@ -308,7 +337,7 @@ export const publisherDispatcher = () => {
const settings = await snapshot.getPromise(settingsState(projectId));
try {
openInEmulator(
botEndpoints[projectId] || 'http://localhost:3979/api/messages',
botEndpoints[projectId]?.url || defaultBotEndpoint,
settings.MicrosoftAppId && settings.MicrosoftAppPassword
? { MicrosoftAppId: settings.MicrosoftAppId, MicrosoftAppPassword: settings.MicrosoftAppPassword }
: { MicrosoftAppPassword: '', MicrosoftAppId: '' }
Expand Down
4 changes: 2 additions & 2 deletions Composer/packages/client/src/recoilModel/dispatchers/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ export const skillDispatcher = () => {
const currentSetting = await snapshot.getPromise(settingsState(projectId));

// Update settings only for skills that have chosen the "Composer local" endpoint and not manifest endpoints
if (projectId && botEndpoints[projectId] && !botProjectSkill.endpointName) {
if (projectId && botEndpoints[projectId]?.url && !botProjectSkill.endpointName) {
updatedSettings = produce(updatedSettings, (draftState) => {
if (!draftState.skill) {
draftState.skill = {};
}
draftState.skill[skillNameIdentifier] = {
endpointUrl: botEndpoints[projectId],
endpointUrl: botEndpoints[projectId].url,
msAppId: currentSetting.MicrosoftAppId ?? '',
};
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ export const webChatEssentialsSelector = selectorFamily<WebChatEssentials, strin
msPassword: settings.MicrosoftAppPassword || '',
};
const botEndpoints = get(botEndpointsState);
const botUrl = botEndpoints[projectId];
const botUrl = botEndpoints[projectId]?.url;
const botName = get(botDisplayNameState(projectId));
const activeLocale = get(localeState(projectId));
const botStatus = get(botStatusState(projectId));
Expand Down
41 changes: 8 additions & 33 deletions Composer/packages/client/src/recoilModel/utils/fontUtil.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

const getOS = () => {
const userAgent = window.navigator.userAgent,
platform = window.navigator.platform,
macosPlatforms = ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'],
windowsPlatforms = ['Win32', 'Win64', 'Windows', 'WinCE'],
iosPlatforms = ['iPhone', 'iPad', 'iPod'];
const os = {
isMacintosh: false,
isLinux: false,
isWindows: false,
isAndroid: false,
isiOS: false,
};

if (macosPlatforms.indexOf(platform) !== -1) {
os.isMacintosh = true;
} else if (iosPlatforms.indexOf(platform) !== -1) {
os.isiOS = true;
} else if (windowsPlatforms.indexOf(platform) !== -1) {
os.isWindows = true;
} else if (/Android/.test(userAgent)) {
os.isAndroid = true;
} else if (/Linux/.test(platform)) {
os.isLinux = true;
}
return os;
};
import { platform, OS } from '../../utils/os';

// font align with vscode https://github.com/microsoft/vscode/blob/main/src/vs/editor/common/config/editorOptions.ts#L3680
const DEFAULT_WINDOWS_FONT_FAMILY = "Consolas, 'Courier New', monospace";
const DEFAULT_MAC_FONT_FAMILY = "Menlo, Monaco, 'Courier New', monospace";
const DEFAULT_LINUX_FONT_FAMILY = "'Droid Sans Mono', 'monospace', monospace, 'Droid Sans Fallback'";

export const getDefaultFontSettings = () => {
const PLATFORM = getOS();
const platformName = platform();
return {
fontFamily: PLATFORM.isMacintosh
? DEFAULT_MAC_FONT_FAMILY
: PLATFORM.isLinux
? DEFAULT_LINUX_FONT_FAMILY
: DEFAULT_WINDOWS_FONT_FAMILY,
fontFamily:
platformName === OS.MacOS
? DEFAULT_MAC_FONT_FAMILY
: platformName === OS.Linux
? DEFAULT_LINUX_FONT_FAMILY
: DEFAULT_WINDOWS_FONT_FAMILY,
fontWeight: 'normal',
fontSize: '14px',
lineHeight: 0,
Expand Down
27 changes: 27 additions & 0 deletions Composer/packages/client/src/utils/__tests__/os.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { platform, OS } from '../os';

describe('platform', () => {
it.each([
[
OS.Windows,
'5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) @bfc/electron-server/1.4.0-nightly.237625.d1378c6 Chrome/80.0.3987.165 Electron/8.2.4 Safari/537.36',
],
[
OS.MacOS,
'5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.85 Safari/537.36 Edg/90.0.818.46',
],
[
OS.Linux,
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36',
],
[
OS.Unix,
'Mozilla/5.0 (X11; CrOS x86_64 13020.67.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.97 Safari/537.36',
],
])('%s', (expectedOS, userAgentString) => {
expect(platform(userAgentString)).toBe(expectedOS);
});
});
30 changes: 30 additions & 0 deletions Composer/packages/client/src/utils/os.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

export enum OS {
Windows = 'Windows',
MacOS = 'MacOS',
Linux = 'Linux',
Unix = 'Unix',
Unknown = 'Unknown',
}

export function platform(userAgent: string = window.navigator.userAgent): OS {
if (userAgent.includes('Win')) {
return OS.Windows;
}

if (userAgent.includes('Mac')) {
return OS.MacOS;
}

if (userAgent.includes('Linux')) {
return OS.Linux;
}

if (userAgent.includes('X11')) {
return OS.Unix;
}

return OS.Unknown;
}
Loading

0 comments on commit 0c914b2

Please sign in to comment.