diff --git a/Composer/packages/client/src/App.tsx b/Composer/packages/client/src/App.tsx
index e72c40f125..fe83d396dd 100644
--- a/Composer/packages/client/src/App.tsx
+++ b/Composer/packages/client/src/App.tsx
@@ -21,13 +21,15 @@ const Logger = () => {
export const App: React.FC = () => {
const { appLocale } = useRecoilValue(userSettingsState);
- const { fetchExtensions, fetchFeatureFlags } = useRecoilValue(dispatcherState);
+
+ const { fetchExtensions, fetchFeatureFlags, checkNodeVersion } = useRecoilValue(dispatcherState);
useEffect(() => {
loadLocale(appLocale);
}, [appLocale]);
useEffect(() => {
+ checkNodeVersion();
fetchExtensions();
fetchFeatureFlags();
}, []);
diff --git a/Composer/packages/client/src/components/CreationFlow/v2/CreateOptions.tsx b/Composer/packages/client/src/components/CreationFlow/v2/CreateOptions.tsx
index 1d639a40ea..3f7a601f7c 100644
--- a/Composer/packages/client/src/components/CreationFlow/v2/CreateOptions.tsx
+++ b/Composer/packages/client/src/components/CreationFlow/v2/CreateOptions.tsx
@@ -15,11 +15,14 @@ import { DialogWrapper, DialogTypes } from '@bfc/ui-shared';
import { navigate, RouteComponentProps } from '@reach/router';
import querystring from 'query-string';
import axios from 'axios';
+import { useRecoilValue } from 'recoil';
import { DialogCreationCopy } from '../../../constants';
-import { getAliasFromPayload } from '../../../utils/electronUtil';
+import { getAliasFromPayload, isElectron } from '../../../utils/electronUtil';
+import { userHasNodeInstalledState } from '../../../recoilModel';
import { CreateBotV2 } from './CreateBot';
+import { NodeModal } from './NodeModal';
// -------------------- CreateOptions -------------------- //
type CreateOptionsProps = {
@@ -36,6 +39,8 @@ export function CreateOptionsV2(props: CreateOptionsProps) {
const [option, setOption] = useState('Create');
const [isOpenCreateModal, setIsOpenCreateModal] = useState(false);
const { templates, onDismiss, onNext, onJumpToOpenModal, fetchTemplates, fetchReadMe } = props;
+ const [showNodeModal, setShowNodeModal] = useState(false);
+ const userHasNode = useRecoilValue(userHasNodeInstalledState);
useEffect(() => {
// open bot directly if alias exist.
@@ -100,6 +105,12 @@ export function CreateOptionsV2(props: CreateOptionsProps) {
}
};
+ useEffect(() => {
+ if (!userHasNode) {
+ setShowNodeModal(true);
+ }
+ }, [userHasNode]);
+
return (
+ {isElectron() && showNodeModal && }
);
}
diff --git a/Composer/packages/client/src/components/CreationFlow/v2/NodeModal.tsx b/Composer/packages/client/src/components/CreationFlow/v2/NodeModal.tsx
new file mode 100644
index 0000000000..0feaa94798
--- /dev/null
+++ b/Composer/packages/client/src/components/CreationFlow/v2/NodeModal.tsx
@@ -0,0 +1,55 @@
+/* eslint-disable react/no-danger */
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+/** @jsx jsx */
+import { DialogTypes, DialogWrapper } from '@bfc/ui-shared/lib/components/DialogWrapper';
+import { jsx } from '@emotion/core';
+import formatMessage from 'format-message';
+import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/components/Button';
+import { DialogFooter } from 'office-ui-fabric-react/lib/components/Dialog';
+import React from 'react';
+import { Text } from 'office-ui-fabric-react/lib/Text';
+import { mergeStyles } from 'office-ui-fabric-react/lib/Styling';
+
+const dialogFooterClass = mergeStyles({
+ marginTop: '25px',
+});
+
+type NodeModalProps = {
+ setIsOpen: Function;
+ isOpen: boolean;
+};
+
+export const NodeModal: React.FC = (props) => {
+ return (
+ {
+ props.setIsOpen(false);
+ }}
+ >
+
+ {formatMessage(
+ 'Bot Framework Composer requires Node.js in order to create and run a new bot. Click “Install Node.js” to install the latest version'
+ )}
+
+
+
+ {
+ props.setIsOpen(false);
+ }}
+ />
+
+
+ );
+};
diff --git a/Composer/packages/client/src/recoilModel/atoms/appState.ts b/Composer/packages/client/src/recoilModel/atoms/appState.ts
index 38bbb36428..c8de7d826a 100644
--- a/Composer/packages/client/src/recoilModel/atoms/appState.ts
+++ b/Composer/packages/client/src/recoilModel/atoms/appState.ts
@@ -344,3 +344,8 @@ export const isWebChatPanelVisibleState = atom({
key: getFullyQualifiedKey('isWebChatPanelVisible'),
default: false,
});
+
+export const userHasNodeInstalledState = atom({
+ key: getFullyQualifiedKey('userHasNodeInstalled'),
+ default: true,
+});
diff --git a/Composer/packages/client/src/recoilModel/dispatchers/application.ts b/Composer/packages/client/src/recoilModel/dispatchers/application.ts
index 906f1d2a35..259d31253e 100644
--- a/Composer/packages/client/src/recoilModel/dispatchers/application.ts
+++ b/Composer/packages/client/src/recoilModel/dispatchers/application.ts
@@ -4,6 +4,7 @@
import { CallbackInterface, useRecoilCallback } from 'recoil';
import debounce from 'lodash/debounce';
+import formatMessage from 'format-message';
import {
appUpdateState,
@@ -15,11 +16,14 @@ import {
pageElementState,
debugPanelExpansionState,
debugPanelActiveTabState,
+ userHasNodeInstalledState,
+ applicationErrorState,
} from '../atoms/appState';
import { AppUpdaterStatus, CreationFlowStatus, CreationFlowType } from '../../constants';
import OnboardingState from '../../utils/onboardingStorage';
import { StateError, AppUpdateState } from '../../recoilModel/types';
import { DebugDrawerKeys } from '../../pages/design/DebugPanel/TabExtensions/types';
+import httpClient from '../../utils/httpUtil';
import { setError } from './shared';
@@ -130,7 +134,21 @@ export const applicationDispatcher = () => {
}
);
+ const checkNodeVersion = useRecoilCallback(({ set }: CallbackInterface) => async () => {
+ try {
+ const response = await httpClient.get(`/utilities/checkNode`);
+ const userHasNode = response.data?.userHasNode;
+ set(userHasNodeInstalledState, userHasNode);
+ } catch (err) {
+ set(applicationErrorState, {
+ message: formatMessage('Error checking node version'),
+ summary: err.message,
+ });
+ }
+ });
+
return {
+ checkNodeVersion,
setAppUpdateStatus,
setAppUpdateShowing,
setAppUpdateError,
diff --git a/Composer/packages/server/src/controllers/utilities.ts b/Composer/packages/server/src/controllers/utilities.ts
index 72c76c6187..61ae91c18b 100644
--- a/Composer/packages/server/src/controllers/utilities.ts
+++ b/Composer/packages/server/src/controllers/utilities.ts
@@ -1,8 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+import { exec } from 'child_process';
+import { promisify } from 'util';
+
import { Request, Response } from 'express';
import { parseQnAContent } from '../models/utilities/parser';
+const execAsync = promisify(exec);
async function getQnaContent(req: Request, res: Response) {
try {
@@ -16,6 +20,26 @@ async function getQnaContent(req: Request, res: Response) {
}
}
+async function checkNodeVersion(req: Request, res: Response) {
+ try {
+ const command = 'node -v';
+ const { stderr: checkNodeError, stdout: nodeVersion } = await execAsync(command);
+ if (checkNodeError) {
+ throw new Error();
+ } else {
+ res.status(200).json({
+ userHasNode: true,
+ nodeVersion: nodeVersion,
+ });
+ }
+ } catch (e) {
+ res.status(200).json({
+ userHasNode: false,
+ });
+ }
+}
+
export const UtilitiesController = {
getQnaContent,
+ checkNodeVersion,
};
diff --git a/Composer/packages/server/src/locales/en-US.json b/Composer/packages/server/src/locales/en-US.json
index da9f1ceb70..8b7a65eb2e 100644
--- a/Composer/packages/server/src/locales/en-US.json
+++ b/Composer/packages/server/src/locales/en-US.json
@@ -494,6 +494,15 @@
"bot_framework_composer_is_a_visual_authoring_canva_c3947d91": {
"message": "Bot Framework Composer is a visual authoring canvas for building bots and other types of conversational application with the Microsoft Bot Framework technology stack. With Composer you will find everything you need to build a modern, state-of-the-art conversational experience."
},
+ "bot_framework_composer_is_an_open_source_visual_au_2be2e02b": {
+ "message": "Bot Framework Composer is an open-source visual authoring canvas for developers and multi-disciplinary teams to build bots. Composer integrates LUIS and QnA Maker, and allows sophisticated composition of bot replies using language generation."
+ },
+ "bot_framework_composer_requires_node_js_in_order_t_a1d3dfb": {
+ "message": "Bot Framework Composer requires Node.js in order to run. Click “Install Node.js” to install the latest version"
+ },
+ "bot_framework_provides_the_most_comprehensive_expe_e34a7f5d": {
+ "message": "Bot Framework provides the most comprehensive experience for building conversational applications."
+ },
"bot_framework_emulator_fefd4a59": {
"message": "Bot Framework Emulator"
},
@@ -1448,6 +1457,9 @@
"error_afac7133": {
"message": "Error:"
},
+ "error_checking_node_version_98bfbf4c": {
+ "message": "Error checking node version"
+ },
"error_encountered_when_getting_template_readme_17dcbc61": {
"message": "### Error encountered when getting template readMe"
},
@@ -1880,6 +1892,9 @@
"install_more_adapters_in_a_the_package_manager_a_156fb028": {
"message": "Install more adapters in the package manager."
},
+ "install_node_js_1857298c": {
+ "message": "Install Node.js"
+ },
"install_pre_release_versions_of_composer_daily_to__ceb41b54": {
"message": "Install pre-release versions of Composer, daily, to access and test the latest features. Learn more."
},
diff --git a/Composer/packages/server/src/router/api.ts b/Composer/packages/server/src/router/api.ts
index dcbf05d18c..060a9b1b9c 100644
--- a/Composer/packages/server/src/router/api.ts
+++ b/Composer/packages/server/src/router/api.ts
@@ -98,6 +98,8 @@ router.use('/assets/locales/', express.static(path.join(__dirname, '..', '..', '
//help api
router.get('/utilities/qna/parse', UtilitiesController.getQnaContent);
+router.get('/utilities/checkNode', UtilitiesController.checkNodeVersion);
+
// extensions
router.get('/extensions', ExtensionsController.listExtensions);
router.post('/extensions', ExtensionsController.addExtension);