Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
2 changes: 2 additions & 0 deletions Composer/packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@
"format-message-cli": "^6.2.3",
"fs-extra": "7.0.1",
"html-webpack-plugin": "4.0.0-beta.8",
"jest-websocket-mock": "^2.2.0",
"mini-css-extract-plugin": "0.8.0",
"mock-socket": "9.0.3",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"pnp-webpack-plugin": "1.5.0",
"postcss-flexbugs-fixes": "4.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ import { ActionButton } from 'office-ui-fabric-react/lib/Button';
import formatMessage from 'format-message';
import { CommunicationColors } from '@uifabric/fluent-theme';

import { botRuntimeErrorState, dispatcherState } from '../../recoilModel';
import { botBuildTimeErrorState, dispatcherState } from '../../recoilModel';

type BotErrorViewerProps = {
projectId: string;
};

export const BotErrorViewer: React.FC<BotErrorViewerProps> = ({ projectId }) => {
const { setActiveTabInDebugPanel, setDebugPanelExpansion } = useRecoilValue(dispatcherState);
const botRuntimeErrorMsg = useRecoilValue(botRuntimeErrorState(projectId));
const botBuildTimeError = useRecoilValue(botBuildTimeErrorState(projectId));

const openErrorDialog = () => {
setActiveTabInDebugPanel('RuntimeLog');
Expand All @@ -26,7 +26,7 @@ export const BotErrorViewer: React.FC<BotErrorViewerProps> = ({ projectId }) =>

return (
<Fragment>
{botRuntimeErrorMsg?.message && (
{botBuildTimeError?.message && (
<ActionButton
styles={{
root: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,6 @@ export const BotRuntimeStatus = React.memo((props: BotRuntimeStatusProps) => {
break;

case BotStatus.connected: {
if (isRunning) {
setTimeout(() => {
getPublishStatus(projectId, defaultPublishConfig);
}, pollingInterval);
}
setIntervalRunning(false);
TelemetryClient.track('StartBotCompleted', { projectId, status: currentBotStatus });
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,16 @@

/** @jsx jsx */
import { jsx, css } from '@emotion/core';
import { SharedColors } from '@uifabric/fluent-theme';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { FontWeights } from 'office-ui-fabric-react/lib/Styling';
import { FontSizes } from '@uifabric/fluent-theme';

// -------------------- Styles -------------------- //

const calloutLabel = css`
font-size: ${FontSizes.size18};
font-weight: ${FontWeights.bold};
`;

const calloutContainer = css`
padding: 10px;
`;

const calloutDescription = css`
word-break: break-word;
`;
Expand Down Expand Up @@ -135,9 +130,13 @@ export const ErrorCallout: React.FC<IErrorCalloutProps> = (props) => {
return <div>{parsed.map(renderRow)}</div>;
}
};

return (
<div css={calloutContainer} data-testid="runtime-error-callout">
<div
css={{
color: `${SharedColors.red10}`,
}}
data-testid="runtime-error-callout"
>
<p css={calloutLabel}>{error.title}</p>
<p css={calloutDescription}>
{buildErrorMessage(error)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,15 @@ export function useBotOperations() {
const botRuntimeOperations = useRecoilValue(botRuntimeOperationsSelector);
const rootBotId = useRecoilValue(rootBotProjectIdSelector);
const [trackedProjectIds, setProjectsToTrack] = useState<string[]>([]);
const { updateSettingsForSkillsWithoutManifest, resetBotRuntimeError, setBotStatus } = useRecoilValue(
dispatcherState
);
const { updateSettingsForSkillsWithoutManifest, resetBotRuntimeLog, setBotStatus } = useRecoilValue(dispatcherState);

const handleBotStart = async (
projectId: string,
config: IPublishConfig,
sensitiveSettings,
botBuildRequired: boolean
) => {
resetBotRuntimeError(projectId);
resetBotRuntimeLog(projectId);
setBotStatus(projectId, BotStatus.pending);
if (botBuildRequired) {
// Default recognizer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as React from 'react';

import httpClient from '../../../utils/httpUtil';
import { renderWithRecoil } from '../../../../__tests__/testUtils/renderWithRecoil';
import { botRuntimeErrorState, botStatusState } from '../../../recoilModel';
import { botBuildTimeErrorState, botStatusState } from '../../../recoilModel';
import { BotStatus } from '../../../constants';
import { BotRuntimeStatus } from '../../BotRuntimeController/BotRuntimeStatus';

Expand Down Expand Up @@ -100,7 +100,7 @@ describe('<BotRuntimeStatus />', () => {
it('should show error if bot start failed', async () => {
const { findByText } = renderWithRecoil(<BotRuntimeStatus projectId={projectId} />, ({ set }) => {
set(botStatusState(projectId), BotStatus.failed);
set(botRuntimeErrorState(projectId), {
set(botBuildTimeErrorState(projectId), {
title: 'Error',
message: 'Failed to bind to port 3979',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import * as React from 'react';

import { renderWithRecoil } from '../../../../__tests__/testUtils/renderWithRecoil';
import { botRuntimeErrorState, botStatusState } from '../../../recoilModel';
import { botBuildTimeErrorState, botStatusState } from '../../../recoilModel';
import { BotStatus, BotStatusesCopy } from '../../../constants';
import { BotStatusIndicator } from '../../BotRuntimeController/BotStatusIndicator';

Expand Down Expand Up @@ -41,7 +41,7 @@ describe('<BotStatusIndicator />', () => {
it('should show error if bot start failed', async () => {
const { findByText } = renderWithRecoil(<BotStatusIndicator projectId={projectId} />, ({ set }) => {
set(botStatusState(projectId), BotStatus.failed);
set(botRuntimeErrorState(projectId), {
set(botBuildTimeErrorState(projectId), {
title: 'Error',
message: 'Failed to bind to port 3979',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const state = {
};

const mocks = {
resetBotRuntimeError: jest.fn(),
resetBotRuntimeLog: jest.fn(),
publishToTarget: jest.fn(),
setBotStatus: jest.fn(),
stopBot: jest.fn(),
Expand All @@ -32,7 +32,7 @@ const initRecoilState = ({ set }) => {
});

set(dispatcherState, {
resetBotRuntimeError: mocks.resetBotRuntimeError,
resetBotRuntimeLog: mocks.resetBotRuntimeLog,
publishToTarget: mocks.publishToTarget,
setBotStatus: mocks.setBotStatus,
stopPublishBot: mocks.stopBot,
Expand All @@ -42,7 +42,7 @@ const initRecoilState = ({ set }) => {
// TODO: An integration test needs to be added to test this component better.
describe('useBotOperations', () => {
afterEach(() => {
mocks.resetBotRuntimeError.mockReset();
mocks.resetBotRuntimeLog.mockReset();
mocks.publishToTarget.mockReset();
mocks.setBotStatus.mockReset();
mocks.stopBot.mockReset();
Expand All @@ -61,7 +61,7 @@ describe('useBotOperations', () => {
await act(async () => {
result.current.startSingleBot(state.skillId);
});
expect(mocks.resetBotRuntimeError).toHaveBeenLastCalledWith(state.skillId);
expect(mocks.resetBotRuntimeLog).toHaveBeenLastCalledWith(state.skillId);
expect(mocks.publishToTarget).toHaveBeenLastCalledWith(
state.skillId,
defaultPublishConfig,
Expand Down
3 changes: 3 additions & 0 deletions Composer/packages/client/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ export const Text = {
get DOTNETFAILURE() {
return formatMessage('Composer needs .NET Core SDK');
},
get BOTRUNTIMEERROR() {
return formatMessage('Composer Runtime Error');
},
};

export enum LuisConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const OutputsTabLogHeader: React.FC<DebugPanelTabHeaderProps> = () => {
marginRight: '4px',
}}
>
{formatMessage('Outputs')}
{formatMessage('Output')}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,27 @@

/** @jsx jsx */
import { jsx } from '@emotion/core';
import { NeutralColors, SharedColors } from '@uifabric/fluent-theme';
import { NeutralColors } from '@uifabric/fluent-theme';
import { useRecoilValue } from 'recoil';
import { default as AnsiUp } from 'ansi_up';
import { useEffect, useRef } from 'react';
import sanitizeHtml from 'sanitize-html';
import formatMessage from 'format-message';

import { botRuntimeErrorState, botRuntimeLogState } from '../../../../../recoilModel';
import { botBuildTimeErrorState, dispatcherState, runtimeStandardOutputDataState } from '../../../../../recoilModel';
import { getDefaultFontSettings } from '../../../../../recoilModel/utils/fontUtil';
import httpClient from '../../../../../utils/httpUtil';
import { ErrorCallout } from '../../../../../components/BotRuntimeController/ErrorCallout';
import { checkIfDotnetVersionMissing, missingDotnetVersionError } from '../../../../../utils/runtimeErrors';
import { BotStartError } from '../../../../../recoilModel/types';
import { Text } from '../../../../../constants';

const genericErrorMessage = () => {
return {
message: 'Runtime Log',
summary: formatMessage('Error occurred trying to fetch runtime standard output'),
};
};

const ansiUp = new AnsiUp();
const DEFAULT_FONT_SETTINGS = getDefaultFontSettings();
Expand All @@ -21,15 +33,69 @@ const createMarkup = (txt: string) => {
};

export const RuntimeOutputLog: React.FC<{ projectId: string }> = ({ projectId }) => {
const runtimeLogs = useRecoilValue(botRuntimeLogState(projectId));
const botRuntimeErrors = useRecoilValue(botRuntimeErrorState(projectId));
const runtimeData = useRecoilValue(runtimeStandardOutputDataState(projectId));
const botBuildErrors = useRecoilValue(botBuildTimeErrorState(projectId));
const { setRuntimeStandardOutputData, setApplicationLevelError } = useRecoilValue(dispatcherState);

const runtimeLogsContainerRef = useRef<HTMLDivElement | null>(null);

const runtimeTrafficChannel = useRef<WebSocket | null>(null);

useEffect(() => {
if (runtimeLogsContainerRef?.current) {
runtimeLogsContainerRef.current.scrollTop = runtimeLogsContainerRef.current.scrollHeight;
}
}, [runtimeLogs, botRuntimeErrors]);
}, [runtimeData]);

useEffect(() => {
const setupLogConnection = async () => {
try {
const runtimeStreamUrl = await httpClient.get(`/publish/runtimeLogUrl/${projectId}`);

runtimeTrafficChannel.current = new WebSocket(runtimeStreamUrl.data);

if (runtimeTrafficChannel.current) {
runtimeTrafficChannel.current.onmessage = (event) => {
const data: { standardError: string; standardOutput: string } = JSON.parse(event.data);

let standardError: BotStartError | null = null;
if (data.standardError) {
const isDotnetError = checkIfDotnetVersionMissing({
message: data.standardError ?? '',
});

if (isDotnetError) {
standardError = {
title: Text.DOTNETFAILURE,
...missingDotnetVersionError,
};
} else {
standardError = {
title: Text.BOTRUNTIMEERROR,
message: data.standardError,
};
}
}
setRuntimeStandardOutputData(projectId, {
standardError,
standardOutput: data.standardOutput,
});
};
}
} catch (ex) {
setApplicationLevelError(genericErrorMessage());
}
};

if (!runtimeTrafficChannel.current) {
setupLogConnection();
}

return () => {
runtimeTrafficChannel.current?.close();
runtimeTrafficChannel.current = null;
};
}, []);

return (
<div
Expand All @@ -38,33 +104,31 @@ export const RuntimeOutputLog: React.FC<{ projectId: string }> = ({ projectId })
height: 'calc(100% - 25px)',
display: 'flex',
flexDirection: 'column',
padding: '15px 24px',
padding: '10px 16px',
fontSize: DEFAULT_FONT_SETTINGS.fontSize,
fontFamily: DEFAULT_FONT_SETTINGS.fontFamily,
color: `${NeutralColors.black}`,
width: 'auto',
overflowY: 'auto',
overflowX: 'hidden',
}}
data-testid="Runtime-Output-Logs"
data-testid="runtime-output-logs"
>
<div
css={{
margin: 0,
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
lineHeight: '20px',
}}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={createMarkup(runtimeLogs)}
/>
<div
css={{
color: `${SharedColors.red10}`,
}}
>
<ErrorCallout error={botRuntimeErrors} />
</div>
{runtimeData.standardOutput && (
<div
css={{
margin: 0,
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
lineHeight: '20px',
}}
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={createMarkup(runtimeData.standardOutput)}
data-testid="runtime-standard-output"
/>
)}
{botBuildErrors && <ErrorCallout error={botBuildErrors} />}
{runtimeData.standardError && <ErrorCallout error={runtimeData.standardError} />}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import formatMessage from 'format-message';

import { TabExtensionConfig, RuntimeLogTabKey } from '../types';

import { OutputsTabContent } from './OutputsTabContent';
import { OutputsTabLogHeader } from './OutputsTabLogHeader';
import { OutputsTabContent } from './OutputTabContent';
import { OutputsTabLogHeader } from './OutputTabLogHeader';

export const RuntimeOutputTabConfig: TabExtensionConfig = {
key: RuntimeLogTabKey,
Expand Down
Loading