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);