Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -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 } 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 = '#005A9E';
Comment thread
a-b-r-o-w-n marked this conversation as resolved.
Outdated
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);
Comment thread
hatpick marked this conversation as resolved.
} 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 href="https://ngrok.com/download" rel="noopener noreferrer" target="_blank">
Comment thread
a-b-r-o-w-n marked this conversation as resolved.
Outdated
{children}
</Link>
),
})}
</p>
<div css={commandContainer}>
{command}
<IconButton
ariaLabel={formatMessage('Copy project location to clipboard')}
iconProps={{ iconName: 'Copy' }}
styles={copyIconStyles}
title={formatMessage('Copy project location to clipboard')}
Comment thread
a-b-r-o-w-n marked this conversation as resolved.
Outdated
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.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,3 +511,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
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
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.
Comment thread
a-b-r-o-w-n marked this conversation as resolved.
// 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;
}
12 changes: 9 additions & 3 deletions Composer/packages/server/src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
"a_form_dialog_enables_your_bot_to_collect_pieces_o_fdd3fe56": {
"message": "A form dialog enables your bot to collect pieces of information ."
},
"a_install_ngrok_a_and_run_the_following_command_to_634f3414": {
"message": "<a>Install ngrok</a> and run the following command to continue"
},
"a_knowledge_base_name_cannot_contain_spaces_or_spe_91dd53ac": {
"message": "A knowledge base name cannot contain spaces or special characters. Use letters, numbers, -, or _."
},
Expand Down Expand Up @@ -3335,12 +3338,12 @@
"select_which_tasks_this_skill_can_perform_172b0eae": {
"message": "Select which tasks this skill can perform"
},
"select_your_azure_directory_then_choose_the_subscr_69776814": {
"message": "Select your Azure directory, then choose the subscription where your existing resource is located and the keys you want to use. "
},
"select_your_azure_directory_then_choose_the_subscr_7034a3c0": {
"message": "Select your Azure directory, then choose the subscription where you’d like your new { service } resource."
},
"select_your_azure_directory_then_choose_the_subscr_8c3aa61": {
"message": "Select your Azure directory, then choose the subscription where your existing resource is located and the keys you want to use."
},
"select_your_azure_directory_then_choose_the_subscr_e3d67edf": {
"message": "Select your Azure directory, then choose the subscription where you’d like your new { service } resource. "
},
Expand Down Expand Up @@ -3404,6 +3407,9 @@
"setup_language_understanding_so_that_you_can_start_97addd86": {
"message": "Setup Language Understanding so that you can start and test your bot."
},
"setup_tunneling_software_to_test_your_remote_skill_12c344c6": {
"message": "Setup tunneling software to test your remote skill"
},
"share_as_a_skill_63554df2": {
"message": "Share as a skill"
},
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/types/src/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type PublishResult = {
status?: number;
/** for local publish */
endpointURL?: string;
port?: number;
/** for PVA publish */
action?: {
href: string;
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/types/src/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export type Notification = {
read?: boolean;
hidden?: boolean;
onRenderCardContent?: ((props: Notification) => JSX.Element) | React.FC<any>;
data?: Record<string, unknown>;
Comment thread
a-b-r-o-w-n marked this conversation as resolved.
Outdated
};

export type ApplicationContextApi = {
Expand Down
1 change: 1 addition & 0 deletions extensions/localPublish/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ class LocalPublisher implements PublishPlugin<PublishConfig> {
status: 200,
result: {
message: 'Running',
port,
endpointURL: url,
},
};
Expand Down