diff --git a/Composer/packages/client/src/components/BotRuntimeController/ErrorCallout.tsx b/Composer/packages/client/src/components/BotRuntimeController/ErrorCallout.tsx index 035bad5cef..0ec8db5c8b 100644 --- a/Composer/packages/client/src/components/BotRuntimeController/ErrorCallout.tsx +++ b/Composer/packages/client/src/components/BotRuntimeController/ErrorCallout.tsx @@ -33,8 +33,8 @@ const descriptionText = css` `; const descriptionLongText = css` - overflow: auto; font-size: small; + white-space: pre-wrap; `; const descriptionShow = css``; const descriptionHide = css` diff --git a/Composer/packages/client/src/components/GetStarted/GetStarted.tsx b/Composer/packages/client/src/components/GetStarted/GetStarted.tsx index 08a1309e52..0c1aa2cd8d 100644 --- a/Composer/packages/client/src/components/GetStarted/GetStarted.tsx +++ b/Composer/packages/client/src/components/GetStarted/GetStarted.tsx @@ -5,8 +5,9 @@ import { jsx } from '@emotion/core'; import React from 'react'; import formatMessage from 'format-message'; -import { Panel, IPanelStyles } from 'office-ui-fabric-react/lib/Panel'; -import { Pivot, PivotItem, IPivotStyles } from 'office-ui-fabric-react/lib/Pivot'; +import { Panel, IPanelStyles, PanelType } from 'office-ui-fabric-react/lib/Panel'; +import { Pivot, PivotItem } from 'office-ui-fabric-react/lib/Pivot'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; import { GetStartedNextSteps } from './GetStartedNextSteps'; import { GetStartedLearn } from './GetStartedLearn'; @@ -25,45 +26,35 @@ const panelStyles = { root: { marginTop: 50, }, - navigation: { - display: 'block', - height: 'auto', - }, } as IPanelStyles; -const pivotStyles = { root: { paddingLeft: 20, paddingTop: 10, width: '100%' } } as IPivotStyles; - export const GetStarted: React.FC = (props) => { const { projectId, onDismiss } = props; const renderTabs = () => { return ( - - - - - - - - + + + + + + + + + + ); }; - const onRenderNavigationContent = React.useCallback( - (props, defaultRender) => ( -
{defaultRender(props)}
- ), - [] - ); - return ( ); }; diff --git a/Composer/packages/client/src/components/GetStarted/GetStartedNextSteps.tsx b/Composer/packages/client/src/components/GetStarted/GetStartedNextSteps.tsx index 030a7d43f8..6b65bc9e42 100644 --- a/Composer/packages/client/src/components/GetStarted/GetStartedNextSteps.tsx +++ b/Composer/packages/client/src/components/GetStarted/GetStartedNextSteps.tsx @@ -18,6 +18,8 @@ import { dispatcherState, settingsState } from '../../recoilModel'; import { mergePropertiesManagedByRootBot } from '../../recoilModel/dispatchers/utils/project'; import { rootBotProjectIdSelector } from '../../recoilModel/selectors/project'; import { navigateTo } from '../../utils/navigation'; +import { DisableFeatureToolTip } from '../DisableFeatureToolTip'; +import { usePVACheck } from '../../hooks/usePVACheck'; import { GetStartedTask } from './GetStartedTask'; import { NextSteps } from './types'; @@ -48,6 +50,7 @@ export const GetStartedNextSteps: React.FC = (props) => { const [highlightLUIS, setHighlightLUIS] = useState(false); const [highlightQNA, setHighlightQNA] = useState(false); + const isPVABot = usePVACheck(projectId); const hideManageLuis = () => { setDisplayManageLuis(false); @@ -131,6 +134,7 @@ export const GetStartedNextSteps: React.FC = (props) => { setDisplayManageLuis(true); } }, + isDisabled: isPVABot, }); } if (props.requiresQNA) { @@ -151,6 +155,7 @@ export const GetStartedNextSteps: React.FC = (props) => { setDisplayManageQNA(true); } }, + isDisabled: isPVABot, }); } setRequiredNextSteps(newNextSteps); @@ -170,6 +175,7 @@ export const GetStartedNextSteps: React.FC = (props) => { TelemetryClient.track('GettingStartedActionClicked', { taskName: 'publishing', priority: 'recommended' }); openLink(linkToPublishProfile); }, + isDisabled: false, }); } setRecommendedNextSteps(newRecomendedSteps); @@ -185,6 +191,7 @@ export const GetStartedNextSteps: React.FC = (props) => { TelemetryClient.track('GettingStartedActionClicked', { taskName: 'packages', priority: 'optional' }); openLink(linkToPackageManager); }, + isDisabled: isPVABot, }, { key: 'editlg', @@ -196,6 +203,7 @@ export const GetStartedNextSteps: React.FC = (props) => { TelemetryClient.track('GettingStartedActionClicked', { taskName: 'editlg', priority: 'optional' }); openLink(linkToLGEditor); }, + isDisabled: false, }, { key: 'editlu', @@ -207,6 +215,7 @@ export const GetStartedNextSteps: React.FC = (props) => { TelemetryClient.track('GettingStartedActionClicked', { taskName: 'editlu', priority: 'optional' }); openLink(linkToLUEditor); }, + isDisabled: false, }, { key: 'insights', @@ -220,6 +229,7 @@ export const GetStartedNextSteps: React.FC = (props) => { TelemetryClient.track('GettingStartedActionClicked', { taskName: 'insights', priority: 'optional' }); openLink(linkToAppInsights); }, + isDisabled: isPVABot, }, { key: 'devops', @@ -231,6 +241,7 @@ export const GetStartedNextSteps: React.FC = (props) => { TelemetryClient.track('GettingStartedActionClicked', { taskName: 'devops', priority: 'optional' }); openLink(linkToDevOps); }, + isDisabled: false, }, ]; @@ -245,12 +256,19 @@ export const GetStartedNextSteps: React.FC = (props) => { TelemetryClient.track('GettingStartedActionClicked', { taskName: 'connections', priority: 'optional' }); openLink(linkToConnections); }, + isDisabled: isPVABot, }); } setOptionalSteps(optSteps); }, [botProject, props.requiresLUIS, props.requiresQNA, props.showTeachingBubble]); + const getStartedTaskElement = (step: NextSteps) => ( + + + + ); + return (
@@ -304,27 +322,21 @@ export const GetStartedNextSteps: React.FC = (props) => { {requiredNextSteps.length ? (

{formatMessage('Required')}

- {requiredNextSteps.map((step) => ( - - ))} + {requiredNextSteps.map((step) => getStartedTaskElement(step))}
) : null} {recommendedNextSteps.length ? (

{formatMessage('Recommended')}

- {recommendedNextSteps.map((step) => ( - - ))} + {recommendedNextSteps.map((step) => getStartedTaskElement(step))}
) : null} {optionalSteps.length ? (

{formatMessage('Optional')}

- {optionalSteps.map((step) => ( - - ))} + {optionalSteps.map((step) => getStartedTaskElement(step))}
) : null}
diff --git a/Composer/packages/client/src/components/GetStarted/GetStartedTask.tsx b/Composer/packages/client/src/components/GetStarted/GetStartedTask.tsx index 09cab3fd85..3dc21ae99b 100644 --- a/Composer/packages/client/src/components/GetStarted/GetStartedTask.tsx +++ b/Composer/packages/client/src/components/GetStarted/GetStartedTask.tsx @@ -2,7 +2,7 @@ // Licensed under the MIT License. /** @jsx jsx */ -import { jsx } from '@emotion/core'; +import { css, jsx } from '@emotion/core'; import React from 'react'; import { FluentTheme, SharedColors } from '@uifabric/fluent-theme'; import { ActionButton } from 'office-ui-fabric-react/lib/Button'; @@ -15,6 +15,12 @@ type TaskProps = { step: NextSteps; }; +const getStartedStepStyle = (disabled?: boolean) => css` + margin-bottom: 20px; + pointer-events: ${disabled ? 'none' : 'auto'}; + opacity: ${disabled ? 0.4 : 1}; +`; + export const GetStartedTask: React.FC = (props) => { const icon = props.step.checked ? 'CompletedSolid' : props.step.required ? 'Error' : 'Completed'; const color = props.step.checked @@ -23,7 +29,7 @@ export const GetStartedTask: React.FC = (props) => { ? SharedColors.orange20 : SharedColors.cyanBlue10; return ( -
+
void; highlight?: (step?: NextSteps) => void; + isDisabled: boolean; }; diff --git a/Composer/packages/client/src/components/Header.tsx b/Composer/packages/client/src/components/Header.tsx index 2f4b4ccfd6..39a15d084e 100644 --- a/Composer/packages/client/src/components/Header.tsx +++ b/Composer/packages/client/src/components/Header.tsx @@ -158,7 +158,7 @@ export const Header = () => { const locale = useRecoilValue(localeState(projectId)); const appUpdate = useRecoilValue(appUpdateState); const [teachingBubbleVisibility, setTeachingBubbleVisibility] = useState(); - const [showGetStartedTeachingBubble, setshowGetStartedTeachingBubble] = useState(false); + const [showGetStartedTeachingBubble, setShowGetStartedTeachingBubble] = useState(false); const settings = useRecoilValue(settingsState(projectId)); const isWebChatPanelVisible = useRecoilValue(isWebChatPanelVisibleState); const botProjectSolutionLoaded = useRecoilValue(botProjectSpaceLoadedState); @@ -213,10 +213,10 @@ export const Header = () => { // pop out get started if #getstarted is in the URL useEffect(() => { if (location.hash === '#getstarted') { - setshowGetStartedTeachingBubble(true); + setShowGetStartedTeachingBubble(true); setShowGetStarted(true); } else { - setshowGetStartedTeachingBubble(false); + setShowGetStartedTeachingBubble(false); } }, [location]); @@ -386,7 +386,7 @@ export const Header = () => { { isWebChatPanelVisible={isWebChatPanelVisible} /> ) : null} - { - setShowTeachingBubble(true); - }} - onDismiss={() => { - toggleGetStarted(false); - }} - /> + + { + setShowTeachingBubble(true); + }} + onDismiss={() => { + toggleGetStarted(false); + }} + />
); }; diff --git a/Composer/packages/client/src/components/NavItem.tsx b/Composer/packages/client/src/components/NavItem.tsx index e5d0ae5c31..88e5b9f639 100644 --- a/Composer/packages/client/src/components/NavItem.tsx +++ b/Composer/packages/client/src/components/NavItem.tsx @@ -42,6 +42,7 @@ const link = (active: boolean, disabled: boolean) => css` : `&:hover { background-color: ${NeutralColors.gray50}; } + &:focus { outline: none; .ms-Fabric--isFocusVisible &::after { diff --git a/Composer/packages/client/src/components/Notifications/NotificationContainer.tsx b/Composer/packages/client/src/components/Notifications/NotificationContainer.tsx index 756176c364..9f7c8dbf87 100644 --- a/Composer/packages/client/src/components/Notifications/NotificationContainer.tsx +++ b/Composer/packages/client/src/components/Notifications/NotificationContainer.tsx @@ -4,10 +4,12 @@ /** @jsx jsx */ import { jsx, css } from '@emotion/core'; import isEmpty from 'lodash/isEmpty'; +import { Layer } from 'office-ui-fabric-react/lib/Layer'; import { useRecoilValue } from 'recoil'; import { dispatcherState } from '../../recoilModel'; import { notificationsSelector } from '../../recoilModel/selectors/notifications'; +import { zIndices } from '../../utils/zIndices'; import { NotificationCard } from './NotificationCard'; @@ -15,12 +17,15 @@ import { NotificationCard } from './NotificationCard'; const container = css` cursor: default; + top: 50px; + height: calc(100vh - 50px); position: absolute; right: 0px; padding: 6px; - z-index: 1; `; +const layerStyles = { root: { zIndex: zIndices.notificationContainer } }; + // -------------------- NotificationContainer -------------------- // export const NotificationContainer = () => { @@ -30,18 +35,20 @@ export const NotificationContainer = () => { if (isEmpty(notifications)) return null; return ( -
- {notifications.map((item) => { - return ( - - ); - })} -
+ +
+ {notifications.map((item) => { + return ( + + ); + })} +
+
); }; diff --git a/Composer/packages/client/src/components/Notifications/NotificationPanel.tsx b/Composer/packages/client/src/components/Notifications/NotificationPanel.tsx index 9db19059aa..a86bb3c4f1 100644 --- a/Composer/packages/client/src/components/Notifications/NotificationPanel.tsx +++ b/Composer/packages/client/src/components/Notifications/NotificationPanel.tsx @@ -57,7 +57,7 @@ const NotificationPanel: React.FC = ({ {formatMessage('Clear all')} - {defaultRender!(props)} + {defaultRender?.(props)}
), [handleClearAll] @@ -67,7 +67,7 @@ const NotificationPanel: React.FC = ({ = () => { return ( -
-
-

{formatMessage(`About`)}

-
-
{formatMessage(`Release: `) + (process.env.COMPOSER_VERSION || 'Unknown')}
-
-

- {formatMessage( - `Bot Framework Composer is a visual authoring canvas for building bots and other types of conversational application with the Microsoft Bot Framework technology stack. With Composer you will find everything you need to build a modern, state-of-the-art conversational experience.` - )} -

-

- {formatMessage( - `Bot Framework Composer enables developers and multi-disciplinary teams to build all kinds of conversational experiences, using the latest components from the Bot Framework: SDK, LG, LU, and declarative file formats, all without writing code.` - )} +

+
+
+ {formatMessage.rich( + 'Our privacy statement is located at https://go.microsoft.com/fwlink/?LinkID=824704. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.', + { + a: ({ children }) => {children}, + } + )} +
+
+ {formatMessage.rich( + '

Copyright (c) Microsoft Corporation.

MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

', + { p: ({ children }) =>

{children}

} + )} +
+
+ {formatMessage(`Release: `) + + (isElectron() + ? (window as any).appVersion + : `${process.env.COMPOSER_VERSION}-${process.env.GIT_SHA}` || 'Unknown')} +
+
+
+
{formatMessage(`SDK runtime packages`)}
+
- {formatMessage(`Learn more`)} + {process.env.SDK_PACKAGE_VERSION || 'Unknown'} -

-
-
-
-
{formatMessage(`SDK runtime packages`)}
-
- - {process.env.SDK_PACKAGE_VERSION || 'Unknown'} - -
+
+
+ + {formatMessage(`Getting Help`)} + +
+
+ - {formatMessage(`Getting Help`)} + {formatMessage(`Terms of Use`)}
-
-
- - - {formatMessage(`Terms of Use`)} - -
-
- - - {formatMessage(`Privacy`)} - -
-
); diff --git a/Composer/packages/client/src/pages/about/styles.js b/Composer/packages/client/src/pages/about/styles.js index 5b177bc492..d2849708e9 100644 --- a/Composer/packages/client/src/pages/about/styles.js +++ b/Composer/packages/client/src/pages/about/styles.js @@ -3,28 +3,11 @@ import { css } from '@emotion/core'; import { FontWeights, FontSizes } from 'office-ui-fabric-react/lib/Styling'; -export const outline = css` - display: flex; - flex-direction: column; - height: 100%; - margin: 32px 50px 0px 32px; - border: 1px solid #979797; - overflow-x: auto; -`; export const content = css` height: 100%; `; -export const title = css` - display: block; - height: 36px; - margin: 33px 0px 0px 42px; - font-size: ${FontSizes.xLarge}; - font-weight: ${FontWeights.semibold}; - line-height: 32px; -`; - export const body = css` width: auto; margin-top: 26px; @@ -32,43 +15,41 @@ export const body = css` `; export const version = css` - font-size: ${FontSizes.large}; + font-size: ${FontSizes.medium}; font-weight: ${FontWeights.regular}; line-height: 32px; `; -export const description = css` - font-size: ${FontSizes.mediumPlus}; - font-weight: ${FontWeights.regular}; - line-height: 32px; - width: 50%; - margin-top: 20px; -`; - -export const DiagnosticsText = css` +export const diagnosticsText = css` width: 50%; font-size: 24px; margin-top: 20px; `; export const smallText = css` - margin-top: 20px; - font-size: 13px; + margin: 20px 20px 20px 0; + font-size: 14px; +`; + +export const smallerText = css` + margin: 20px 20px 20px 0; + font-size: 12px; `; -export const DiagnosticsInfoText = css` + +export const diagnosticsInfoText = css` display: flex; justify-content: space-between; width: 550px; font-size: 24px; `; -export const DiagnosticsInfoTextAlignLeft = css` +export const diagnosticsInfoTextAlignLeft = css` text-align: left; font-size: ${FontSizes.mediumPlus}; font-weight: ${FontWeights.semibold}; `; -export const DiagnosticsInfo = css` +export const diagnosticsInfo = css` margin-top: 40px; `; diff --git a/Composer/packages/client/src/pages/botProject/adapters/ABSChannels.tsx b/Composer/packages/client/src/pages/botProject/adapters/ABSChannels.tsx index c70f536a27..6a8c47d1ca 100644 --- a/Composer/packages/client/src/pages/botProject/adapters/ABSChannels.tsx +++ b/Composer/packages/client/src/pages/botProject/adapters/ABSChannels.tsx @@ -139,8 +139,8 @@ export const ABSChannels: React.FC = (props) => { const config = JSON.parse(profile.configuration); setCurrentResource({ microsoftAppId: config?.settings?.MicrosoftAppId, - resourceName: config.name, - resourceGroupName: config.resourceGroup || config.name, + resourceName: config.botName || config.name, + resourceGroupName: config.resourceGroup || config.botName || config.name, subscriptionId: config.subscriptionId, }); } diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/DiagnosticsTab/DiagnosticsTabHeader.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/DiagnosticsTab/DiagnosticsTabHeader.tsx index 3415f3d786..e75c51ed1e 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/DiagnosticsTab/DiagnosticsTabHeader.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/DiagnosticsTab/DiagnosticsTabHeader.tsx @@ -5,10 +5,12 @@ import { jsx, css } from '@emotion/core'; import formatMessage from 'format-message'; +import { useAutoFix } from './useAutoFix'; import { useDiagnosticsStatistics } from './useDiagnostics'; export const DiagnosticsHeader = () => { const { hasError, hasWarning } = useDiagnosticsStatistics(); + useAutoFix(); return (
{ + const diagnostics = useDiagnosticsData(); + const botProjectSpace = useRecoilValue(botProjectSpaceSelector); + const { updateDialog } = useRecoilValue(dispatcherState); + + // Auto fix schema absence by setting 'disabled' to true. + useEffect(() => { + const schemaDiagnostics = diagnostics.filter((d) => d.type === DiagnosticType.SCHEMA) as SchemaDiagnostic[]; + /** + * Aggregated diagnostic paths where contains schema problem + * + * Example: + * { + * // projectId + * '2096.637': { + * // dialogId + * 'dialog-1': [ + * 'triggers[0].actions[1]', // diagnostics.dialogPath + * 'triggers[2].actions[3]' + * ] + * } + * } + */ + const aggregatedPaths: { [projectId: string]: { [dialogId: string]: string[] } } = {}; + + // Aggregates schema diagnostics by projectId, dialogId + schemaDiagnostics.forEach((d) => { + const { projectId, id: dialogId, dialogPath } = d; + if (!dialogPath) return; + const currentPaths = get(aggregatedPaths, [projectId, dialogId]); + if (currentPaths) { + currentPaths.push(dialogPath); + } else { + set(aggregatedPaths, [projectId, dialogId], [dialogPath]); + } + }); + + // Consumes aggregatedPaths to update dialogs in recoil store + for (const [projectId, pathsByDialogId] of Object.entries(aggregatedPaths)) { + // Locates dialogs in current project + const dialogsInProject = botProjectSpace.find((bot) => bot.projectId === projectId)?.dialogs; + if (!Array.isArray(dialogsInProject)) continue; + + for (const [dialogId, paths] of Object.entries(pathsByDialogId)) { + // Queries out current dialog data + const dialogData = dialogsInProject.find((dialog) => dialog.id === dialogId)?.content; + if (!dialogData) continue; + + // Filters out those paths where action exists and action.disabled !== true + const pathsToUpdate = paths.filter((p) => { + const data = get(dialogData, p); + return data && !get(data, 'disabled'); + }); + if (!pathsToUpdate.length) continue; + + // Manipulates the 'disabled' property and then submit to Recoil store. + const copy = cloneDeep(dialogData); + for (const p of pathsToUpdate) { + set(copy, `${p}.disabled`, true); + } + updateDialog({ id: dialogId, projectId, content: copy }); + } + } + }, [diagnostics]); +}; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WebChatLog/WebChatLogContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WebChatLog/WebChatLogContent.tsx index b4d7e27aec..78c514cd09 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WebChatLog/WebChatLogContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WebChatLog/WebChatLogContent.tsx @@ -8,24 +8,27 @@ import { useRecoilValue } from 'recoil'; import { ConversationTrafficItem } from '@botframework-composer/types/src'; import formatMessage from 'format-message'; import debounce from 'lodash/debounce'; +import { ActionButton } from 'office-ui-fabric-react/lib/Button'; +import { FontWeights } from 'office-ui-fabric-react/lib/Styling'; +import { SharedColors } from '@uifabric/fluent-theme'; import { dispatcherState, rootBotProjectIdSelector, webChatTrafficState, webChatInspectionDataState, + botStatusState, } from '../../../../../recoilModel'; import { DebugPanelTabHeaderProps } from '../types'; import { WebChatInspectionData } from '../../../../../recoilModel/types'; +import { BotStatus } from '../../../../../constants'; +import { useBotOperations } from '../../../../../components/BotRuntimeController/useBotOperations'; +import { usePVACheck } from '../../../../../hooks/usePVACheck'; import { WebChatInspectorPane } from './WebChatInspectorPane'; import { WebChatActivityLogItem } from './WebChatActivityLogItem'; import { WebChatNetworkLogItem } from './WebChatNetworkLogItem'; -const emptyStateMessage = css` - padding-left: 16px; -`; - const logContainer = (isActive: boolean) => css` height: 100%; display: ${!isActive ? 'none' : 'flex'}; @@ -33,16 +36,30 @@ const logContainer = (isActive: boolean) => css` flex-direction: row; `; -const logPane = css` +const logPane = (trafficLength: number) => css` height: 100%; width: 100%; display: flex; overflow: auto; flex-direction: column; - padding: 16px 0; + padding: ${trafficLength ? '16px 0' : '4px 0'}; box-sizing: border-box; `; +const emptyStateMessageContainer = css` + padding: 0px 16px; + font-size: 12px; +`; + +const actionButton = { + root: { + fontSize: 12, + fontWeight: FontWeights.regular, + color: SharedColors.cyanBlue10, + paddingLeft: 0, + }, +}; + const itemIsSelected = (item: ConversationTrafficItem, currentInspectionData?: WebChatInspectionData) => { return item.id === currentInspectionData?.item?.id; }; @@ -55,7 +72,10 @@ export const WebChatLogContent: React.FC = ({ isActive const [navigateToLatestEntry, navigateToLatestEntryWhenActive] = useState(false); const [currentLogItemCount, setLogItemCount] = useState(0); const webChatContainerRef = useRef(null); - const { setWebChatInspectionData } = useRecoilValue(dispatcherState); + const { setWebChatInspectionData, setWebChatPanelVisibility } = useRecoilValue(dispatcherState); + const currentStatus = useRecoilValue(botStatusState(currentProjectId ?? '')); + const { startAllBots } = useBotOperations(); + const isPVABot = usePVACheck(currentProjectId ?? ''); const navigateToNewestLogEntry = () => { if (currentLogItemCount && webChatContainerRef?.current) { @@ -160,14 +180,49 @@ export const WebChatLogContent: React.FC = ({ isActive } }; + const onOpenWebChatPanelClick = () => { + setWebChatPanelVisibility(true); + }; + + const noWebChatTrafficSection = useMemo(() => { + if (isPVABot) { + return null; + } + + if (currentStatus === BotStatus.inactive) { + return ( +
+ {formatMessage.rich('Your bot project is not running. Start your bot', { + actionButton: ({ children }) => ( + + {children} + + ), + })} +
+ ); + } + + if (currentStatus === BotStatus.connected) { + return ( +
+ {formatMessage.rich('Your bot project is running. Test in Web Chat', { + actionButton: ({ children }) => ( + + {children} + + ), + })} +
+ ); + } + return null; + }, [currentStatus]); + return (
-
- {displayedTraffic.length ? ( - displayedTraffic - ) : ( - {formatMessage('No Web Chat activity yet.')} - )} +
+ {displayedTraffic.length ? displayedTraffic : noWebChatTrafficSection}
diff --git a/Composer/packages/client/src/pages/diagnostics/types.ts b/Composer/packages/client/src/pages/diagnostics/types.ts index 2f615ac6f1..7f1227b949 100644 --- a/Composer/packages/client/src/pages/diagnostics/types.ts +++ b/Composer/packages/client/src/pages/diagnostics/types.ts @@ -17,6 +17,7 @@ export enum DiagnosticType { SKILL, SETTING, GENERAL, + SCHEMA, } export interface IDiagnosticInfo { @@ -94,6 +95,10 @@ export class DialogDiagnostic extends DiagnosticInfo { }; } +export class SchemaDiagnostic extends DialogDiagnostic { + type = DiagnosticType.SCHEMA; +} + export class SkillSettingDiagnostic extends DiagnosticInfo { type = DiagnosticType.SKILL; constructor(rootProjectId: string, projectId: string, id: string, location: string, diagnostic: Diagnostic) { diff --git a/Composer/packages/client/src/pages/setting/SettingsPage.tsx b/Composer/packages/client/src/pages/setting/SettingsPage.tsx index cf6ac83a79..6dbf09f2ec 100644 --- a/Composer/packages/client/src/pages/setting/SettingsPage.tsx +++ b/Composer/packages/client/src/pages/setting/SettingsPage.tsx @@ -106,12 +106,6 @@ const SettingPage: React.FC = () => { return settingLabels.appSettings; }, [location.pathname]); - const onRenderHeaderContent = () => { - return formatMessage( - 'This Page contains detailed information about your bot. For security reasons, they are hidden by default. To test your bot or publish to Azure, you may need to provide these settings' - ); - }; - return ( = () => { pageMode={'settings'} title={title} toolbarItems={[]} - onRenderHeaderContent={onRenderHeaderContent} > ({ get }) => { + const botAssets = get(botAssetsSelectFamily(projectId)); + if (botAssets === null) return []; + + const rootProjectId = get(rootBotProjectIdSelector) ?? projectId; + + /** + * `botAssets.dialogSchema` contains all *.schema files loaded by project indexer. However, it actually messes up sdk.schema and *.dialog.schema. + * To get the correct sdk.schema content, current workaround is to filter schema by id. + * + * TODO: To fix it entirely, we need to differentiate dialog.schema from sdk.schema in indexer. + */ + const sdkSchemaContent = botAssets.dialogSchemas.find((d) => d.id === '')?.content; + if (!sdkSchemaContent) return []; + + const fullDiagnostics: DiagnosticInfo[] = []; + botAssets.dialogs.forEach((dialog) => { + const diagnostics = validateSchema(dialog.id, dialog.content, sdkSchemaContent); + fullDiagnostics.push( + ...diagnostics.map((d) => new SchemaDiagnostic(rootProjectId, projectId, dialog.id, `${dialog.id}.dialog`, d)) + ); + }); + return fullDiagnostics; }, }); @@ -257,6 +286,7 @@ export const diagnosticsSelectorFamily = selectorFamily({ ...get(luDiagnosticsSelectorFamily(projectId)), ...get(lgDiagnosticsSelectorFamily(projectId)), ...get(qnaDiagnosticsSelectorFamily(projectId)), + ...get(schemaDiagnosticsSelectorFamily(projectId)), ], }); diff --git a/Composer/packages/client/src/utils/zIndices.ts b/Composer/packages/client/src/utils/zIndices.ts new file mode 100644 index 0000000000..3e9bb6e7fa --- /dev/null +++ b/Composer/packages/client/src/utils/zIndices.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ZIndexes } from 'office-ui-fabric-react/lib/Styling'; + +/** + * This object keeps track of zIndices use in the app. + * This will help prevent zIndices competing with each other. + * Add your z-index value here and use it in the component. + */ +export const zIndices = { + notificationContainer: ZIndexes.Layer + 1, +}; diff --git a/Composer/packages/electron-server/src/preload.js b/Composer/packages/electron-server/src/preload.js index ff5c0bc7bb..f1ea8ec4f5 100644 --- a/Composer/packages/electron-server/src/preload.js +++ b/Composer/packages/electron-server/src/preload.js @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -const { ipcRenderer } = require('electron'); // eslint-disable-line +const { app, ipcRenderer } = require('electron'); // eslint-disable-line // expose ipcRenderer to the browser window.ipcRenderer = ipcRenderer; +// get the app version to hand into the client +window.appVersion = app.getVersion(); // flag to distinguish electron client from web app client window.__IS_ELECTRON__ = true; diff --git a/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/dialogMocks.ts b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/dialogMocks.ts new file mode 100644 index 0000000000..d3c7c1a8df --- /dev/null +++ b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/dialogMocks.ts @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export const sendActivityStub = { + $kind: 'Microsoft.SendActivity', + $designer: { + id: 'AwT1u7', + }, +}; + +export const switchConditionStub = { + $kind: 'Microsoft.SwitchCondition', + $designer: { + id: 'sJzdQm', + }, + default: [sendActivityStub], + cases: [ + { + value: 'case1', + actions: [sendActivityStub], + }, + ], +}; + +export const onConversationUpdateActivityStub = { + $kind: 'Microsoft.OnConversationUpdateActivity', + $designer: { + id: '376720', + }, + actions: [switchConditionStub], +}; + +export const simpleGreetingDialog: any = { + $kind: 'Microsoft.AdaptiveDialog', + $designer: { + $designer: { + name: 'EmptyBot-1', + description: '', + id: '47yxe0', + }, + }, + autoEndDialog: true, + defaultResultProperty: 'dialog.result', + triggers: [onConversationUpdateActivityStub], + $schema: + 'https://raw.githubusercontent.com/microsoft/BotFramework-Composer/stable/Composer/packages/server/schemas/sdk.schema', + generator: 'EmptyBot-1.lg', + id: 'EmptyBot-1', + recognizer: 'EmptyBot-1.lu.qna', +}; diff --git a/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/sdkSchema.ts b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/sdkSchema.ts new file mode 100644 index 0000000000..de22165971 --- /dev/null +++ b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/sdkSchema.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { SDKKinds } from '@botframework-composer/types'; + +import { + AdaptiveDialogSchema, + IfConditionSchema, + OnConvUpdateSchema, + OnDialogEventSchema, + SwitchConditionSchema, +} from './sdkSchemaMocks'; + +export const sdkSchemaDefinitionMock = { + [SDKKinds.SwitchCondition]: SwitchConditionSchema, + [SDKKinds.IfCondition]: IfConditionSchema, + [SDKKinds.AdaptiveDialog]: AdaptiveDialogSchema, + [SDKKinds.OnDialogEvent]: OnDialogEventSchema, + [SDKKinds.OnConversationUpdateActivity]: OnConvUpdateSchema, +}; diff --git a/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/sdkSchemaMocks.ts b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/sdkSchemaMocks.ts new file mode 100644 index 0000000000..e13b92d786 --- /dev/null +++ b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/__mocks__/sdkSchemaMocks.ts @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { JSONSchema7 } from '@botframework-composer/types'; + +export const AdaptiveDialogSchema: JSONSchema7 = { + $schema: 'https://schemas.botframework.com/schemas/component/v1.0/component.schema', + $role: 'implements(Microsoft.IDialog)', + title: 'Adaptive dialog', + description: 'Flexible, data driven dialog that can adapt to the conversation.', + type: 'object', + properties: { + id: { + type: 'string', + pattern: '^(?!(=)).*', + title: 'Id', + description: 'Optional dialog ID.', + }, + autoEndDialog: { + $ref: 'schema:#/definitions/booleanExpression', + title: 'Auto end dialog', + description: + 'If set to true the dialog will automatically end when there are no further actions. If set to false, remember to manually end the dialog using EndDialog action.', + default: true, + }, + defaultResultProperty: { + type: 'string', + title: 'Default result property', + description: 'Value that will be passed back to the parent dialog.', + default: 'dialog.result', + }, + dialogs: { + type: 'array', + title: 'Dialogs added to DialogSet', + items: { + $kind: 'Microsoft.IDialog', + title: 'Dialog', + description: 'Dialogs will be added to DialogSet.', + }, + }, + recognizer: { + $kind: 'Microsoft.IRecognizer', + title: 'Recognizer', + description: 'Input recognizer that interprets user input into intent and entities.', + }, + generator: { + $kind: 'Microsoft.ILanguageGenerator', + title: 'Language generator', + description: 'Language generator that generates bot responses.', + }, + selector: { + $kind: 'Microsoft.ITriggerSelector', + title: 'Selector', + description: "Policy to determine which trigger is executed. Defaults to a 'best match' selector (optional).", + }, + triggers: { + type: 'array', + description: 'List of triggers defined for this dialog.', + title: 'Triggers', + items: { + $kind: 'Microsoft.ITrigger', + title: 'Event triggers', + description: 'Event triggers for handling events.', + }, + }, + schema: { + title: 'Schema', + description: 'Schema to fill in.', + anyOf: [ + { + $ref: 'http://json-schema.org/draft-07/schema#', + }, + { + type: 'string', + title: 'Reference to JSON schema', + description: 'Reference to JSON schema .dialog file.', + }, + ], + }, + }, +}; + +export const IfConditionSchema: JSONSchema7 = { + $schema: 'https://schemas.botframework.com/schemas/component/v1.0/component.schema', + $role: 'implements(Microsoft.IDialog)', + title: 'If condition', + description: 'Two-way branch the conversation flow based on a condition.', + type: 'object', + required: ['condition'], + properties: { + id: { + type: 'string', + title: 'Id', + description: 'Optional id for the dialog', + }, + condition: { + $ref: 'schema:#/definitions/condition', + title: 'Condition', + description: 'Expression to evaluate.', + examples: ['user.age > 3'], + }, + disabled: { + $ref: 'schema:#/definitions/booleanExpression', + title: 'Disabled', + description: 'Optional condition which if true will disable this action.', + examples: [true, '=user.age > 3'], + }, + actions: { + type: 'array', + items: { + $kind: 'Microsoft.IDialog', + }, + title: 'Actions', + description: 'Actions to execute if condition is true.', + }, + elseActions: { + type: 'array', + items: { + $kind: 'Microsoft.IDialog', + }, + title: 'Else', + description: 'Actions to execute if condition is false.', + }, + }, +}; + +export const OnConvUpdateSchema: JSONSchema7 = { + $schema: 'https://schemas.botframework.com/schemas/component/v1.0/component.schema', + $role: ['implements(Microsoft.ITrigger)', 'extends(Microsoft.OnCondition)'], + title: 'On ConversationUpdate activity', + description: "Actions to perform on receipt of an activity with type 'ConversationUpdate'.", + type: 'object', + required: [], +}; +export const OnDialogEventSchema: JSONSchema7 = { + $schema: 'https://schemas.botframework.com/schemas/component/v1.0/component.schema', + $role: ['implements(Microsoft.ITrigger)', 'extends(Microsoft.OnCondition)'], + title: 'On dialog event', + description: 'Actions to perform when a specific dialog event occurs.', + type: 'object', + properties: { + event: { + type: 'string', + title: 'Dialog event name', + description: 'Name of dialog event.', + }, + }, + required: ['event'], +}; + +export const SwitchConditionSchema: JSONSchema7 = { + $schema: 'https://schemas.botframework.com/schemas/component/v1.0/component.schema', + $role: 'implements(Microsoft.IDialog)', + title: 'Switch condition', + description: 'Execute different actions based on the value of a property.', + type: 'object', + required: ['condition'], + properties: { + id: { + type: 'string', + title: 'Id', + description: 'Optional id for the dialog', + }, + condition: { + $ref: 'schema:#/definitions/stringExpression', + title: 'Condition', + description: 'Property to evaluate.', + examples: ['user.favColor'], + }, + disabled: { + $ref: 'schema:#/definitions/booleanExpression', + title: 'Disabled', + description: 'Optional condition which if true will disable this action.', + examples: [true, '=user.age > 3'], + }, + cases: { + type: 'array', + title: 'Cases', + description: 'Actions for each possible condition.', + items: { + type: 'object', + title: 'Case', + description: 'Case and actions.', + properties: { + value: { + type: ['number', 'integer', 'boolean', 'string'], + title: 'Value', + description: 'The value to compare the condition with.', + examples: ['red', 'true', '13'], + }, + actions: { + type: 'array', + items: { + $kind: 'Microsoft.IDialog', + }, + title: 'Actions', + description: 'Actions to execute.', + }, + }, + required: ['value'], + }, + }, + default: { + type: 'array', + items: { + $kind: 'Microsoft.IDialog', + }, + title: 'Default', + description: 'Actions to execute if none of the cases meet the condition.', + }, + }, +}; + +export const SetPropertiesSchema: JSONSchema7 = { + "$schema": "https://schemas.botframework.com/schemas/component/v1.0/component.schema", + "$role": "implements(Microsoft.IDialog)", + "title": "Set property", + "description": "Set one or more property values.", + "type": "object", + "required": [ + "assignments" + ], + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Optional id for the dialog" + }, + "disabled": { + "$ref": "schema:#/definitions/booleanExpression", + "title": "Disabled", + "description": "Optional condition which if true will disable this action.", + "examples": [ + true, + "=user.age > 3" + ] + }, + "assignments": { + "type": "array", + "title": "Assignments", + "description": "Property value assignments to set.", + "items": { + "type": "object", + "title": "Assignment", + "description": "Property assignment.", + "properties": { + "property": { + "$ref": "schema:#/definitions/stringExpression", + "title": "Property", + "description": "Property (named location to store information).", + "examples": [ + "user.age" + ] + }, + "value": { + "$ref": "schema:#/definitions/valueExpression", + "title": "Value", + "description": "New value or expression.", + "examples": [ + "='milk'", + "=dialog.favColor", + "=dialog.favColor == 'red'" + ] + } + } + } + } + } +} diff --git a/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/schemaUtils.test.ts b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/schemaUtils.test.ts new file mode 100644 index 0000000000..8020e3eaa1 --- /dev/null +++ b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/schemaUtils.test.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { discoverNestedPaths, isTrigger } from '../../../src/validations/schemaValidation/schemaUtils'; + +import { onConversationUpdateActivityStub, simpleGreetingDialog, switchConditionStub } from './__mocks__/dialogMocks'; +import { + AdaptiveDialogSchema, + IfConditionSchema, + OnConvUpdateSchema, + OnDialogEventSchema, + SetPropertiesSchema, + SwitchConditionSchema, +} from './__mocks__/sdkSchemaMocks'; + +describe('#schemaUtils', () => { + it('isTrigger() should recognizer trigger schema.', () => { + expect(isTrigger(OnDialogEventSchema)).toBeTruthy(); + expect(isTrigger(IfConditionSchema)).toBeFalsy(); + expect(isTrigger(AdaptiveDialogSchema)).toBeFalsy(); + }); + + it('discoverNestedProperties() should find correct property names.', () => { + expect(discoverNestedPaths(simpleGreetingDialog, AdaptiveDialogSchema)).toEqual( + expect.arrayContaining(['triggers']) + ); + expect(discoverNestedPaths(onConversationUpdateActivityStub, OnConvUpdateSchema)).toEqual( + expect.arrayContaining(['actions']) + ); + expect(discoverNestedPaths(switchConditionStub, SwitchConditionSchema)).toEqual( + expect.arrayContaining(['cases[0].actions', 'default']) + ); + }); + + it('disconverNestedProperties() should defense invalid input.', () => { + const setPropertiesStub = { + $kind: 'Microsoft.SetProperties', + $designer: { + id: 'sJzdQm', + }, + assignments: [ + { property: 'username', value: 'test' } + ] + }; + + expect(discoverNestedPaths(setPropertiesStub, SetPropertiesSchema)).toEqual([]); + }) +}); diff --git a/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/walkAdaptiveDialog.test.ts b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/walkAdaptiveDialog.test.ts new file mode 100644 index 0000000000..9ecb75e4f9 --- /dev/null +++ b/Composer/packages/lib/indexers/__tests__/validations/schemaValidation/walkAdaptiveDialog.test.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { SDKKinds } from '@botframework-composer/types'; + +import { walkAdaptiveDialog } from '../../../src/validations/schemaValidation/walkAdaptiveDialog'; + +import { simpleGreetingDialog } from './__mocks__/dialogMocks'; +import { sdkSchemaDefinitionMock } from './__mocks__/sdkSchema'; + +describe('visitAdaptiveDialog', () => { + it('should visit every adaptive elements in `simpleGreeting`', () => { + const result: any = {}; + walkAdaptiveDialog(simpleGreetingDialog, sdkSchemaDefinitionMock, ($kind, _, path) => { + result[path] = $kind; + return true; + }); + expect(result).toEqual( + expect.objectContaining({ + '': SDKKinds.AdaptiveDialog, + 'triggers[0]': SDKKinds.OnConversationUpdateActivity, + 'triggers[0].actions[0]': SDKKinds.SwitchCondition, + 'triggers[0].actions[0].default[0]': SDKKinds.SendActivity, + 'triggers[0].actions[0].cases[0].actions[0]': SDKKinds.SendActivity, + }) + ); + }); +}); diff --git a/Composer/packages/lib/indexers/src/validations/index.ts b/Composer/packages/lib/indexers/src/validations/index.ts index d88ead354e..cc4b6a7919 100644 --- a/Composer/packages/lib/indexers/src/validations/index.ts +++ b/Composer/packages/lib/indexers/src/validations/index.ts @@ -69,3 +69,5 @@ export function validateDialog( return { diagnostics: [new Diagnostic(error.message, id)], cache }; } } + +export { validateSchema } from './schemaValidation'; diff --git a/Composer/packages/lib/indexers/src/validations/schemaValidation/index.ts b/Composer/packages/lib/indexers/src/validations/schemaValidation/index.ts new file mode 100644 index 0000000000..2bf294856b --- /dev/null +++ b/Composer/packages/lib/indexers/src/validations/schemaValidation/index.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import { Diagnostic } from '@bfc/shared'; +import formatMessage from 'format-message'; +import { BaseSchema, DiagnosticSeverity, SchemaDefinitions } from '@botframework-composer/types'; + +import { walkAdaptiveDialog } from './walkAdaptiveDialog'; + +const SCHEMA_NOT_FOUND = formatMessage('Schema definition not found in sdk.schema.'); + +export const validateSchema = (dialogId: string, dialogData: BaseSchema, schema: SchemaDefinitions): Diagnostic[] => { + const diagnostics: Diagnostic[] = []; + const schemas: any = schema.definitions ?? {}; + + walkAdaptiveDialog(dialogData, schemas, ($kind, data, path) => { + if (!schemas[$kind]) { + diagnostics.push( + new Diagnostic(`${$kind}: ${SCHEMA_NOT_FOUND}`, `${dialogId}.dialog`, DiagnosticSeverity.Warning, path) + ); + } + return true; + }); + + return diagnostics; +}; diff --git a/Composer/packages/lib/indexers/src/validations/schemaValidation/schemaUtils.ts b/Composer/packages/lib/indexers/src/validations/schemaValidation/schemaUtils.ts new file mode 100644 index 0000000000..2572566bc6 --- /dev/null +++ b/Composer/packages/lib/indexers/src/validations/schemaValidation/schemaUtils.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { BaseSchema, JSONSchema7 } from '@botframework-composer/types'; +import get from 'lodash/get'; + +export const isTrigger = (schema: JSONSchema7): boolean => { + const roles = typeof schema.$role === 'string' ? [schema.$role] : schema.$role ?? []; + + return roles.some((roleString) => { + return roleString.indexOf('implements(Microsoft.ITrigger)') > -1; + }); +}; + +const triggerNesterProperties = ['actions']; + +const propertyDefinesActionArray = (propertyDefinition: JSONSchema7): boolean => { + if (!propertyDefinition) { + return false; + } + const { type, items } = propertyDefinition; + return type === 'array' && Boolean(get(items, '$kind')); +}; + +const discoverNestedSchemaPaths = (data: BaseSchema, schema: JSONSchema7): string[] => { + if (isTrigger(schema)) return triggerNesterProperties; + if (!schema.properties) return []; + + const nestedPaths: string[] = []; + + const entries = Object.entries(schema.properties); + for (const entry of entries) { + const [propertyName, propertyDef] = entry; + /** + * Discover child elements (triggers, actions). For example: + * 1. In Microsoft.IfCondition.schema + * ```json + * properties.actions = { + * "type": "array", + * "items": { + * "$kind": "Microsoft.IDialog" + * }, + * "title": "Actions", + * "description": "Actions to execute if condition is true." + * } + * ``` + * Returns ["actions"]. + * + * 2. In Microsoft.AdaptiveDialog.schema + * ```json + * properties.triggers = { + * "type": "array", + * "description": "List of triggers defined for this dialog.", + * "title": "Triggers", + * "items": { + * "$kind": "Microsoft.ITrigger", + * "title": "Event triggers", + * "description": "Event triggers for handling events." + * } + * } + * ``` + * Returns ["triggers"]. + */ + const propertyData = get(data, propertyName); + if (!Array.isArray(propertyData) || !propertyData.length) continue; + + const isSchemaNested = propertyDefinesActionArray(propertyDef); + const dataContainsAction = Boolean(propertyData[0].$kind); + + if (isSchemaNested && dataContainsAction) { + nestedPaths.push(propertyName); + continue; + } + + /** + * Discover skip-level child elements. + * Currently, this logic can only handle skip-level child actions under the 'actions' field. + * To discover all possible actions under arbitrary levels / field names, needs to traverse the schema tree. + * + * Example: (Reference to SwitchCondition.schema: https://github.com/microsoft/botbuilder-dotnet/blob/main/libraries/Microsoft.Bot.Builder.Dialogs.Adaptive/Schemas/Actions/Microsoft.SwitchCondition.schema) + * properties.cases.items.properties = { + * "value": { ... }, + * "actions": { // Discover this property + * "type": "array", + * "items": { + * "$kind": "Microsoft.IDialog" + * }, + * "title": "Actions", + * "description": "Actions to execute." + * } + * } + */ + const actionsDefUnderItems = get(propertyDef, 'items.properties.actions'); + const schemaHasSkipLevelActions = + propertyDef?.type === 'array' + && Boolean(actionsDefUnderItems) + && propertyDefinesActionArray(actionsDefUnderItems); + + if (schemaHasSkipLevelActions) { + propertyData.forEach((caseData, caseIndex) => { + const caseActions = caseData.actions; + if (!Array.isArray(caseActions) || !caseActions.length) return; + + for (let i = 0; i < caseActions.length; i++) { + nestedPaths.push(`${propertyName}[${caseIndex}].actions`); + } + }); + } + } + + return nestedPaths; +}; + +export const discoverNestedPaths = (data: BaseSchema, schema: JSONSchema7): string[] => { + try { + return discoverNestedSchemaPaths(data, schema); + } catch (e) { + // Met potential schema visit bugs + return []; + } +} \ No newline at end of file diff --git a/Composer/packages/lib/indexers/src/validations/schemaValidation/walkAdaptiveDialog.ts b/Composer/packages/lib/indexers/src/validations/schemaValidation/walkAdaptiveDialog.ts new file mode 100644 index 0000000000..1b7d89e19f --- /dev/null +++ b/Composer/packages/lib/indexers/src/validations/schemaValidation/walkAdaptiveDialog.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { BaseSchema, SchemaDefinitions, SDKKinds } from '@botframework-composer/types'; +import get from 'lodash/get'; + +import { discoverNestedPaths } from './schemaUtils'; + +// returns true to continue the visit. +type VisitAdaptiveComponentFn = ( + $kind: SDKKinds | string, + data: BaseSchema, + currentPath: string, + parentPath: string +) => boolean; + +export const walkAdaptiveDialog = ( + adaptiveDialog: BaseSchema, + sdkSchema: SchemaDefinitions, + fn: VisitAdaptiveComponentFn +): boolean => { + return walkWithPath(adaptiveDialog, sdkSchema, '', '', fn); +}; + +const joinPath = (parentPath: string, currentKey: string | number): string => { + if (typeof currentKey === 'string') { + return parentPath ? `${parentPath}.${currentKey}` : currentKey; + } + + if (typeof currentKey === 'number') { + return parentPath ? `${parentPath}[${currentKey}]` : `[${currentKey}]`; + } + + return ''; +}; + +const walkWithPath = ( + adaptiveData: BaseSchema, + sdkSchema: SchemaDefinitions, + currentPath: string, + parentPath: string, + fn: VisitAdaptiveComponentFn +): boolean => { + const { $kind } = adaptiveData; + // Visit current data before schema validation to make sure all $kind blocks are visited. + fn($kind, adaptiveData, currentPath, parentPath); + + const schema = sdkSchema[$kind]; + const nestedPaths = schema ? discoverNestedPaths(adaptiveData, schema) : adaptiveData.actions ? ['actions'] : []; + if (nestedPaths.length === 0) return true; + + /** + * Examples of nested properties in built-in $kinds: + * 1. ['actions'] in every Trigger $kind + * 2. ['actions'] in Foreach, ForeachPage + * 3. ['actions', 'elseActions'] in IfCondition + * 4. ['cases', 'default'] in SwitchCondition + */ + for (const path of nestedPaths) { + const childElements = get(adaptiveData, path); + if (!Array.isArray(childElements)) { + continue; + } + + /** + * Visit nested adaptive elements. For example: + * 1. Triggers under Dialog; + * 2. Actions under Trigger; + * 3. Actions under Action. + */ + for (let i = 0; i < childElements.length; i++) { + const shouldContinue = walkWithPath( + childElements[i], + sdkSchema, + joinPath(currentPath, `${path}[${i}]`), + currentPath, + fn + ); + if (!shouldContinue) return false; + } + } + + return true; +}; diff --git a/Composer/packages/server/src/locales/en-US.json b/Composer/packages/server/src/locales/en-US.json index 2c9c028f8c..3e8ebb4f55 100644 --- a/Composer/packages/server/src/locales/en-US.json +++ b/Composer/packages/server/src/locales/en-US.json @@ -494,18 +494,12 @@ "bot_files_created_986109df": { "message": "Bot files created" }, - "bot_framework_composer_enables_developers_and_mult_ce0e42a9": { - "message": "Bot Framework Composer enables developers and multi-disciplinary teams to build all kinds of conversational experiences, using the latest components from the Bot Framework: SDK, LG, LU, and declarative file formats, all without writing code." - }, "bot_framework_composer_fae721be": { "message": "Bot Framework Composer" }, "bot_framework_composer_icon_gray_fa72d3d6": { "message": "bot framework composer icon gray" }, - "bot_framework_composer_is_a_visual_authoring_canva_c3947d91": { - "message": "Bot Framework Composer is a visual authoring canvas for building bots and other types of conversational application with the Microsoft Bot Framework technology stack. With Composer you will find everything you need to build a modern, state-of-the-art conversational experience." - }, "bot_framework_composer_requires_node_js_in_order_t_9c45c226": { "message": "Bot Framework Composer requires Node.js in order to create and run a new bot. Click “Install Node.js” to install the latest version" }, @@ -2690,9 +2684,15 @@ "other_1c6d9c79": { "message": "Other" }, + "our_privacy_statement_is_located_at_a_https_go_mic_56534925": { + "message": "Our privacy statement is located at https://go.microsoft.com/fwlink/?LinkID=824704. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices." + }, "output_5023cf84": { "message": "Output" }, + "p_copyright_c_microsoft_corporation_p_p_mit_licens_cd145fd6": { + "message": "

Copyright (c) Microsoft Corporation.

MIT License

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

" + }, "page_number_cdee4179": { "message": "Page number" }, @@ -3740,9 +3740,6 @@ "this_page_contains_detailed_information_about_your_224a2b04": { "message": "This Page contains detailed information about your bot. For security reasons, they are hidden by default. To test your bot or publish to Azure, you may need to provide these settings." }, - "this_page_contains_detailed_information_about_your_b2b3413b": { - "message": "This Page contains detailed information about your bot. For security reasons, they are hidden by default. To test your bot or publish to Azure, you may need to provide these settings" - }, "this_publishing_profile_profilename_is_no_longer_s_eee0f447": { "message": "This publishing profile ({ profileName }) is no longer supported. You are a member of multiple Azure tenants and the profile needs to have a tenant id associated with it. You can either edit the profile by adding the `tenantId` property to it''s configuration or create a new one." }, diff --git a/extensions/azurePublish/src/components/azureProvisionDialog.tsx b/extensions/azurePublish/src/components/azureProvisionDialog.tsx index 6c1cc9c5ff..b4a5a76e53 100644 --- a/extensions/azurePublish/src/components/azureProvisionDialog.tsx +++ b/extensions/azurePublish/src/components/azureProvisionDialog.tsx @@ -171,6 +171,17 @@ const onRenderLabel = (props) => { ); }; +const getResourceRegion = (item: ResourcesItem): string => { + const { key, region } = item; + switch (key) { + case AzureResourceTypes.APP_REGISTRATION: + case AzureResourceTypes.BOT_REGISTRATION: + return 'global'; + default: + return region; + } +}; + const reviewCols: IColumn[] = [ { key: 'Icon', @@ -235,7 +246,7 @@ const reviewCols: IColumn[] = [ onRender: (item: ResourcesItem) => { return (
- {item.key === AzureResourceTypes.APP_REGISTRATION ? 'global' : item?.region} + {getResourceRegion(item)}
); }, @@ -284,6 +295,8 @@ export const AzureProvisionDialog: React.FC = () => { } = usePublishApi(); const telemetryClient: TelemetryClient = useTelemetryClient(); + console.log('TELEMETRY CLIENT', telemetryClient); + const { setItem, getItem, clearAll } = useLocalStorage(); // set type of publish - azurePublish or azureFunctionsPublish const publishType = getType(); @@ -362,22 +375,22 @@ export const AzureProvisionDialog: React.FC = () => { setPage(page); switch (page) { case PageTypes.AddResources: - telemetryClient.track('ProvisionAddResourcesNavigate'); + // telemetryClient.track('ProvisionAddResourcesNavigate'); setTitle(DialogTitle.ADD_RESOURCES); break; case PageTypes.ChooseAction: setTitle(DialogTitle.CHOOSE_ACTION); break; case PageTypes.ConfigProvision: - telemetryClient.track('ProvisionConfigureResources'); + // telemetryClient.track('ProvisionConfigureResources'); setTitle(DialogTitle.CONFIG_RESOURCES); break; case PageTypes.EditJson: - telemetryClient.track('ProvisionEditJSON'); + // telemetryClient.track('ProvisionEditJSON'); setTitle(DialogTitle.EDIT); break; case PageTypes.ReviewResource: - telemetryClient.track('ProvisionReviewResources'); + // telemetryClient.track('ProvisionReviewResources'); setTitle(DialogTitle.REVIEW); break; } @@ -704,11 +717,11 @@ export const AzureProvisionDialog: React.FC = () => { const onSubmit = useCallback((options) => { // call back to the main Composer API to begin this process... - telemetryClient.track('ProvisionStart', { - region: options.location, - subscriptionId: options.subscription, - externalResources: options.externalResources, - }); + // telemetryClient.track('ProvisionStart', { + // region: options.location, + // subscriptionId: options.subscription, + // externalResources: options.externalResources, + // }); startProvision(options); clearAll(); @@ -981,7 +994,7 @@ export const AzureProvisionDialog: React.FC = () => { style={{ margin: '0 4px' }} text={formatMessage('Cancel')} onClick={() => { - telemetryClient.track('ProvisionCancel'); + // telemetryClient.track('ProvisionCancel'); closeDialog(); }} /> @@ -1077,7 +1090,7 @@ export const AzureProvisionDialog: React.FC = () => { text={formatMessage('Next')} onClick={() => { if (formData.creationType === 'generate') { - telemetryClient.track('ProvisionShowHandoff'); + // telemetryClient.track('ProvisionShowHandoff'); setShowHandoff(true); } else { setPageAndTitle(PageTypes.ReviewResource); @@ -1101,7 +1114,7 @@ export const AzureProvisionDialog: React.FC = () => { style={{ margin: '0 4px' }} text={formatMessage('Cancel')} onClick={() => { - telemetryClient.track('ProvisionAddResourcesCancel'); + // telemetryClient.track('ProvisionAddResourcesCancel'); closeDialog(); }} /> diff --git a/extensions/localPublish/src/index.ts b/extensions/localPublish/src/index.ts index 3c46508dd1..8f6d673329 100644 --- a/extensions/localPublish/src/index.ts +++ b/extensions/localPublish/src/index.ts @@ -402,17 +402,15 @@ class LocalPublisher implements PublishPlugin { } config = this.getConfig(settings, skillHostEndpoint); let spawnProcess; + const args = [...commandAndArgs, '--port', port, `--urls`, `http://0.0.0.0:${port}`, ...config]; + this.composer.log('Executing command with arguments: %s %s', startCommand, args.join(' ')); try { - spawnProcess = spawn( - startCommand, - [...commandAndArgs, '--port', port, `--urls`, `http://0.0.0.0:${port}`, ...config], - { - cwd: botDir, - stdio: ['ignore', 'pipe', 'pipe'], - detached: !isWin, // detach in non-windows - shell: isWin, // run in a shell on windows so `npm start` doesn't need to be `npm.cmd start` - } - ); + spawnProcess = spawn(startCommand, args, { + cwd: botDir, + stdio: ['ignore', 'pipe', 'pipe'], + detached: !isWin, // detach in non-windows + shell: isWin, // run in a shell on windows so `npm start` doesn't need to be `npm.cmd start` + }); this.composer.log('Started process %d', spawnProcess.pid); this.setBotStatus(botId, { process: spawnProcess, @@ -455,7 +453,7 @@ class LocalPublisher implements PublishPlugin { configList.push('--MicrosoftAppPassword'); configList.push(config.MicrosoftAppPassword); } - if (config.luis) { + if (config.luis && (config.luis.endpointKey || config.luis.authoringKey)) { configList.push('--luis:endpointKey'); configList.push(config.luis.endpointKey || config.luis.authoringKey); } diff --git a/extensions/packageManager/src/node/feeds/npm/npmFeed.ts b/extensions/packageManager/src/node/feeds/npm/npmFeed.ts index fd88d9d0bd..67f9aa0b78 100644 --- a/extensions/packageManager/src/node/feeds/npm/npmFeed.ts +++ b/extensions/packageManager/src/node/feeds/npm/npmFeed.ts @@ -100,6 +100,8 @@ export class NpmFeed implements IFeed { url = `${url}&from=${query.skip}`; } + url = `${url}&popularity=1.0`; + return url; } } diff --git a/extensions/packageManager/src/node/feeds/nuget/nugetFeed.ts b/extensions/packageManager/src/node/feeds/nuget/nugetFeed.ts index 87f41627e5..40fa802971 100644 --- a/extensions/packageManager/src/node/feeds/nuget/nugetFeed.ts +++ b/extensions/packageManager/src/node/feeds/nuget/nugetFeed.ts @@ -70,6 +70,8 @@ export class NuGetFeed implements IFeed { } const searchResult = httpResponse.data as INuGetSearchResult; + // sort these results by total downloads + searchResult.data = searchResult.data.sort((a, b) => b.totalDownloads - a.totalDownloads); if (searchResult.data) { return this.asPackageDefinition(searchResult); } diff --git a/extensions/packageManager/src/node/feeds/nuget/nugetInterfaces.ts b/extensions/packageManager/src/node/feeds/nuget/nugetInterfaces.ts index 32bb1c4bd1..adccbe1378 100644 --- a/extensions/packageManager/src/node/feeds/nuget/nugetInterfaces.ts +++ b/extensions/packageManager/src/node/feeds/nuget/nugetInterfaces.ts @@ -22,6 +22,7 @@ export interface INuGetPackage { versions: INuGetVersion[]; tags?: string | string[]; projectUrl?: string; + totalDownloads: number; } /** diff --git a/extensions/packageManager/src/node/index.ts b/extensions/packageManager/src/node/index.ts index 8ce2ad215a..dac4bc784a 100644 --- a/extensions/packageManager/src/node/index.ts +++ b/extensions/packageManager/src/node/index.ts @@ -15,13 +15,7 @@ import { FeedFactory } from './feeds/feedFactory'; const API_ROOT = '/api'; const hasSchema = (c) => { - // NOTE: A special case for orchestrator is included here because it does not directly include the schema - // the schema for orchestrator is in a dependent package - // additionally, our schemamerge command only returns the top level components found, even though - // it does properly discover and include the schema from this dependent package. - // without this special case, composer does not see orchestrator as being installed even though it is. - // in the future this should be resolved in the schemamerger library by causing the includesSchema property to be passed up to all parent libraries - return c.includesSchema || c.name.toLowerCase() === 'microsoft.bot.components.orchestrator'; + return c.includesSchema || c.keywords?.includes('msbot-component'); }; const isAdaptiveComponent = (c) => { @@ -110,18 +104,6 @@ export default async (composer: IExtensionRegistration): Promise => { text: formatMessage('nuget'), url: 'https://api.nuget.org/v3/index.json', readonly: true, - defaultQuery: { - prerelease: true, - semVerLevel: '2.0.0', - query: `microsoft.bot.components+tags:${botComponentTag}`, - }, - type: PackageSourceType.NuGet, - }, - { - key: 'nuget-community', - text: formatMessage('community packages'), - url: 'https://api.nuget.org/v3/index.json', - readonly: true, defaultQuery: { prerelease: true, semVerLevel: '2.0.0', @@ -134,17 +116,6 @@ export default async (composer: IExtensionRegistration): Promise => { text: formatMessage('npm'), url: `https://registry.npmjs.org/-/v1/search`, readonly: true, - defaultQuery: { - prerelease: true, - query: `keywords:${botComponentTag}+scope:microsoft`, - }, - type: PackageSourceType.NPM, - }, - { - key: 'npm-community', - text: formatMessage('JS community packages'), - url: `https://registry.npmjs.org/-/v1/search`, - readonly: true, defaultQuery: { prerelease: true, query: `keywords:${botComponentTag}`, @@ -227,6 +198,8 @@ export default async (composer: IExtensionRegistration): Promise => { composer.log('GETTING FEED', packageSource, packageSource.defaultQuery); + // set default page size to 100 + packageSource.defaultQuery.take = 100; const packages = await feed.getPackages(packageSource.defaultQuery); if (Array.isArray(packages)) { diff --git a/extensions/packageManager/src/pages/Library.tsx b/extensions/packageManager/src/pages/Library.tsx index 406f34552c..80bc3333fb 100644 --- a/extensions/packageManager/src/pages/Library.tsx +++ b/extensions/packageManager/src/pages/Library.tsx @@ -516,8 +516,6 @@ const Library: React.FC = () => { updateInstalledComponents(results.data.components); } else { - telemetryClient.track('PackageUninstallFailed', { package: selectedItem.name }); - throw new Error(results.data.message); } @@ -526,11 +524,15 @@ const Library: React.FC = () => { } catch (err) { telemetryClient.track('PackageUninstallFailed', { package: selectedItem.name }); - setApplicationLevelError({ - status: err.response.status, - message: err.response && err.response.data.message ? err.response.data.message : err, - summary: strings.importError, - }); + if (err.response) { + setApplicationLevelError({ + status: err.response.status, + message: err.response && err.response.data.message ? err.response.data.message : err, + summary: strings.importError, + }); + } else { + setApplicationLevelError(err); + } } setWorking(''); }