diff --git a/Composer/packages/client/src/components/BotRuntimeController/BotController.tsx b/Composer/packages/client/src/components/BotRuntimeController/BotController.tsx index fcaedbb27d..93b0e373a4 100644 --- a/Composer/packages/client/src/components/BotRuntimeController/BotController.tsx +++ b/Composer/packages/client/src/components/BotRuntimeController/BotController.tsx @@ -73,7 +73,7 @@ const BotController: React.FC = ({ onHideController, isContr const [startAllBotsOperationQueued, queueStartAllBots] = useState(false); const [botsStartOperationCompleted, setBotsStartOperationCompleted] = useState(false); - const [areBotsStarting, setBotsStarting] = useState(false); + const [areBotsProcessing, setBotsProcessing] = useState(false); const [startPanelButtonText, setStartPanelButtonText] = useState(''); const { startAllBots, stopAllBots } = useBotOperations(); const builderEssentials = useRecoilValue(buildConfigurationSelector); @@ -99,7 +99,7 @@ const BotController: React.FC = ({ onHideController, isContr }, [projectCollection, errors]); useEffect(() => { - const botsStarting = + const botsProcessing = startAllBotsOperationQueued || projectCollection.some(({ status }) => { return ( @@ -111,25 +111,39 @@ const BotController: React.FC = ({ onHideController, isContr status == BotStatus.stopping ); }); - setBotsStarting(botsStarting); + setBotsProcessing(botsProcessing); const botOperationsCompleted = projectCollection.some( ({ status }) => status === BotStatus.connected || status === BotStatus.failed ); setBotsStartOperationCompleted(botOperationsCompleted); - if (botsStarting) { + if (botsProcessing) { setStatusIconClass(undefined); - setStartPanelButtonText( - formatMessage( - `{ - total, plural, - =1 {Starting bot..} - other {Starting bots.. ({running}/{total} running)} - }`, - { running: runningBots.projectIds.length, total: runningBots.totalBots } - ) - ); + const botsStopping = projectCollection.some(({ status }) => status == BotStatus.stopping); + if (botsStopping) { + setStartPanelButtonText( + formatMessage( + `{ + total, plural, + =1 {Stopping bot..} + other {Stopping bots.. ({running}/{total} running)} + }`, + { running: runningBots.projectIds.length, total: runningBots.totalBots } + ) + ); + } else { + setStartPanelButtonText( + formatMessage( + `{ + total, plural, + =1 {Starting bot..} + other {Starting bots.. ({running}/{total} running)} + }`, + { running: runningBots.projectIds.length, total: runningBots.totalBots } + ) + ); + } return; } @@ -223,7 +237,7 @@ const BotController: React.FC = ({ onHideController, isContr aria-roledescription={formatMessage('Bot Controller')} ariaDescription={startPanelButtonText} data-testid={'startBotButton'} - disabled={disableStartBots || areBotsStarting} + disabled={disableStartBots || areBotsProcessing} iconProps={{ iconName: statusIconClass, styles: { @@ -260,7 +274,7 @@ const BotController: React.FC = ({ onHideController, isContr title={startPanelButtonText} onClick={handleClick} > - {areBotsStarting && ( + {areBotsProcessing && ( ', () => { ); await findByText('Starting bots.. (1/4 running)'); }); + + it('should show bots are stopping', async () => { + const initRecoilState = ({ set }) => { + const projectIds = ['123a.234', '456a.234', '789a.234', '1323.sdf']; + set(botProjectIdsState, projectIds); + set(botStatusState(projectIds[0]), BotStatus.published); + set(botStatusState(projectIds[1]), BotStatus.publishing); + set(botStatusState(projectIds[2]), BotStatus.connected); + set(botStatusState(projectIds[3]), BotStatus.stopping); + }; + const { findByText } = renderWithRecoil( + , + initRecoilState + ); + await findByText('Stopping bots.. (2/4 running)'); + }); }); diff --git a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts index ac05f62912..1a143b8c4f 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts @@ -309,7 +309,8 @@ export const publisherDispatcher = () => { const { set, snapshot } = callbackHelpers; try { const currentBotStatus = await snapshot.getPromise(botStatusState(projectId)); - if (currentBotStatus !== BotStatus.failed) { + // Change to "Stopping" status only if the Bot is not in a failed state or inactive state + if (currentBotStatus !== BotStatus.failed && currentBotStatus !== BotStatus.inactive) { set(botStatusState(projectId), BotStatus.stopping); } diff --git a/Composer/packages/client/src/shell/useShell.ts b/Composer/packages/client/src/shell/useShell.ts index 41113270da..c65de5b1f3 100644 --- a/Composer/packages/client/src/shell/useShell.ts +++ b/Composer/packages/client/src/shell/useShell.ts @@ -50,6 +50,7 @@ import TelemetryClient from '../telemetry/TelemetryClient'; import { lgFilesSelectorFamily } from '../recoilModel/selectors/lg'; import { getMemoryVariables } from '../recoilModel/dispatchers/utils/project'; import { createNotification } from '../recoilModel/dispatchers/notification'; +import { useBotOperations } from '../components/BotRuntimeController/useBotOperations'; import { useLgApi } from './lgApi'; import { useLuApi } from './luApi'; @@ -141,6 +142,7 @@ export function useShell(source: EventSource, projectId: string): Shell { const triggerApi = useTriggerApi(projectId); const actionApi = useActionApi(projectId); const { dialogId, selected, focused, promptTab } = designPageLocation; + const { stopSingleBot } = useBotOperations(); const dialogsMap = useMemo(() => { return dialogs.reduce((result, dialog) => { @@ -270,6 +272,9 @@ export function useShell(source: EventSource, projectId: string): Shell { }, updateFlowZoomRate, reloadProject: () => reloadProject(projectId), + stopBot: (targetProjectId: string) => { + stopSingleBot(targetProjectId); + }, ...lgApi, ...luApi, ...qnaApi, diff --git a/Composer/packages/extension-client/src/hooks/useProjectApi.ts b/Composer/packages/extension-client/src/hooks/useProjectApi.ts index 71fc00ff21..f199b57b81 100644 --- a/Composer/packages/extension-client/src/hooks/useProjectApi.ts +++ b/Composer/packages/extension-client/src/hooks/useProjectApi.ts @@ -41,6 +41,7 @@ const PROJECT_KEYS = [ 'api.updateDialogSchema', 'api.createTrigger', 'api.createQnATrigger', + 'api.stopBot', 'api.updateSkill', 'api.updateRecognizer', ]; diff --git a/Composer/packages/types/src/shell.ts b/Composer/packages/types/src/shell.ts index 09ae8176cb..9262bd47df 100644 --- a/Composer/packages/types/src/shell.ts +++ b/Composer/packages/types/src/shell.ts @@ -147,6 +147,7 @@ export type ProjectContextApi = { updateDialogSchema: (_: DialogSchemaFile) => Promise; createTrigger: (id: string, formData, autoSelected?: boolean) => void; createQnATrigger: (id: string) => void; + stopBot: (projectId: string) => void; updateSkill: (skillId: string, skillsData: { skill: Skill; selectedEndpointIndex: number }) => Promise; updateRecognizer: (projectId: string, dialogId: string, kind: LuProviderType) => void; }; diff --git a/extensions/packageManager/src/pages/Library.tsx b/extensions/packageManager/src/pages/Library.tsx index ab1585e4a8..045ff68ac0 100644 --- a/extensions/packageManager/src/pages/Library.tsx +++ b/extensions/packageManager/src/pages/Library.tsx @@ -64,7 +64,7 @@ export interface PackageSourceFeed extends IDropdownOption { const Library: React.FC = () => { const [items, setItems] = useState([]); - const { projectId, reloadProject, projectCollection: allProjectCollection } = useProjectApi(); + const { projectId, reloadProject, projectCollection: allProjectCollection, stopBot } = useProjectApi(); const { setApplicationLevelError, navigateTo, confirm } = useApplicationApi(); const telemetryClient: TelemetryClient = useTelemetryClient(); @@ -391,6 +391,7 @@ const Library: React.FC = () => { const importComponent = async (packageName, version, isUpdating, source) => { try { + stopBot(currentProjectId); const results = await installComponentAPI(currentProjectId, packageName, version, isUpdating, source); // check to see if there was a conflict that requires confirmation @@ -424,7 +425,6 @@ const Library: React.FC = () => { setReadmeHidden(false); } - // reload modified content await reloadProject(); } } catch (err) { @@ -515,6 +515,7 @@ const Library: React.FC = () => { closeDialog(); setWorking(strings.uninstallProgress); try { + stopBot(currentProjectId); const results = await uninstallComponentAPI(currentProjectId, selectedItem.name); if (results.data.success) {