Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const BotController: React.FC<BotControllerProps> = ({ 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);
Expand All @@ -99,7 +99,7 @@ const BotController: React.FC<BotControllerProps> = ({ onHideController, isContr
}, [projectCollection, errors]);

useEffect(() => {
const botsStarting =
const botsProcessing =
startAllBotsOperationQueued ||
projectCollection.some(({ status }) => {
return (
Expand All @@ -111,25 +111,39 @@ const BotController: React.FC<BotControllerProps> = ({ 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;
}

Expand Down Expand Up @@ -223,7 +237,7 @@ const BotController: React.FC<BotControllerProps> = ({ onHideController, isContr
aria-roledescription={formatMessage('Bot Controller')}
ariaDescription={startPanelButtonText}
data-testid={'startBotButton'}
disabled={disableStartBots || areBotsStarting}
disabled={disableStartBots || areBotsProcessing}
iconProps={{
iconName: statusIconClass,
styles: {
Expand Down Expand Up @@ -260,7 +274,7 @@ const BotController: React.FC<BotControllerProps> = ({ onHideController, isContr
title={startPanelButtonText}
onClick={handleClick}
>
{areBotsStarting && (
{areBotsProcessing && (
<Spinner
size={SpinnerSize.small}
styles={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,20 @@ describe('<BotController />', () => {
);
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(
<BotController isControllerHidden={false} onHideController={jest.fn()} />,
initRecoilState
);
await findByText('Stopping bots.. (2/4 running)');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
5 changes: 5 additions & 0 deletions Composer/packages/client/src/shell/useShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -270,6 +272,9 @@ export function useShell(source: EventSource, projectId: string): Shell {
},
updateFlowZoomRate,
reloadProject: () => reloadProject(projectId),
stopBot: (targetProjectId: string) => {
stopSingleBot(targetProjectId);
},
...lgApi,
...luApi,
...qnaApi,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const PROJECT_KEYS = [
'api.updateDialogSchema',
'api.createTrigger',
'api.createQnATrigger',
'api.stopBot',
'api.updateSkill',
'api.updateRecognizer',
];
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 @@ -147,6 +147,7 @@ export type ProjectContextApi = {
updateDialogSchema: (_: DialogSchemaFile) => Promise<void>;
createTrigger: (id: string, formData, autoSelected?: boolean) => void;
createQnATrigger: (id: string) => void;
stopBot: (projectId: string) => void;
updateSkill: (skillId: string, skillsData: { skill: Skill; selectedEndpointIndex: number }) => Promise<void>;
updateRecognizer: (projectId: string, dialogId: string, kind: LuProviderType) => void;
};
Expand Down
5 changes: 3 additions & 2 deletions extensions/packageManager/src/pages/Library.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export interface PackageSourceFeed extends IDropdownOption {

const Library: React.FC = () => {
const [items, setItems] = useState<LibraryRef[]>([]);
const { projectId, reloadProject, projectCollection: allProjectCollection } = useProjectApi();
const { projectId, reloadProject, projectCollection: allProjectCollection, stopBot } = useProjectApi();
const { setApplicationLevelError, navigateTo, confirm } = useApplicationApi();
const telemetryClient: TelemetryClient = useTelemetryClient();

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -424,7 +425,6 @@ const Library: React.FC = () => {
setReadmeHidden(false);
}

// reload modified content
await reloadProject();
}
} catch (err) {
Expand Down Expand Up @@ -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) {
Expand Down