Skip to content
This repository was archived by the owner on Jul 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const ImportModal: React.FC<RouteComponentProps> = (props) => {
title: '',
onRenderCardContent: ImportSuccessNotificationWrapper({
importedToExisting: true,
location: existingProject.location,
location: path,
}),
});
addNotification(notification);
Expand Down
2 changes: 1 addition & 1 deletion Composer/packages/client/src/pages/publish/Publish.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ const LogDialog = (props) => {
<Dialog
dialogContentProps={logDialogProps}
hidden={false}
minWidth={450}
minWidth={700}
modalProps={{ isBlocking: true }}
onDismiss={props.onDismiss}
>
Expand Down
30 changes: 25 additions & 5 deletions Composer/packages/client/src/pages/publish/publishStatusList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky';
import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip';
import { Selection } from 'office-ui-fabric-react/lib/DetailsList';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { Link } from 'office-ui-fabric-react/lib/Link';
import { Spinner, SpinnerSize } from 'office-ui-fabric-react/lib/Spinner';
import moment from 'moment';
import { useMemo, useState, useEffect } from 'react';
Expand All @@ -35,6 +36,10 @@ export interface IStatus {
status: number;
message: string;
comment: string;
action?: {
href: string;
label: string;
};
}

function onRenderDetailsHeader(props, defaultRender) {
Expand Down Expand Up @@ -99,8 +104,8 @@ export const PublishStatusList: React.FC<IStatusListProps> = (props) => {
name: formatMessage('Status'),
className: 'publishstatus',
fieldName: 'status',
minWidth: 70,
maxWidth: 90,
minWidth: 40,
maxWidth: 40,
isResizable: true,
data: 'string',
onRender: (item: IStatus) => {
Expand All @@ -123,14 +128,29 @@ export const PublishStatusList: React.FC<IStatusListProps> = (props) => {
name: formatMessage('Message'),
className: 'publishmessage',
fieldName: 'message',
minWidth: 70,
maxWidth: 90,
minWidth: 150,
maxWidth: 300,
isResizable: true,
isCollapsible: true,
isMultiline: true,
data: 'string',
onRender: (item: IStatus) => {
return <span>{item.message}</span>;
return (
<span>
{item.message}
{item.action && (
<Link
aria-label={item.action.label}
href={item.action.href}
rel="noopener noreferrer"
style={{ marginLeft: '3px' }}
target="_blank"
>
{item.action.label}
</Link>
)}
</span>
);
},
isPadded: true,
},
Expand Down
1 change: 1 addition & 0 deletions Composer/packages/types/src/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type PullResponse = {
error?: any;
eTag?: string;
status: number;
/** Path to the pulled .zip containing updated bot content */
zipPath?: string;
};

Expand Down
1 change: 1 addition & 0 deletions Composer/packages/types/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type IBotProject = {
fileStorage: any;
dir: string;
dataDir: string;
eTag?: string;
id: string | undefined;
name: string;
builder: any;
Expand Down
12 changes: 8 additions & 4 deletions Composer/packages/ui-plugins/cross-trained/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

import { PluginConfig } from '@bfc/extension-client';
import { SDKKinds } from '@bfc/shared';
import { SDKKinds, checkForPVASchema } from '@bfc/shared';
import formatMessage from 'format-message';

const config: PluginConfig = {
Expand All @@ -15,12 +15,16 @@ const config: PluginConfig = {
},
intentEditor: 'LuIntentEditor',
seedNewRecognizer: (shellData) => {
const { qnaFiles, luFiles, currentDialog, locale } = shellData;
const { qnaFiles, luFiles, currentDialog, locale, schemas } = shellData;
const qnaFile = qnaFiles.find((f) => f.id === `${currentDialog.id}.${locale}`);
const luFile = luFiles.find((f) => f.id === `${currentDialog.id}.${locale}`);

if (!qnaFile || !luFile) {
alert(formatMessage(`NO LU OR QNA FILE WITH NAME { id }`, { id: currentDialog.id }));
if (!luFile) {
alert(formatMessage(`NO LU FILE WITH NAME { id }`, { id: currentDialog.id }));
}

if (!qnaFile && !checkForPVASchema(schemas.sdk)) {
alert(formatMessage(`NO QNA FILE WITH NAME { id }`, { id: currentDialog.id }));
}

return `${currentDialog.id}.lu.qna`;
Expand Down
87 changes: 45 additions & 42 deletions extensions/pvaPublish/src/node/publish.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { IBotProject } from '@botframework-composer/types';
import { join } from 'path';
import { createReadStream, createWriteStream } from 'fs';
import { ensureDirSync, remove } from 'fs-extra';
import { createWriteStream } from 'fs';
import { ensureDirSync } from 'fs-extra';
import fetch, { RequestInit } from 'node-fetch';
import stream from 'stream';

import {
PVAPublishJob,
Expand Down Expand Up @@ -42,47 +43,39 @@ export const publish = async (
const { comment = '' } = metadata;

try {
logger.log('Starting publish to Power Virtual Agents.');
// authenticate with PVA
const base = baseUrl || getBaseUrl();
const creds = getAuthCredentials(base);
const accessToken = await getAccessToken(creds);

// TODO: Investigate optimizing stream logic before enabling extension.
// (https://github.com/microsoft/BotFramework-Composer/pull/4446#discussion_r510314378)

// where we will store the bot .zip
const zipDir = join(process.env.COMPOSER_TEMP_DIR as string, 'pva-publish');
ensureDirSync(zipDir);
const zipPath = join(zipDir, 'bot.zip');

// write the .zip to disk
const zipWriteStream = createWriteStream(zipPath);
// write the .zip to a buffer in memory
logger.log('Writing bot content to in-memory buffer.');
const botContentWriter = new stream.Writable();
const botContentData = [];
botContentWriter._write = (chunk, encoding, callback) => {
botContentData.push(chunk);
callback(); // let the internal write() call know that the _write() was successful
};
await new Promise((resolve, reject) => {
project.exportToZip((archive: NodeJS.ReadStream & { finalize: () => void; on: (ev, listener) => void }) => {
archive.on('error', (err) => {
console.error('Got error trying to export to zip: ', err);
reject(err.message);
});
archive.pipe(zipWriteStream);
archive.on('end', () => {
archive.unpipe();
zipWriteStream.end();
resolve();
});
});
project.exportToZip(
{ files: ['*.botproject'], directories: ['/knowledge-base/'] },
(archive: NodeJS.ReadStream & { finalize: () => void; on: (ev, listener) => void }) => {
archive.on('error', (err) => {
console.error('Got error trying to export to zip: ', err);
reject(err.message);
});
archive.on('end', () => {
archive.unpipe();
logger.log('Done reading bot content.');
resolve();
});
archive.pipe(botContentWriter);
}
);
});

// open up the .zip for reading
const zipReadStream = createReadStream(zipPath);
await new Promise((resolve, reject) => {
zipReadStream.on('error', (err) => {
reject(err);
});
zipReadStream.once('readable', () => {
resolve();
});
});
const length = zipReadStream.readableLength;
const botContent = Buffer.concat(botContentData);
logger.log('In-memory buffer created from bot content.');

// initiate the publish job
let url = `${base}api/botmanagement/${API_VERSION}/environments/${envId}/bots/${botId}/composer/publishoperations?deleteMissingDependencies=${deleteMissingDependencies}`;
Expand All @@ -91,15 +84,16 @@ export const publish = async (
}
const res = await fetch(url, {
method: 'POST',
body: zipReadStream,
body: botContent,
headers: {
...getAuthHeaders(accessToken, tenantId),
'Content-Type': 'application/zip',
'Content-Length': length.toString(),
'Content-Length': botContent.buffer.byteLength,
'If-Match': project.eTag,
},
});
const job: PVAPublishJob = await res.json();
logger.log('Publish job started: %O', job);

// transform the PVA job to a publish response
const result = xformJobToResult(job);
Expand All @@ -109,7 +103,7 @@ export const publish = async (
ensurePublishProfileHistory(botProjectId, profileName);
publishHistory[botProjectId][profileName].unshift(result);

remove(zipDir); // clean up zip -- fire and forget
logger.log('Publish call successful.');

return {
status: result.status,
Expand Down Expand Up @@ -173,6 +167,9 @@ export const getStatus = async (
logger.log('Got updated status from publish job: %O', job);

// transform the PVA job to a publish response
if (!job.lastUpdateTimeUtc) {
job.lastUpdateTimeUtc = Date.now().toString(); // patch update time if server doesn't send one
}
const result = xformJobToResult(job);

// update publish history
Expand Down Expand Up @@ -291,13 +288,19 @@ const xformJobToResult = (job: PVAPublishJob): PublishResult => {
eTag: job.importedContentEtag,
id: job.operationId, // what is this used for in Composer?
log: (job.diagnostics || []).map((diag) => `---\n${JSON.stringify(diag, null, 2)}\n---\n`).join('\n'),
message: getUserFriendlyMessage(job.state),
message: getUserFriendlyMessage(job),
time: new Date(job.lastUpdateTimeUtc),
status: getStatusFromJobState(job.state),
action: getAction(job),
};
return result;
};

const getAction = (job) => {
if (job.state !== 'Done' || job.testUrl == null || job.testUrl == undefined) return null;
return { href: job.testUrl, label: 'Test in Power Virtual Agents' };
};

const getStatusFromJobState = (state: PublishState): number => {
switch (state) {
case 'Done':
Expand Down Expand Up @@ -337,8 +340,8 @@ const getOperationIdOfLastJob = (botProjectId: string, profileName: string): str
return '';
};

const getUserFriendlyMessage = (state: PublishState): string => {
switch (state) {
const getUserFriendlyMessage = (job: PVAPublishJob): string => {
switch (job.state) {
case 'Done':
return 'Publish successful.';

Expand Down
2 changes: 2 additions & 0 deletions extensions/pvaPublish/src/node/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type PVAPublishJob = {
operationId: string;
startTimeUtc: string;
state: PublishState;
testUrl: string;
};

type DiagnosticInfo = {
Expand Down Expand Up @@ -52,6 +53,7 @@ export interface PublishResult {
message: string;
status?: number;
time?: Date;
action?: { href: string; label: string };
}

/** Copied from @bfc/extension */
Expand Down