diff --git a/Composer/packages/client/src/pages/botProject/CreatePublishProfileDialog.tsx b/Composer/packages/client/src/pages/botProject/CreatePublishProfileDialog.tsx index 402d8e3918..530c1f0764 100644 --- a/Composer/packages/client/src/pages/botProject/CreatePublishProfileDialog.tsx +++ b/Composer/packages/client/src/pages/botProject/CreatePublishProfileDialog.tsx @@ -22,10 +22,11 @@ import { actionButton } from './styles'; type CreatePublishProfileDialogProps = { projectId: string; + onUpdateIsCreateProfileFromSkill: (isCreateProfileFromSkill: boolean) => void; }; export const CreatePublishProfileDialog: React.FC = (props) => { - const { projectId } = props; + const { projectId, onUpdateIsCreateProfileFromSkill } = props; const { publishTargets } = useRecoilValue(settingsState(projectId)); const { getPublishTargetTypes, setPublishTargets } = useRecoilValue(dispatcherState); const publishTypes = useRecoilValue(publishTypesState(projectId)); @@ -102,6 +103,7 @@ export const CreatePublishProfileDialog: React.FC ) : null} diff --git a/Composer/packages/client/src/pages/botProject/create-publish-profile/PublishProfileDialog.tsx b/Composer/packages/client/src/pages/botProject/create-publish-profile/PublishProfileDialog.tsx index b98bfe332b..6fee5f553b 100644 --- a/Composer/packages/client/src/pages/botProject/create-publish-profile/PublishProfileDialog.tsx +++ b/Composer/packages/client/src/pages/botProject/create-publish-profile/PublishProfileDialog.tsx @@ -30,6 +30,7 @@ type PublishProfileDialogProps = { types: PublishType[]; projectId: string; setPublishTargets: (targets: PublishTarget[], projectId: string) => Promise; + onUpdateIsCreateProfileFromSkill?: (isCreateProfileFromSkill: boolean) => void; }; const Page = { @@ -38,7 +39,15 @@ const Page = { }; export const PublishProfileDialog: React.FC = (props) => { - const { current, types, projectId, closeDialog, targets, setPublishTargets } = props; + const { + current, + types, + projectId, + closeDialog, + targets, + setPublishTargets, + onUpdateIsCreateProfileFromSkill, + } = props; const [name, setName] = useState(current?.item.name || ''); const [targetType, setTargetType] = useState(current?.item.type || ''); @@ -194,7 +203,8 @@ export const PublishProfileDialog: React.FC = (props) arm = getTokenFromCache('accessToken'); graph = getTokenFromCache('graphToken'); } - provisionToTarget(fullConfig, config.type, projectId, arm, graph, current?.item); + await provisionToTarget(fullConfig, config.type, projectId, arm, graph, current?.item); + onUpdateIsCreateProfileFromSkill?.(true); }; }, [name, targetType, types, savePublishTarget]); diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx index 18f7c5c883..3eb56e2e22 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/constants.tsx @@ -86,6 +86,7 @@ export interface ContentProps { setSelectedDialogs: (dialogs: any[]) => void; setSelectedTriggers: (selectedTriggers: any[]) => void; setSkillManifest: (_: Partial) => void; + onUpdateIsCreateProfileFromSkill: (isCreateProfileFromSkill: boolean) => void; schema: JSONSchema7; selectedDialogs: any[]; selectedTriggers: any[]; diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectProfile.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectProfile.tsx index 51bf07dc99..ea1f8b5a65 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectProfile.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/content/SelectProfile.tsx @@ -76,7 +76,14 @@ export const getManifestId = ( return fileId; }; -export const SelectProfile: React.FC = ({ manifest, setSkillManifest, value, onChange, projectId }) => { +export const SelectProfile: React.FC = ({ + manifest, + setSkillManifest, + value, + onChange, + projectId, + onUpdateIsCreateProfileFromSkill, +}) => { const [publishingTargets, setPublishingTargets] = useState([]); const [currentTarget, setCurrentTarget] = useState(); const { updateCurrentTarget } = useRecoilValue(dispatcherState); @@ -201,7 +208,10 @@ export const SelectProfile: React.FC = ({ manifest, setSkillManife ) : (
- +
); }; diff --git a/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx b/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx index 5efb3939d9..a4c034e227 100644 --- a/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx +++ b/Composer/packages/client/src/pages/design/exportSkillModal/index.tsx @@ -3,7 +3,7 @@ /** @jsx jsx */ import { jsx } from '@emotion/core'; -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import formatMessage from 'format-message'; import { Dialog, DialogFooter, DialogType } from 'office-ui-fabric-react/lib/Dialog'; import { DefaultButton, PrimaryButton } from 'office-ui-fabric-react/lib/Button'; @@ -15,6 +15,7 @@ import { navigate } from '@reach/router'; import { isUsingAdaptiveRuntime } from '@bfc/shared'; import cloneDeep from 'lodash/cloneDeep'; +import { Notification } from '../../../recoilModel/types'; import { dispatcherState, skillManifestsState, @@ -26,11 +27,22 @@ import { settingsState, rootBotProjectIdSelector, } from '../../../recoilModel'; -import { mergePropertiesManagedByRootBot } from '../../../recoilModel/dispatchers/utils/project'; +import { + getSensitiveProperties, + mergePropertiesManagedByRootBot, +} from '../../../recoilModel/dispatchers/utils/project'; +import { getTokenFromCache } from '../../../utils/auth'; +import { ApiStatus, PublishStatusPollingUpdater } from '../../../utils/publishStatusPollingUpdater'; +import { + getSkillPublishedNotificationCardProps, + getSkillPendingNotificationCardProps, +} from '../../publish/Notifications'; +import { createNotification } from '../../../recoilModel/dispatchers/notification'; +import { getManifestUrl } from '../../../utils/skillManifestUtil'; -import { styles } from './styles'; -import { generateSkillManifest } from './generateSkillManifest'; import { editorSteps, ManifestEditorSteps, order } from './constants'; +import { generateSkillManifest } from './generateSkillManifest'; +import { styles } from './styles'; interface ExportSkillModalProps { isOpen: boolean; @@ -46,11 +58,12 @@ const ExportSkillModal: React.FC = ({ onSubmit, onDismiss const luFiles = useRecoilValue(luFilesSelectorFamily(projectId)); const qnaFiles = useRecoilValue(qnaFilesSelectorFamily(projectId)); const skillManifests = useRecoilValue(skillManifestsState(projectId)); - const { updateSkillManifest } = useRecoilValue(dispatcherState); + const { updateSkillManifest, publishToTarget, addNotification, updateNotification } = useRecoilValue(dispatcherState); const [currentStep, setCurrentStep] = useState(0); const [errors, setErrors] = useState({}); const [schema, setSchema] = useState({}); + const [isHidden, setIsHidden] = useState(false); const [skillManifest, setSkillManifest] = useState>({}); const { content = {}, id } = skillManifest; @@ -71,6 +84,94 @@ const ExportSkillModal: React.FC = ({ onSubmit, onDismiss !isAdaptive ? skillConfiguration?.allowedCallers : runtimeSettings?.skills?.allowedCallers ?? [] ); + const [isCreateProfileFromSkill, setIsCreateProfileFromSkill] = useState(false); + const publishUpdaterRef = useRef(); + const publishNotificationRef = useRef(); + const resetDialog = () => { + handleDismiss(); + setIsHidden(false); + }; + // stop polling updater + const stopUpdater = () => { + publishUpdaterRef.current && publishUpdaterRef.current.stop(); + publishUpdaterRef.current = undefined; + resetDialog(); + }; + + const deleteNotificationCard = async () => { + publishNotificationRef.current = undefined; + }; + const changeNotificationStatus = async (data) => { + const { apiResponse } = data; + if (!apiResponse) { + stopUpdater(); + deleteNotificationCard(); + return; + } + const responseData = apiResponse.data; + + if (responseData.status !== ApiStatus.Publishing) { + // Show result notifications + const displayedNotification = publishNotificationRef.current; + const publishUpdater = publishUpdaterRef.current; + if (displayedNotification && publishUpdater) { + const currentTarget = publishTargets?.find((target) => target.name === publishUpdater.getPublishTargetName()); + const url = currentTarget + ? getManifestUrl(JSON.parse(currentTarget.configuration).hostname, skillManifest) + : ''; + const notificationCard = getSkillPublishedNotificationCardProps({ ...responseData }, url); + updateNotification(displayedNotification.id, notificationCard); + } + stopUpdater(); + } + }; + + useEffect(() => { + const publish = async () => { + try { + if (!publishTargets || publishTargets.length === 0) return; + const currentTarget = publishTargets.find((item) => { + const config = JSON.parse(item.configuration); + return ( + config.settings && + config.settings.MicrosoftAppId && + config.hostname && + config.settings.MicrosoftAppId.length > 0 && + config.hostname.length > 0 + ); + }); + if (isCreateProfileFromSkill && currentTarget) { + const skillPublishPenddingNotificationCard = getSkillPendingNotificationCardProps(); + publishNotificationRef.current = createNotification(skillPublishPenddingNotificationCard); + addNotification(publishNotificationRef.current); + const sensitiveSettings = getSensitiveProperties(settings); + const token = getTokenFromCache('accessToken'); + await publishToTarget(projectId, currentTarget, { comment: '' }, sensitiveSettings, token); + publishUpdaterRef.current = new PublishStatusPollingUpdater(projectId, currentTarget.name); + publishUpdaterRef.current.start(changeNotificationStatus); + } + } catch (e) { + console.log(e.message); + } + }; + publish(); + }, [isCreateProfileFromSkill, publishTargets?.length]); + useEffect(() => { + isCreateProfileFromSkill && setIsHidden(true); + }, [isCreateProfileFromSkill]); + + useEffect(() => { + // Clear intervals when unmount + return () => { + if (publishUpdaterRef.current) { + stopUpdater(); + } + if (publishNotificationRef.current) { + deleteNotificationCard(); + } + }; + }, []); + const updateAllowedCallers = React.useCallback( (allowedCallers: string[] = []) => { const updatedSetting = isAdaptive @@ -167,7 +268,7 @@ const ExportSkillModal: React.FC = ({ onSubmit, onDismiss title: title(), styles: styles.dialog, }} - hidden={false} + hidden={isHidden} modalProps={{ isBlocking: false, styles: styles.modal, @@ -211,6 +312,7 @@ const ExportSkillModal: React.FC = ({ onSubmit, onDismiss value={content} onChange={(manifestContent) => setSkillManifest({ ...skillManifest, content: manifestContent })} onUpdateCallers={setCallers} + onUpdateIsCreateProfileFromSkill={setIsCreateProfileFromSkill} /> diff --git a/Composer/packages/client/src/pages/publish/Notifications.tsx b/Composer/packages/client/src/pages/publish/Notifications.tsx index faa33dcd9f..bee62cb218 100644 --- a/Composer/packages/client/src/pages/publish/Notifications.tsx +++ b/Composer/packages/client/src/pages/publish/Notifications.tsx @@ -71,7 +71,10 @@ export const getPublishedNotificationCardProps = (item: BotStatus): CardProps => }; }; -export const getSkillPublishedNotificationCardProps = (item: BotStatus, url?: string): CardProps => { +export const getSkillPublishedNotificationCardProps = ( + item: { status: number } & Record, + url?: string +): CardProps => { const skillCardContent = css` display: flex; padding: 0 16px 16px 12px; @@ -185,6 +188,22 @@ export const getPendingNotificationCardProps = (items: BotStatus[], isSkill = fa }; }; +export const getSkillPendingNotificationCardProps = (): CardProps => { + return { + title: '', + description: formatMessage('Publishing your skill...'), + type: 'pending', + onRenderCardContent: (props) => ( +
+ +
+ +
+
+ ), + }; +}; + export const getPendingQNANotificationCardProps = (): CardProps => { return { title: '', diff --git a/Composer/packages/client/src/pages/publish/publishPageUtils.tsx b/Composer/packages/client/src/pages/publish/publishPageUtils.tsx index 028db9c728..73d90b13fe 100644 --- a/Composer/packages/client/src/pages/publish/publishPageUtils.tsx +++ b/Composer/packages/client/src/pages/publish/publishPageUtils.tsx @@ -4,6 +4,7 @@ import { PublishTarget, SkillManifestFile } from '@bfc/shared'; import { ApiStatus } from '../../utils/publishStatusPollingUpdater'; +import { getManifestUrl } from '../../utils/skillManifestUtil'; import { Bot, BotStatus, BotPublishHistory, BotProjectType, BotPropertyType } from './type'; @@ -35,11 +36,9 @@ const findSkillManifestUrl = (skillManifests: SkillManifestFile[], hostname: str const urls: string[] = []; for (const skillManifest of skillManifests || []) { for (const endpoint of skillManifest?.content?.endpoints || []) { - if ( - endpoint?.msAppId === appId && - !urls.includes(`https://${hostname}.azurewebsites.net/manifests/${skillManifest.id}.json`) - ) { - urls.push(`https://${hostname}.azurewebsites.net/manifests/${skillManifest.id}.json`); + const url = getManifestUrl(hostname, skillManifest); + if (endpoint?.msAppId === appId && !urls.includes(url)) { + urls.push(url); } } } diff --git a/Composer/packages/client/src/recoilModel/dispatchers/notification.ts b/Composer/packages/client/src/recoilModel/dispatchers/notification.ts index be0ec113bd..1b0fa5c63e 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/notification.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/notification.ts @@ -25,8 +25,8 @@ export const deleteNotificationInternal = ({ reset, set }: CallbackInterface, id return notifications.filter((notification) => notification !== id); }); }; -export const updateNotificationInternal = ({ set }: CallbackInterface, id: string, newValue: CardProps) => { - set(notificationsState(id), { ...newValue, id: id }); +export const updateNotificationInternal = ({ set }: CallbackInterface, id: string, newValue: Partial) => { + set(notificationsState(id), (current) => ({ ...current, ...newValue })); // check if notification exist set(notificationIdsState, (notificationIds) => { if (notificationIds.some((notificationId) => notificationId === id)) { @@ -53,10 +53,17 @@ export const notificationDispatcher = () => { set(notificationsState(id), (notification) => ({ ...notification, hidden: true })); }); + const updateNotification = useRecoilCallback( + (callbackHelper: CallbackInterface) => (id: string, newValue: Partial) => { + updateNotificationInternal(callbackHelper, id, newValue); + } + ); + return { addNotification, deleteNotification, hideNotification, markNotificationAsRead, + updateNotification, }; }; diff --git a/Composer/packages/client/src/recoilModel/dispatchers/provision.ts b/Composer/packages/client/src/recoilModel/dispatchers/provision.ts index 35b341969d..5046bc0c0e 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/provision.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/provision.ts @@ -90,7 +90,7 @@ export const provisionDispatcher = () => { }); // call provision status api interval to update the state. - updateProvisionStatus( + await updateProvisionStatus( callbackHelpers, result.data.id, projectId, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts index f77fe39563..4fb78cc5c1 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/publisher.ts @@ -186,6 +186,7 @@ export const publisherDispatcher = () => { const referredLuFiles = luUtil.checkLuisBuild(luFiles, dialogs); const referredQnaFiles = qnaUtil.checkQnaBuild(qnaFiles, dialogs); const response = await httpClient.post(`/publish/${projectId}/publish/${target.name}`, { + publishTarget: target, accessToken: token, metadata: { ...metadata, @@ -251,7 +252,7 @@ export const publisherDispatcher = () => { const { set, snapshot } = callbackHelpers; try { const filePersistence = await snapshot.getPromise(filePersistenceState(projectId)); - filePersistence.flush(); + await filePersistence.flush(); const response = await httpClient.get(`/publish/${projectId}/history/${target.name}`); set(publishHistoryState(projectId), (publishHistory) => ({ ...publishHistory, diff --git a/Composer/packages/client/src/utils/publishStatusPollingUpdater.ts b/Composer/packages/client/src/utils/publishStatusPollingUpdater.ts index 9545c78eb2..24993dff27 100644 --- a/Composer/packages/client/src/utils/publishStatusPollingUpdater.ts +++ b/Composer/packages/client/src/utils/publishStatusPollingUpdater.ts @@ -99,6 +99,10 @@ export class PublishStatusPollingUpdater { isSameUpdater(botProjectId: string, targetName: string) { return this.botProjectId === botProjectId && this.publishTargetName === targetName; } + + getPublishTargetName() { + return this.publishTargetName; + } } export const pollingUpdaterList: PublishStatusPollingUpdater[] = []; diff --git a/Composer/packages/client/src/utils/skillManifestUtil.ts b/Composer/packages/client/src/utils/skillManifestUtil.ts new file mode 100644 index 0000000000..2f6e36ff19 --- /dev/null +++ b/Composer/packages/client/src/utils/skillManifestUtil.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export const getManifestUrl = (hostname, skillManifest) => + `https://${hostname}.azurewebsites.net/manifests/${skillManifest.id}.json`; diff --git a/Composer/packages/server/src/controllers/publisher.ts b/Composer/packages/server/src/controllers/publisher.ts index b1fc0cfc39..38314bee70 100644 --- a/Composer/packages/server/src/controllers/publisher.ts +++ b/Composer/packages/server/src/controllers/publisher.ts @@ -4,7 +4,7 @@ import { join } from 'path'; import merge from 'lodash/merge'; -import { defaultPublishConfig, PublishResult } from '@bfc/shared'; +import { defaultPublishConfig, PublishResult, PublishTarget } from '@bfc/shared'; import { ensureDirSync, remove } from 'fs-extra'; import extractZip from 'extract-zip'; @@ -22,6 +22,7 @@ function extensionImplementsMethod(extensionName: string, methodName: string): b return extensionName && ExtensionContext.extensions.publish[extensionName]?.methods[methodName]; } +let currentPublishTarget: PublishTarget; export const PublishController = { getTypes: async (req, res) => { res.json( @@ -53,8 +54,9 @@ export const PublishController = { publish: async (req, res) => { const target: string = req.params.target; const user = await ExtensionContext.getUserFromRequest(req); - const { accessToken = '', metadata, sensitiveSettings } = req.body; + const { accessToken = '', metadata, sensitiveSettings, publishTarget } = req.body; const projectId: string = req.params.projectId; + currentPublishTarget = publishTarget; const currentProject = await BotProjectService.getProjectById(projectId, user); // deal with publishTargets not exist in settings @@ -62,7 +64,11 @@ export const PublishController = { const allTargets = [defaultPublishConfig, ...publishTargets]; const profiles = allTargets.filter((t) => t.name === target); - const profile = profiles.length ? profiles[0] : undefined; + let profile: PublishTarget = profiles[0]; + if (!profile) { + profile = publishTarget; + currentProject.settings?.publishTargets && currentProject.settings?.publishTargets.push(publishTarget); + } const extensionName = profile ? profile.type : ''; // get the publish plugin key try { @@ -143,7 +149,11 @@ export const PublishController = { const allTargets = [defaultPublishConfig, ...publishTargets]; const profiles = allTargets.filter((t) => t.name === target); - const profile = profiles.length ? profiles[0] : undefined; + let profile: PublishTarget = profiles[0]; + if (!profile) { + profile = currentPublishTarget; + currentProject.settings?.publishTargets && currentProject.settings?.publishTargets.push(currentPublishTarget); + } // get the publish plugin key const extensionName = profile ? profile.type : ''; if (profile && extensionImplementsMethod(extensionName, 'getStatus')) {