From 59e70a8dd4fef28f3e78cbde17e19600647d1be0 Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 7 Jun 2021 09:41:35 -0700 Subject: [PATCH 01/40] Started stubbing out bot state debug panel --- .../BotStateLog/BotStateLogContent.tsx | 14 +++++++++ .../BotStateLog/BotStateLogHeader.tsx | 29 +++++++++++++++++++ .../TabExtensions/BotStateLog/config.ts | 16 ++++++++++ .../TabExtensions/BotStateLog/index.ts | 4 +++ .../WebChatLog/WebChatLogItemHeader.tsx | 2 +- .../design/DebugPanel/TabExtensions/index.ts | 2 ++ .../design/DebugPanel/TabExtensions/types.ts | 7 ++++- 7 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogContent.tsx create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogHeader.tsx create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/config.ts create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/index.ts diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogContent.tsx new file mode 100644 index 0000000000..8bdbc6ce1b --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogContent.tsx @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React from 'react'; + +import { DebugPanelTabHeaderProps } from '../types'; + +export const BotStateLogContent: React.FC = ({ isActive }) => { + // if (!isActive) { + // return null; + // } + + return

Bot state

; +}; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogHeader.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogHeader.tsx new file mode 100644 index 0000000000..c1eae27d39 --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogHeader.tsx @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx, css } from '@emotion/core'; +import formatMessage from 'format-message'; + +import { DebugPanelTabHeaderProps } from '../types'; + +export const BotStateLogHeader: React.FC = () => { + return ( +
+
+ {formatMessage('Bot state')} +
+
+ ); +}; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/config.ts b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/config.ts new file mode 100644 index 0000000000..0ffb630108 --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/config.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import formatMessage from 'format-message'; + +import { TabExtensionConfig, BotStateTabKey } from '../types'; + +import { BotStateLogHeader } from './BotStateLogHeader'; +import { BotStateLogContent } from './BotStateLogContent'; + +export const BotStateTabConfig: TabExtensionConfig = { + key: BotStateTabKey, + description: () => formatMessage('Bot state log'), + HeaderWidget: BotStateLogHeader, + ContentWidget: BotStateLogContent, +}; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/index.ts b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/index.ts new file mode 100644 index 0000000000..11e13f2217 --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export * from './config'; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WebChatLog/WebChatLogItemHeader.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WebChatLog/WebChatLogItemHeader.tsx index 4ee2644f04..a299b49272 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WebChatLog/WebChatLogItemHeader.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WebChatLog/WebChatLogItemHeader.tsx @@ -42,7 +42,7 @@ export const WebChatLogItemHeader: React.FC = ({ isAct flex-direction: row; align-items: center; `} - data-testid="Tab-Diagnostics" + data-testid="Tab-WebChat" >
Date: Tue, 8 Jun 2021 17:33:11 -0700 Subject: [PATCH 02/40] Rough outline of bot state inspector pane --- .../BotStateLog/BotStateInspectorPane.tsx | 129 ++++++++++++++++++ .../BotStateLog/BotStateLogContent.tsx | 87 +++++++++++- .../WebChatLog/WebChatLogContent.tsx | 2 +- .../client/src/recoilModel/atoms/botState.ts | 12 +- .../src/recoilModel/dispatchers/webchat.ts | 28 +++- Composer/packages/types/src/server.ts | 2 + 6 files changed, 251 insertions(+), 9 deletions(-) create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateInspectorPane.tsx diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateInspectorPane.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateInspectorPane.tsx new file mode 100644 index 0000000000..55c015e7b0 --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateInspectorPane.tsx @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import React, { useMemo, useCallback } from 'react'; +import { ConversationActivityTrafficItem } from '@botframework-composer/types'; +import { useRecoilValue } from 'recoil'; +import { Resizable } from 're-resizable'; +import { NeutralColors } from '@uifabric/fluent-theme'; + +import { + dispatcherState, + inspectedBotStateIndexState, + rootBotProjectIdSelector, + botStateInspectionDataState, +} from '../../../../../recoilModel'; + +const inspectorPane = css` + display: flex; + flex-flow: column nowrap; + padding: 8px 16px; + width: 100%; +`; + +const inspectorPaneToolbar = css` + display: flex; + flex-flow: row nowrap; + width: 100%; +`; + +const inspectorPaneContent = css` + width: 100%; + overflow-y: auto; + overflow-x: hidden; +`; + +type BotStateInspectorPaneProps = { + botStateTraffic: ConversationActivityTrafficItem[]; +}; + +export const BotStateInspectorPane: React.FC = (props) => { + const { botStateTraffic = [] } = props; + const currentProjectId = useRecoilValue(rootBotProjectIdSelector); + const inspectedBotState = useRecoilValue(botStateInspectionDataState(currentProjectId ?? '')); + const inspectedBotStateIndex = useRecoilValue(inspectedBotStateIndexState(currentProjectId ?? '')); + const { setBotStateInspectionData, setInspectedBotStateIndex } = useRecoilValue(dispatcherState); + + const prevButtonIsDisabled = useMemo(() => { + return !!(inspectedBotStateIndex === undefined || !botStateTraffic.length || inspectedBotStateIndex === 0); + }, [botStateTraffic, inspectedBotStateIndex]); + + const nextButtonIsDisabled = useMemo(() => { + return !!( + inspectedBotStateIndex === undefined || + !botStateTraffic.length || + inspectedBotStateIndex >= botStateTraffic.length - 1 + ); + }, [botStateTraffic, inspectedBotStateIndex]); + + const inspectPrevBotState = useCallback(() => { + if (currentProjectId && inspectedBotStateIndex !== undefined) { + const newIndex = inspectedBotStateIndex - 1; + setInspectedBotStateIndex(currentProjectId, newIndex); + setBotStateInspectionData(currentProjectId, botStateTraffic[newIndex].activity); + } + }, [botStateTraffic, currentProjectId, inspectedBotStateIndex]); + + const inspectNextBotState = useCallback(() => { + if (currentProjectId && inspectedBotStateIndex !== undefined) { + const newIndex = inspectedBotStateIndex + 1; + setInspectedBotStateIndex(currentProjectId, newIndex); + setBotStateInspectionData(currentProjectId, botStateTraffic[newIndex].activity); + } + }, [botStateTraffic, currentProjectId, inspectedBotStateIndex]); + + const showDiff = useCallback(() => { + console.log('TODO: Show diff'); + }, []); + + const copyBotState = useCallback(() => { + console.log('TODO: Copy bot state'); + }, []); + + return ( + +
+
+ + + + + +
+
{JSON.stringify(inspectedBotState, null, 2)}
+
+
+ ); +}; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogContent.tsx index 8bdbc6ce1b..6af2b0f58f 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogContent.tsx @@ -1,14 +1,91 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import React from 'react'; +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import React, { useMemo, useCallback } from 'react'; +import { useRecoilValue } from 'recoil'; +import { ConversationActivityTrafficItem } from '@botframework-composer/types'; import { DebugPanelTabHeaderProps } from '../types'; +import { + rootBotProjectIdSelector, + webChatTrafficState, + dispatcherState, + inspectedBotStateIndexState, +} from '../../../../../recoilModel'; +import { WebChatActivityLogItem } from '../WebChatLog/WebChatActivityLogItem'; +import { WebChatInspectionData } from '../../../../../recoilModel/types'; + +import { BotStateInspectorPane } from './BotStateInspectorPane'; + +const itemIsSelected = (itemIndex: number, currentlyInspectedIndex?: number) => { + return itemIndex === currentlyInspectedIndex; +}; + +const logContainer = css` + height: 100%; + width: 100%; + display: flex; + overflow: auto; + flex-direction: row; +`; + +const logPane = (trafficLength: number) => css` + height: 100%; + width: 50%; + display: flex; + overflow: auto; + flex-direction: column; + padding: ${trafficLength ? '16px 0' : '4px 0'}; + box-sizing: border-box; +`; export const BotStateLogContent: React.FC = ({ isActive }) => { - // if (!isActive) { - // return null; - // } + const currentProjectId = useRecoilValue(rootBotProjectIdSelector); + const rawWebChatTraffic = useRecoilValue(webChatTrafficState(currentProjectId ?? '')); + const inspectedBotStateIndex = useRecoilValue(inspectedBotStateIndexState(currentProjectId ?? '')); + const { setBotStateInspectionData, setInspectedBotStateIndex } = useRecoilValue(dispatcherState); + + const onClickTraffic = useCallback( + (data: WebChatInspectionData, index: number) => { + if (currentProjectId && data.item.trafficType === 'activity') { + console.log('clicking ', data); + setBotStateInspectionData(currentProjectId, data.item.activity); + setInspectedBotStateIndex(currentProjectId, index); + } + }, + [currentProjectId] + ); + + const botStateTraffic = useMemo(() => { + return rawWebChatTraffic.filter( + (t) => t.trafficType === 'activity' && t.activity.type === 'trace' && t.activity.name === 'BotState' + ) as ConversationActivityTrafficItem[]; + }, [rawWebChatTraffic]); - return

Bot state

; + if (isActive) { + return ( +
+
+ {botStateTraffic.map((botStateTrace, index) => { + const onClickTrafficWrapper = (data: WebChatInspectionData) => { + onClickTraffic(data, index); + }; + return ( + + ); + })} +
+ +
+ ); + } else { + return null; + } }; 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 b7fa7dd8d1..88830c4931 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 @@ -5,7 +5,7 @@ import { css, jsx } from '@emotion/core'; import React, { useMemo, useEffect, useState, useRef, useCallback } from 'react'; import { useRecoilValue } from 'recoil'; -import { ConversationTrafficItem } from '@botframework-composer/types/src'; +import { ConversationTrafficItem } from '@botframework-composer/types'; import formatMessage from 'format-message'; import debounce from 'lodash/debounce'; import { ActionButton } from 'office-ui-fabric-react/lib/Button'; diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index 4e337064a5..79ec6022bb 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -19,7 +19,7 @@ import { RecognizerFile, PublishTarget, } from '@bfc/shared'; -import { ConversationTrafficItem } from '@botframework-composer/types'; +import { ConversationTrafficItem, Activity } from '@botframework-composer/types'; import { atomFamily } from 'recoil'; import { BotStartError, DesignPageLocation, WebChatInspectionData, RuntimeOutputData } from '../../recoilModel/types'; @@ -447,6 +447,16 @@ export const webChatInspectionDataState = atomFamily({ + key: getFullyQualifiedKey('botStateInspectionData'), + default: undefined, +}); + +export const inspectedBotStateIndexState = atomFamily({ + key: getFullyQualifiedKey('inspectedBotStateIndex'), + default: undefined, +}); + export const projectIndexingState = atomFamily({ key: getFullyQualifiedKey('projectIndexing'), default: false, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts b/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts index 7d9f02d82d..18d06c53e8 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts @@ -2,10 +2,16 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ConversationTrafficItem } from '@botframework-composer/types'; +import { ConversationTrafficItem, Activity } from '@botframework-composer/types'; import { useRecoilCallback, CallbackInterface } from 'recoil'; -import { webChatTrafficState, webChatInspectionDataState, isWebChatPanelVisibleState } from '../atoms'; +import { + webChatTrafficState, + webChatInspectionDataState, + isWebChatPanelVisibleState, + botStateInspectionDataState, + inspectedBotStateIndexState, +} from '../atoms'; import { WebChatInspectionData } from '../types'; export const webChatLogDispatcher = () => { @@ -13,6 +19,8 @@ export const webChatLogDispatcher = () => { const { set } = callbackHelpers; set(webChatTrafficState(projectId), []); set(webChatInspectionDataState(projectId), undefined); // clear the inspection panel + set(botStateInspectionDataState(projectId), undefined); + set(inspectedBotStateIndexState(projectId), undefined); }); const setWebChatPanelVisibility = useRecoilCallback((callbackHelpers: CallbackInterface) => (value: boolean) => { @@ -43,9 +51,25 @@ export const webChatLogDispatcher = () => { } ); + const setBotStateInspectionData = useRecoilCallback( + (callbackHelpers: CallbackInterface) => (projectId: string, inspectionData: Activity) => { + const { set } = callbackHelpers; + set(botStateInspectionDataState(projectId), inspectionData); + } + ); + + const setInspectedBotStateIndex = useRecoilCallback( + (callbackHelpers: CallbackInterface) => (projectId: string, botStateIndex: number) => { + const { set } = callbackHelpers; + set(inspectedBotStateIndexState(projectId), botStateIndex); + } + ); + return { clearWebChatLogs, appendWebChatTraffic, + setBotStateInspectionData, + setInspectedBotStateIndex, setWebChatPanelVisibility, setWebChatInspectionData, }; diff --git a/Composer/packages/types/src/server.ts b/Composer/packages/types/src/server.ts index 6ee8b1a508..020caadd46 100644 --- a/Composer/packages/types/src/server.ts +++ b/Composer/packages/types/src/server.ts @@ -95,3 +95,5 @@ export type ConversationNetworkErrorItem = { timestamp: number; trafficType: 'networkError'; }; + +export { Activity }; From c5e2d2081446ba6852dbe6c0319782c0d280ee6e Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 10 Jun 2021 13:30:29 -0700 Subject: [PATCH 03/40] Stubbed out new table view --- .../BotStateLog/BotStateInspectorPane.tsx | 129 ------------- .../BotStateLog/BotStateLogContent.tsx | 91 --------- .../TabExtensions/BotStateLog/config.ts | 16 -- .../WatchTab/WatchTabContent.tsx | 181 ++++++++++++++++++ .../WatchTabHeader.tsx} | 6 +- .../TabExtensions/WatchTab/config.ts | 16 ++ .../{BotStateLog => WatchTab}/index.ts | 0 .../design/DebugPanel/TabExtensions/index.ts | 4 +- .../design/DebugPanel/TabExtensions/types.ts | 4 +- .../client/src/recoilModel/atoms/botState.ts | 12 +- .../src/recoilModel/dispatchers/webchat.ts | 28 +-- 11 files changed, 207 insertions(+), 280 deletions(-) delete mode 100644 Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateInspectorPane.tsx delete mode 100644 Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogContent.tsx delete mode 100644 Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/config.ts create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx rename Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/{BotStateLog/BotStateLogHeader.tsx => WatchTab/WatchTabHeader.tsx} (76%) create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/config.ts rename Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/{BotStateLog => WatchTab}/index.ts (100%) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateInspectorPane.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateInspectorPane.tsx deleted file mode 100644 index 55c015e7b0..0000000000 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateInspectorPane.tsx +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { css, jsx } from '@emotion/core'; -import React, { useMemo, useCallback } from 'react'; -import { ConversationActivityTrafficItem } from '@botframework-composer/types'; -import { useRecoilValue } from 'recoil'; -import { Resizable } from 're-resizable'; -import { NeutralColors } from '@uifabric/fluent-theme'; - -import { - dispatcherState, - inspectedBotStateIndexState, - rootBotProjectIdSelector, - botStateInspectionDataState, -} from '../../../../../recoilModel'; - -const inspectorPane = css` - display: flex; - flex-flow: column nowrap; - padding: 8px 16px; - width: 100%; -`; - -const inspectorPaneToolbar = css` - display: flex; - flex-flow: row nowrap; - width: 100%; -`; - -const inspectorPaneContent = css` - width: 100%; - overflow-y: auto; - overflow-x: hidden; -`; - -type BotStateInspectorPaneProps = { - botStateTraffic: ConversationActivityTrafficItem[]; -}; - -export const BotStateInspectorPane: React.FC = (props) => { - const { botStateTraffic = [] } = props; - const currentProjectId = useRecoilValue(rootBotProjectIdSelector); - const inspectedBotState = useRecoilValue(botStateInspectionDataState(currentProjectId ?? '')); - const inspectedBotStateIndex = useRecoilValue(inspectedBotStateIndexState(currentProjectId ?? '')); - const { setBotStateInspectionData, setInspectedBotStateIndex } = useRecoilValue(dispatcherState); - - const prevButtonIsDisabled = useMemo(() => { - return !!(inspectedBotStateIndex === undefined || !botStateTraffic.length || inspectedBotStateIndex === 0); - }, [botStateTraffic, inspectedBotStateIndex]); - - const nextButtonIsDisabled = useMemo(() => { - return !!( - inspectedBotStateIndex === undefined || - !botStateTraffic.length || - inspectedBotStateIndex >= botStateTraffic.length - 1 - ); - }, [botStateTraffic, inspectedBotStateIndex]); - - const inspectPrevBotState = useCallback(() => { - if (currentProjectId && inspectedBotStateIndex !== undefined) { - const newIndex = inspectedBotStateIndex - 1; - setInspectedBotStateIndex(currentProjectId, newIndex); - setBotStateInspectionData(currentProjectId, botStateTraffic[newIndex].activity); - } - }, [botStateTraffic, currentProjectId, inspectedBotStateIndex]); - - const inspectNextBotState = useCallback(() => { - if (currentProjectId && inspectedBotStateIndex !== undefined) { - const newIndex = inspectedBotStateIndex + 1; - setInspectedBotStateIndex(currentProjectId, newIndex); - setBotStateInspectionData(currentProjectId, botStateTraffic[newIndex].activity); - } - }, [botStateTraffic, currentProjectId, inspectedBotStateIndex]); - - const showDiff = useCallback(() => { - console.log('TODO: Show diff'); - }, []); - - const copyBotState = useCallback(() => { - console.log('TODO: Copy bot state'); - }, []); - - return ( - -
-
- - - - - -
-
{JSON.stringify(inspectedBotState, null, 2)}
-
-
- ); -}; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogContent.tsx deleted file mode 100644 index 6af2b0f58f..0000000000 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogContent.tsx +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -/** @jsx jsx */ -import { css, jsx } from '@emotion/core'; -import React, { useMemo, useCallback } from 'react'; -import { useRecoilValue } from 'recoil'; -import { ConversationActivityTrafficItem } from '@botframework-composer/types'; - -import { DebugPanelTabHeaderProps } from '../types'; -import { - rootBotProjectIdSelector, - webChatTrafficState, - dispatcherState, - inspectedBotStateIndexState, -} from '../../../../../recoilModel'; -import { WebChatActivityLogItem } from '../WebChatLog/WebChatActivityLogItem'; -import { WebChatInspectionData } from '../../../../../recoilModel/types'; - -import { BotStateInspectorPane } from './BotStateInspectorPane'; - -const itemIsSelected = (itemIndex: number, currentlyInspectedIndex?: number) => { - return itemIndex === currentlyInspectedIndex; -}; - -const logContainer = css` - height: 100%; - width: 100%; - display: flex; - overflow: auto; - flex-direction: row; -`; - -const logPane = (trafficLength: number) => css` - height: 100%; - width: 50%; - display: flex; - overflow: auto; - flex-direction: column; - padding: ${trafficLength ? '16px 0' : '4px 0'}; - box-sizing: border-box; -`; - -export const BotStateLogContent: React.FC = ({ isActive }) => { - const currentProjectId = useRecoilValue(rootBotProjectIdSelector); - const rawWebChatTraffic = useRecoilValue(webChatTrafficState(currentProjectId ?? '')); - const inspectedBotStateIndex = useRecoilValue(inspectedBotStateIndexState(currentProjectId ?? '')); - const { setBotStateInspectionData, setInspectedBotStateIndex } = useRecoilValue(dispatcherState); - - const onClickTraffic = useCallback( - (data: WebChatInspectionData, index: number) => { - if (currentProjectId && data.item.trafficType === 'activity') { - console.log('clicking ', data); - setBotStateInspectionData(currentProjectId, data.item.activity); - setInspectedBotStateIndex(currentProjectId, index); - } - }, - [currentProjectId] - ); - - const botStateTraffic = useMemo(() => { - return rawWebChatTraffic.filter( - (t) => t.trafficType === 'activity' && t.activity.type === 'trace' && t.activity.name === 'BotState' - ) as ConversationActivityTrafficItem[]; - }, [rawWebChatTraffic]); - - if (isActive) { - return ( -
-
- {botStateTraffic.map((botStateTrace, index) => { - const onClickTrafficWrapper = (data: WebChatInspectionData) => { - onClickTraffic(data, index); - }; - return ( - - ); - })} -
- -
- ); - } else { - return null; - } -}; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/config.ts b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/config.ts deleted file mode 100644 index 0ffb630108..0000000000 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/config.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import formatMessage from 'format-message'; - -import { TabExtensionConfig, BotStateTabKey } from '../types'; - -import { BotStateLogHeader } from './BotStateLogHeader'; -import { BotStateLogContent } from './BotStateLogContent'; - -export const BotStateTabConfig: TabExtensionConfig = { - key: BotStateTabKey, - description: () => formatMessage('Bot state log'), - HeaderWidget: BotStateLogHeader, - ContentWidget: BotStateLogContent, -}; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx new file mode 100644 index 0000000000..e219ceb9ea --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import React, { useMemo, useState, useCallback } from 'react'; +import { useRecoilValue } from 'recoil'; +import { ConversationActivityTrafficItem, Activity } from '@botframework-composer/types'; +import { IIconProps } from 'office-ui-fabric-react/lib/Icon'; +import { CommandBarButton } from 'office-ui-fabric-react/lib/Button'; +import { DetailsList, DetailsListLayoutMode, IColumn } from 'office-ui-fabric-react/lib/DetailsList'; +import formatMessage from 'format-message'; +import { JsonEditor } from '@bfc/code-editor'; + +import { DebugPanelTabHeaderProps } from '../types'; +import { rootBotProjectIdSelector, webChatTrafficState } from '../../../../../recoilModel'; +import { getDefaultFontSettings } from '../../../../../recoilModel/utils/fontUtil'; + +const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); + +const contentContainer = css` + display: flex; + flex-flow: column nowrap; + height: 100%; + width: 100%; +`; + +const toolbar = css` + display: flex; + flex-flow: row nowrap; + height: 24px; + padding: 8px 16px; +`; + +const content = css` + display: flex; + flex-flow: column nowrap; + height: 100%; + width: 100%; + overflow-y: scroll; +`; + +const editorStyles = css` + border: none; +`; + +const objectCell = css` + height: 160px; + width: 360px; +`; + +const addIcon: IIconProps = { + iconName: 'Add', +}; + +const removeIcon: IIconProps = { + iconName: 'Cancel', +}; + +const NameColumnKey = 'column1'; +const ValueColumnKey = 'column2'; +const watchTableColumns: IColumn[] = [ + { key: NameColumnKey, name: 'Name', fieldName: 'name', minWidth: 100, maxWidth: 200, isResizable: true }, + { key: ValueColumnKey, name: 'Value', fieldName: 'value', minWidth: 100, maxWidth: 360, isResizable: true }, +]; + +const watchTableLayout: DetailsListLayoutMode = DetailsListLayoutMode.fixedColumns; + +const getValueFromBotTraceScope = (delimitedProperty: string, botTrace: Activity) => { + const propertySegments = delimitedProperty.split('.'); + const value = propertySegments.reduce( + (accumulator: object | string | number | boolean | undefined, segment, index) => { + // first try to grab the specified property off the root of the bot trace's memory + if (index === 0) { + console.log('grabbing root value: ', segment); + return botTrace?.value[segment]; + } + // if we are not on the root, try accessing the next value of the desired property + if (typeof accumulator === 'object') { + return accumulator[segment]; + } else { + return undefined; + } + }, + undefined + ); + return value; +}; + +export const WatchTabContent: React.FC = ({ isActive }) => { + const currentProjectId = useRecoilValue(rootBotProjectIdSelector); + const rawWebChatTraffic = useRecoilValue(webChatTrafficState(currentProjectId ?? '')); + // TODO: move to recoil + const [watchedProperties, setWatchedProperties] = useState(['user.boolean', 'user.complexObj']); + + const mostRecentBotState = useMemo(() => { + const botStateTraffic = rawWebChatTraffic.filter( + (t) => t.trafficType === 'activity' && t.activity.type === 'trace' && t.activity.name === 'BotState' + ) as ConversationActivityTrafficItem[]; + if (botStateTraffic.length) { + return botStateTraffic[botStateTraffic.length - 1]; + } + }, [rawWebChatTraffic]); + + const renderColumn = useCallback( + (item: string, index: number | undefined, column: IColumn | undefined) => { + if (column) { + if (column.key === NameColumnKey) { + // render picker + return {item}; + } else if (column.key === ValueColumnKey) { + // render the value display + if (mostRecentBotState) { + const value = getValueFromBotTraceScope(item, mostRecentBotState?.activity); + if (typeof value === 'object') { + // render monaco view + // TODO: is there some way we can expand the height of the cell based on the number of object keys? + return ( +
+ null} + /> +
+ ); + } else if (value === undefined) { + // don't render anything + return null; + } else { + // render primitive view + return {String(value)}; + } + } else { + // no bot trace available + return null; + } + } + } + return null; + }, + [mostRecentBotState] + ); + + if (isActive) { + return ( +
+ {/** TODO: factor toolbar and content out into own components? */} +
+ + +
+
+ +
+
+ ); + } else { + return null; + } +}; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogHeader.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabHeader.tsx similarity index 76% rename from Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogHeader.tsx rename to Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabHeader.tsx index c1eae27d39..9722b507cd 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/BotStateLogHeader.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabHeader.tsx @@ -7,7 +7,7 @@ import formatMessage from 'format-message'; import { DebugPanelTabHeaderProps } from '../types'; -export const BotStateLogHeader: React.FC = () => { +export const WatchTabHeader: React.FC = () => { return (
= () => { flex-direction: row; align-items: center; `} - data-testid="Tab-BotState" + data-testid="Tab-Watch" >
- {formatMessage('Bot state')} + {formatMessage('Watch')}
); diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/config.ts b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/config.ts new file mode 100644 index 0000000000..07b109093e --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/config.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import formatMessage from 'format-message'; + +import { TabExtensionConfig, WatchTabKey } from '../types'; + +import { WatchTabHeader } from './WatchTabHeader'; +import { WatchTabContent } from './WatchTabContent'; + +export const WatchTabConfig: TabExtensionConfig = { + key: WatchTabKey, + description: () => formatMessage('Watch tab'), + HeaderWidget: WatchTabHeader, + ContentWidget: WatchTabContent, +}; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/index.ts b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/index.ts similarity index 100% rename from Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/BotStateLog/index.ts rename to Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/index.ts diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/index.ts b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/index.ts index f1b2dd4c26..e541ab4376 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/index.ts +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/index.ts @@ -5,13 +5,13 @@ import { TabExtensionConfig } from './types'; import { DiagnosticsTabConfig } from './DiagnosticsTab'; import { WebChatLogTabConfig } from './WebChatLog/config'; import { RuntimeOutputTabConfig } from './RuntimeOutputLog'; -import { BotStateTabConfig } from './BotStateLog'; +import { WatchTabConfig } from './WatchTab'; const implementedDebugExtensions: TabExtensionConfig[] = [ DiagnosticsTabConfig, WebChatLogTabConfig, RuntimeOutputTabConfig, - BotStateTabConfig, + WatchTabConfig, ]; export default implementedDebugExtensions; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/types.ts b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/types.ts index 1d0e5a2c76..c256aa5796 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/types.ts +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/types.ts @@ -6,13 +6,13 @@ import { FC } from 'react'; export const DiagnosticsTabKey = 'Diagnostics'; export const WebChatInspectorTabKey = 'WebChatInspector'; export const RuntimeLogTabKey = 'RuntimeLog'; -export const BotStateTabKey = 'BotState'; +export const WatchTabKey = 'Watch'; export type DebugDrawerKeys = | typeof DiagnosticsTabKey | typeof WebChatInspectorTabKey | typeof RuntimeLogTabKey - | typeof BotStateTabKey; + | typeof WatchTabKey; export type DebugPanelTabHeaderProps = { isActive: boolean; diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index 79ec6022bb..4e337064a5 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -19,7 +19,7 @@ import { RecognizerFile, PublishTarget, } from '@bfc/shared'; -import { ConversationTrafficItem, Activity } from '@botframework-composer/types'; +import { ConversationTrafficItem } from '@botframework-composer/types'; import { atomFamily } from 'recoil'; import { BotStartError, DesignPageLocation, WebChatInspectionData, RuntimeOutputData } from '../../recoilModel/types'; @@ -447,16 +447,6 @@ export const webChatInspectionDataState = atomFamily({ - key: getFullyQualifiedKey('botStateInspectionData'), - default: undefined, -}); - -export const inspectedBotStateIndexState = atomFamily({ - key: getFullyQualifiedKey('inspectedBotStateIndex'), - default: undefined, -}); - export const projectIndexingState = atomFamily({ key: getFullyQualifiedKey('projectIndexing'), default: false, diff --git a/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts b/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts index 18d06c53e8..7d9f02d82d 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts @@ -2,16 +2,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ConversationTrafficItem, Activity } from '@botframework-composer/types'; +import { ConversationTrafficItem } from '@botframework-composer/types'; import { useRecoilCallback, CallbackInterface } from 'recoil'; -import { - webChatTrafficState, - webChatInspectionDataState, - isWebChatPanelVisibleState, - botStateInspectionDataState, - inspectedBotStateIndexState, -} from '../atoms'; +import { webChatTrafficState, webChatInspectionDataState, isWebChatPanelVisibleState } from '../atoms'; import { WebChatInspectionData } from '../types'; export const webChatLogDispatcher = () => { @@ -19,8 +13,6 @@ export const webChatLogDispatcher = () => { const { set } = callbackHelpers; set(webChatTrafficState(projectId), []); set(webChatInspectionDataState(projectId), undefined); // clear the inspection panel - set(botStateInspectionDataState(projectId), undefined); - set(inspectedBotStateIndexState(projectId), undefined); }); const setWebChatPanelVisibility = useRecoilCallback((callbackHelpers: CallbackInterface) => (value: boolean) => { @@ -51,25 +43,9 @@ export const webChatLogDispatcher = () => { } ); - const setBotStateInspectionData = useRecoilCallback( - (callbackHelpers: CallbackInterface) => (projectId: string, inspectionData: Activity) => { - const { set } = callbackHelpers; - set(botStateInspectionDataState(projectId), inspectionData); - } - ); - - const setInspectedBotStateIndex = useRecoilCallback( - (callbackHelpers: CallbackInterface) => (projectId: string, botStateIndex: number) => { - const { set } = callbackHelpers; - set(inspectedBotStateIndexState(projectId), botStateIndex); - } - ); - return { clearWebChatLogs, appendWebChatTraffic, - setBotStateInspectionData, - setInspectedBotStateIndex, setWebChatPanelVisibility, setWebChatInspectionData, }; From 48d68fba30e86d6d064f65433287cfe2c71aad05 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 10 Jun 2021 15:11:00 -0700 Subject: [PATCH 04/40] Added placeholder input fields --- .../WatchTab/WatchTabContent.tsx | 60 +++++++++++++++---- .../client/src/recoilModel/atoms/botState.ts | 5 ++ .../src/recoilModel/dispatchers/webchat.ts | 16 ++++- 3 files changed, 70 insertions(+), 11 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index e219ceb9ea..e9db7a8a5d 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -3,7 +3,7 @@ /** @jsx jsx */ import { css, jsx } from '@emotion/core'; -import React, { useMemo, useState, useCallback } from 'react'; +import React, { useMemo, useCallback, useRef } from 'react'; import { useRecoilValue } from 'recoil'; import { ConversationActivityTrafficItem, Activity } from '@botframework-composer/types'; import { IIconProps } from 'office-ui-fabric-react/lib/Icon'; @@ -11,9 +11,15 @@ import { CommandBarButton } from 'office-ui-fabric-react/lib/Button'; import { DetailsList, DetailsListLayoutMode, IColumn } from 'office-ui-fabric-react/lib/DetailsList'; import formatMessage from 'format-message'; import { JsonEditor } from '@bfc/code-editor'; +import debounce from 'lodash/debounce'; import { DebugPanelTabHeaderProps } from '../types'; -import { rootBotProjectIdSelector, webChatTrafficState } from '../../../../../recoilModel'; +import { + rootBotProjectIdSelector, + webChatTrafficState, + watchedVariablesState, + dispatcherState, +} from '../../../../../recoilModel'; import { getDefaultFontSettings } from '../../../../../recoilModel/utils/fontUtil'; const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); @@ -66,13 +72,13 @@ const watchTableColumns: IColumn[] = [ const watchTableLayout: DetailsListLayoutMode = DetailsListLayoutMode.fixedColumns; +// this can be exported and used in other places const getValueFromBotTraceScope = (delimitedProperty: string, botTrace: Activity) => { const propertySegments = delimitedProperty.split('.'); const value = propertySegments.reduce( (accumulator: object | string | number | boolean | undefined, segment, index) => { // first try to grab the specified property off the root of the bot trace's memory if (index === 0) { - console.log('grabbing root value: ', segment); return botTrace?.value[segment]; } // if we are not on the root, try accessing the next value of the desired property @@ -90,8 +96,19 @@ const getValueFromBotTraceScope = (delimitedProperty: string, botTrace: Activity export const WatchTabContent: React.FC = ({ isActive }) => { const currentProjectId = useRecoilValue(rootBotProjectIdSelector); const rawWebChatTraffic = useRecoilValue(webChatTrafficState(currentProjectId ?? '')); - // TODO: move to recoil - const [watchedProperties, setWatchedProperties] = useState(['user.boolean', 'user.complexObj']); + const watchedProperties = useRecoilValue(watchedVariablesState(currentProjectId ?? '')); + const { setWatchedVariables } = useRecoilValue(dispatcherState); + + const setPropertyValue = useRef( + debounce((event: React.ChangeEvent, propertyIndex: number) => { + if (currentProjectId) { + const updatedProperties = [...watchedProperties]; + updatedProperties[propertyIndex] = event?.target?.value; + console.log('setting watched properties: ', updatedProperties); + setWatchedVariables(currentProjectId, updatedProperties); + } + }, 500) + ).current; const mostRecentBotState = useMemo(() => { const botStateTraffic = rawWebChatTraffic.filter( @@ -102,12 +119,26 @@ export const WatchTabContent: React.FC = ({ isActive } } }, [rawWebChatTraffic]); + // we need to refresh the details list every time a new bot state comes in + const refreshedWatchedProperties = useMemo(() => { + return [...watchedProperties]; + }, [mostRecentBotState, watchedProperties]); + const renderColumn = useCallback( (item: string, index: number | undefined, column: IColumn | undefined) => { - if (column) { + if (column && index !== undefined) { if (column.key === NameColumnKey) { // render picker - return {item}; + return ( + { + ev.persist(); + setPropertyValue(ev, index); + }} + > + ); + //return {item}; } else if (column.key === ValueColumnKey) { // render the value display if (mostRecentBotState) { @@ -157,18 +188,27 @@ export const WatchTabContent: React.FC = ({ isActive } [mostRecentBotState] ); + const onClickAdd = useCallback(() => { + setWatchedVariables(currentProjectId ?? '', [...watchedProperties, 'placeholder']); + }, [watchedProperties]); + + const onClickRemove = useCallback(() => { + watchedProperties.pop(); + setWatchedVariables(currentProjectId ?? '', [...watchedProperties]); + }, [watchedProperties]); + if (isActive) { return (
{/** TODO: factor toolbar and content out into own components? */}
- - + +
diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index 4e337064a5..bce356cfa0 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -452,6 +452,11 @@ export const projectIndexingState = atomFamily({ default: false, }); +export const watchedVariablesState = atomFamily({ + key: getFullyQualifiedKey('watchedVariables'), + default: [], +}); + export const runtimeStandardOutputDataState = atomFamily({ key: getFullyQualifiedKey('runtimeStandardOutputData'), default: { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts b/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts index 7d9f02d82d..bc7f4adf51 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts @@ -5,7 +5,12 @@ import { ConversationTrafficItem } from '@botframework-composer/types'; import { useRecoilCallback, CallbackInterface } from 'recoil'; -import { webChatTrafficState, webChatInspectionDataState, isWebChatPanelVisibleState } from '../atoms'; +import { + webChatTrafficState, + webChatInspectionDataState, + isWebChatPanelVisibleState, + watchedVariablesState, +} from '../atoms'; import { WebChatInspectionData } from '../types'; export const webChatLogDispatcher = () => { @@ -13,6 +18,7 @@ export const webChatLogDispatcher = () => { const { set } = callbackHelpers; set(webChatTrafficState(projectId), []); set(webChatInspectionDataState(projectId), undefined); // clear the inspection panel + set(watchedVariablesState(projectId), []); // TODO: might not want to do this depending on how annoying it is for the user -- do you want to wipe variables when you restart convo? }); const setWebChatPanelVisibility = useRecoilCallback((callbackHelpers: CallbackInterface) => (value: boolean) => { @@ -43,9 +49,17 @@ export const webChatLogDispatcher = () => { } ); + const setWatchedVariables = useRecoilCallback( + (callbackHelpers: CallbackInterface) => (projectId: string, variables: string[]) => { + const { set } = callbackHelpers; + set(watchedVariablesState(projectId), [...variables]); + } + ); + return { clearWebChatLogs, appendWebChatTraffic, + setWatchedVariables, setWebChatPanelVisibility, setWebChatInspectionData, }; From 3457bc8ef297bd285fa05f48367ef8d831d7ea4a Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Tue, 15 Jun 2021 13:56:26 -0700 Subject: [PATCH 05/40] Watch variable update Signed-off-by: Srinaath Ravichandran --- .../RuntimeOutputLog/OutputTabContent.tsx | 1 + .../WatchTab/WatchTabContent.tsx | 132 +++++--- .../WatchVariablePicker.tsx | 281 ++++++++++++++++++ .../utils/components/PropertyTreeItem.tsx | 91 ++++++ .../WatchVariableSelector/utils/helpers.ts | 111 +++++++ .../utils/hooks/useNoSearchResultMenuItem.tsx | 38 +++ 6 files changed, 614 insertions(+), 40 deletions(-) create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PropertyTreeItem.tsx create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/helpers.ts create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/hooks/useNoSearchResultMenuItem.tsx diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/RuntimeOutputLog/OutputTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/RuntimeOutputLog/OutputTabContent.tsx index ec34d8c558..3e0b1f605f 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/RuntimeOutputLog/OutputTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/RuntimeOutputLog/OutputTabContent.tsx @@ -119,6 +119,7 @@ export const OutputsTabContent: React.FC = ({ isActive splitterSize="5px" > +
diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index e9db7a8a5d..a13be7a886 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -3,24 +3,29 @@ /** @jsx jsx */ import { css, jsx } from '@emotion/core'; -import React, { useMemo, useCallback, useRef } from 'react'; +import React, { useMemo, useCallback, useState, useEffect } from 'react'; import { useRecoilValue } from 'recoil'; +import { v4 as uuidv4 } from 'uuid'; import { ConversationActivityTrafficItem, Activity } from '@botframework-composer/types'; import { IIconProps } from 'office-ui-fabric-react/lib/Icon'; import { CommandBarButton } from 'office-ui-fabric-react/lib/Button'; -import { DetailsList, DetailsListLayoutMode, IColumn } from 'office-ui-fabric-react/lib/DetailsList'; +import { + DetailsList, + DetailsListLayoutMode, + IColumn, + SelectionMode, + Selection, + IObjectWithKey, +} from 'office-ui-fabric-react/lib/DetailsList'; import formatMessage from 'format-message'; import { JsonEditor } from '@bfc/code-editor'; -import debounce from 'lodash/debounce'; +import produce from 'immer'; import { DebugPanelTabHeaderProps } from '../types'; -import { - rootBotProjectIdSelector, - webChatTrafficState, - watchedVariablesState, - dispatcherState, -} from '../../../../../recoilModel'; +import { rootBotProjectIdSelector, webChatTrafficState } from '../../../../../recoilModel'; import { getDefaultFontSettings } from '../../../../../recoilModel/utils/fontUtil'; +import { WatchVariablePicker } from '../../WatchVariableSelector/WatchVariablePicker'; +import { getMemoryVariables } from '../../../../../recoilModel/dispatchers/utils/project'; const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); @@ -96,19 +101,40 @@ const getValueFromBotTraceScope = (delimitedProperty: string, botTrace: Activity export const WatchTabContent: React.FC = ({ isActive }) => { const currentProjectId = useRecoilValue(rootBotProjectIdSelector); const rawWebChatTraffic = useRecoilValue(webChatTrafficState(currentProjectId ?? '')); - const watchedProperties = useRecoilValue(watchedVariablesState(currentProjectId ?? '')); - const { setWatchedVariables } = useRecoilValue(dispatcherState); - - const setPropertyValue = useRef( - debounce((event: React.ChangeEvent, propertyIndex: number) => { - if (currentProjectId) { - const updatedProperties = [...watchedProperties]; - updatedProperties[propertyIndex] = event?.target?.value; - console.log('setting watched properties: ', updatedProperties); - setWatchedVariables(currentProjectId, updatedProperties); - } - }, 500) - ).current; + const [watchVariables, setWatchVariable] = useState>({}); + + const [selectedItems, setSelectedItems] = useState(); + + const selection = useMemo( + () => + new Selection({ + onSelectionChanged: () => { + //console.log('handle selection change',selection.getSelection()) + setSelectedItems(selection.getSelection()); + }, + selectionMode: SelectionMode.multiple, + }), + [] + ); + + const [memoryVariablesPayload, setMemoryVariablesPayload] = useState({ + kind: 'property', + data: { properties: [] }, + }); + + useEffect(() => { + if (currentProjectId) { + const abortController = new AbortController(); + (async () => { + try { + const variables = await getMemoryVariables(currentProjectId, { signal: abortController.signal }); + setMemoryVariablesPayload({ kind: 'property', data: { properties: variables } }); + } catch (e) { + // error can be due to abort + } + })(); + } + }, [currentProjectId]); const mostRecentBotState = useMemo(() => { const botStateTraffic = rawWebChatTraffic.filter( @@ -119,30 +145,45 @@ export const WatchTabContent: React.FC = ({ isActive } } }, [rawWebChatTraffic]); + const onSelectPath = useCallback( + (variableId: string, path: string) => { + setWatchVariable({ + ...watchVariables, + [variableId]: path, + }); + }, + [watchVariables] + ); + // we need to refresh the details list every time a new bot state comes in const refreshedWatchedProperties = useMemo(() => { - return [...watchedProperties]; - }, [mostRecentBotState, watchedProperties]); + return Object.entries(watchVariables).map(([key, value]) => { + return { + key, + value, + }; + }); + }, [mostRecentBotState, watchVariables]); const renderColumn = useCallback( - (item: string, index: number | undefined, column: IColumn | undefined) => { + (item: { key: string; value: string }, index: number | undefined, column: IColumn | undefined) => { if (column && index !== undefined) { if (column.key === NameColumnKey) { // render picker return ( - { - ev.persist(); - setPropertyValue(ev, index); - }} - > + ); //return {item}; } else if (column.key === ValueColumnKey) { // render the value display if (mostRecentBotState) { - const value = getValueFromBotTraceScope(item, mostRecentBotState?.activity); + const value = getValueFromBotTraceScope(item.value, mostRecentBotState?.activity); if (typeof value === 'object') { // render monaco view // TODO: is there some way we can expand the height of the cell based on the number of object keys? @@ -185,17 +226,26 @@ export const WatchTabContent: React.FC = ({ isActive } } return null; }, - [mostRecentBotState] + [mostRecentBotState, memoryVariablesPayload, watchVariables] ); const onClickAdd = useCallback(() => { - setWatchedVariables(currentProjectId ?? '', [...watchedProperties, 'placeholder']); - }, [watchedProperties]); + setWatchVariable({ + ...watchVariables, + [uuidv4()]: '', + }); + }, [watchVariables]); - const onClickRemove = useCallback(() => { - watchedProperties.pop(); - setWatchedVariables(currentProjectId ?? '', [...watchedProperties]); - }, [watchedProperties]); + const onClickRemove = () => { + const updated = produce(watchVariables, (draftState) => { + if (selectedItems?.length) { + selectedItems.map((item: IObjectWithKey) => { + delete draftState[item.key as string]; + }); + } + }); + setWatchVariable(updated); + }; if (isActive) { return ( @@ -210,6 +260,8 @@ export const WatchTabContent: React.FC = ({ isActive } columns={watchTableColumns} items={refreshedWatchedProperties} layoutMode={watchTableLayout} + selection={selection} + selectionMode={SelectionMode.multiple} onRenderItemColumn={renderColumn} />
diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx new file mode 100644 index 0000000000..95a00de62d --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { jsx } from '@emotion/core'; +import formatMessage from 'format-message'; +import React, { useMemo, useCallback, useEffect, useRef, FocusEvent, KeyboardEvent, useState, FormEvent } from 'react'; +import { TextField, ITextField } from 'office-ui-fabric-react/lib/TextField'; +import { Text } from 'office-ui-fabric-react/lib/Text'; +import debounce from 'lodash/debounce'; +import { IStackStyles, Stack } from 'office-ui-fabric-react/lib/Stack'; +import { + IContextualMenuItem, + IContextualMenuItemProps, + ContextualMenu, + DirectionalHint, +} from 'office-ui-fabric-react/lib/ContextualMenu'; +import { NeutralColors } from '@uifabric/fluent-theme'; + +import { PropertyTreeItem, PropertyItem } from './utils/components/PropertyTreeItem'; +import { useNoSearchResultMenuItem } from './utils/hooks/useNoSearchResultMenuItem'; +import { computePropertyItemTree, getAllNodes, WatchDataPayload } from './utils/helpers'; + +type WatchVariablePickerProps = { + payload: WatchDataPayload; + disabled?: boolean; + variableId: string; + path: string; + onSelectPath: (id: string, selectedPath: string) => void; +}; + +const getStrings = () => { + return { + emptyMessage: formatMessage('No properties found'), + searchPlaceholder: formatMessage('Add a property'), + }; +}; + +const defaultTreeItemHeight = 36; + +const labelContainerStyle: IStackStyles = { + root: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', height: defaultTreeItemHeight }, +}; + +export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) => { + const { payload, variableId, path, onSelectPath } = props; + const [query, setQuery] = useState(path); + const inputBoxElement = useRef(null); + const pickerContainerElement = useRef(null); + const [showContextualMenu, setShowContextualMenu] = React.useState(false); + const [items, setItems] = useState([]); + const [propertyTreeExpanded, setPropertyTreeExpanded] = React.useState>({}); + const uiStrings = useMemo(() => getStrings(), []); + + useEffect(() => { + setQuery(path); + }, [path]); + + const noSearchResultMenuItem = useNoSearchResultMenuItem(uiStrings.emptyMessage); + + const propertyTreeConfig = useMemo(() => { + const { properties } = (payload as WatchDataPayload).data; + return { root: computePropertyItemTree(properties) }; + }, [payload]); + + const getContextualMenuItems = (): IContextualMenuItem[] => { + const { root } = propertyTreeConfig; + const { nodes, levels, paths } = getAllNodes(root, { + expanded: propertyTreeExpanded, + skipRoot: true, + }); + + const onToggleExpand = (itemId: string, expanded: boolean) => { + setPropertyTreeExpanded({ ...propertyTreeExpanded, [itemId]: expanded }); + }; + + return nodes.map((node) => ({ + key: node.id, + text: node.name, + secondaryText: paths[node.id], + onClick: (event) => { + if (node.children.length) { + event?.preventDefault(); + onToggleExpand(node.id, !propertyTreeExpanded[node.id]); + } else { + const path = paths[node.id]; + setQuery(path); + event?.preventDefault(); + onHideContextualMenu(); + onSelectPath(variableId, path); + } + }, + data: { + node, + path: paths[node.id], + level: levels[node.id] - 1, + onToggleExpand, + }, + })); + }; + + const onShowContextualMenu = (event: FocusEvent) => { + event.preventDefault(); + setShowContextualMenu(true); + }; + + const onHideContextualMenu = () => { + setShowContextualMenu(false); + }; + + const flatPropertyListItems = React.useMemo(() => { + const { root } = propertyTreeConfig; + const { nodes, paths } = getAllNodes(root, { skipRoot: true }); + + return nodes.map((node) => ({ + text: node.id, + key: node.id, + secondaryText: paths[node.id], + onClick: (event) => { + event?.preventDefault(); + const path = paths[node.id]; + onSelectPath(variableId, path); + onHideContextualMenu(); + }, + data: { + node, + path: paths[node.id], + level: 0, + }, + })) as IContextualMenuItem[]; + }, [payload, propertyTreeConfig]); + + const getFilterPredicate = useCallback((q: string) => { + return (item: IContextualMenuItem) => + item.data.node.children.length === 0 && item.secondaryText?.toLowerCase().indexOf(q.toLowerCase()) !== -1; + }, []); + + const menuItems = useMemo(getContextualMenuItems, [payload, propertyTreeExpanded]); + + useEffect(() => setItems(menuItems), [menuItems]); + + const handleDebouncedSearch: () => void = useCallback( + debounce(() => { + if (query) { + const searchableItems = flatPropertyListItems; + + const predicate = getFilterPredicate(query); + + const filteredItems = searchableItems.filter(predicate); + + if (!filteredItems || !filteredItems.length) { + filteredItems.push(noSearchResultMenuItem); + } + + setItems(filteredItems); + } else { + setItems(menuItems); + } + }, 500), + [menuItems, flatPropertyListItems, noSearchResultMenuItem, query] + ); + + useEffect(() => { + handleDebouncedSearch(); + }, [menuItems, flatPropertyListItems, noSearchResultMenuItem, query]); + + const onTextBoxFocus = (event: FocusEvent) => { + onShowContextualMenu(event); + }; + + const onTextBoxKeyDown = (event: KeyboardEvent) => { + if (event.keyCode == 13) { + event.preventDefault(); + onHideContextualMenu(); + inputBoxElement.current?.blur(); + } + }; + + const onDismiss = useCallback(() => { + setPropertyTreeExpanded({}); + onHideContextualMenu(); + }, []); + + return ( +
+ , val: string | undefined) => { + setQuery(val ?? ''); + }} + onFocus={onTextBoxFocus} + onKeyDown={onTextBoxKeyDown} + /> + { + const { + item: { secondaryText: path }, + } = itemProps; + + const { onToggleExpand, level, node } = itemProps.item.data as { + node: PropertyItem; + onToggleExpand: (itemId: string, expanded: boolean) => void; + level: number; + }; + + const renderLabel = () => { + const pathNodes = (path ?? '').split('.'); + return ( + + {pathNodes.map((pathNode, idx) => ( + + {`${pathNode}${idx === pathNodes.length - 1 && node.children.length === 0 ? '' : '.'}`} + + ))} + + ); + }; + + const renderSearchResultLabel = () => ( + + {path} + + ); + + return ( + + ); + }} + delayUpdateFocusOnHover={false} + directionalHint={DirectionalHint.bottomLeftEdge} + hidden={!showContextualMenu} + items={items} + shouldFocusOnMount={false} + styles={{ + root: { + maxHeight: '200px', + overflowY: 'auto', + width: '240px', + }, + }} + target={pickerContainerElement} + onDismiss={onDismiss} + onItemClick={onHideContextualMenu} + /> +
+ ); +}); diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PropertyTreeItem.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PropertyTreeItem.tsx new file mode 100644 index 0000000000..8bf839297c --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PropertyTreeItem.tsx @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import styled from '@emotion/styled'; +import { NeutralColors } from '@uifabric/fluent-theme'; +import { Icon, IIconStyles } from 'office-ui-fabric-react/lib/Icon'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import * as React from 'react'; + +export type PropertyItem = { + id: string; + name: string; + children: PropertyItem[]; +}; + +const DEFAULT_TREE_ITEM_HEIGHT = 36; +const DEFAULT_INDENTATION_PADDING = 16; +const expandIconWidth = 16; + +const toggleExpandIconStyle: IIconStyles = { + root: { + height: DEFAULT_TREE_ITEM_HEIGHT, + width: DEFAULT_TREE_ITEM_HEIGHT, + display: 'inline-flex', + justifyContent: 'center', + alignItems: 'center', + fontSize: 8, + transition: 'background 250ms ease', + selectors: { + '&:hover': { background: NeutralColors.gray50 }, + '&:before': { + content: '""', + }, + }, + }, +}; + +const Root = styled(Stack)({ + height: DEFAULT_TREE_ITEM_HEIGHT, +}); + +const Content = styled(Stack)<{ + width: string; +}>({ flex: 1, overflow: 'hidden' }, (props) => ({ + width: props.width, +})); + +type PropertyTreeItemProps = { + item: PropertyItem; + level: number; + onRenderLabel: (item: PropertyItem) => React.ReactNode; + expanded?: boolean; + onToggleExpand?: (itemId: string, expanded: boolean) => void; +}; + +export const PropertyTreeItem = React.memo((props: PropertyTreeItemProps) => { + const { expanded = false, item, level, onToggleExpand, onRenderLabel } = props; + + const paddingLeft = level * DEFAULT_INDENTATION_PADDING; + + const toggleExpanded = React.useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + onToggleExpand?.(item.id, !expanded); + }, + [expanded, onToggleExpand, item] + ); + + const isExpandable = !!item.children?.length && onToggleExpand; + + return ( + + {isExpandable ? ( + + ) : ( +
+ )} + + {onRenderLabel(item)} + + + ); +}); diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/helpers.ts b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/helpers.ts new file mode 100644 index 0000000000..18c09a8b26 --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/helpers.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import uniq from 'lodash/uniq'; + +export type PropertyItem = { + id: string; + name: string; + children: PropertyItem[]; +}; + +export type WatchDataPayload = { + kind: string; + data: { + properties: readonly string[]; + }; +}; + +/** + * Converts the list pf properties to a tree and returns the root. + * @param properties List of available properties. + */ +export const computePropertyItemTree = (properties: readonly string[]): PropertyItem => { + // Generate random unique ids + const generateId = () => { + const arr = crypto.getRandomValues(new Uint32Array(1)); + return `${arr[0]}`; + }; + + const items = properties.slice().sort(); + const dummyRoot = { id: 'root', name: 'root', children: [] }; + + const helper = (currentNode: PropertyItem, prefix: string, scopedItems: string[], level: number) => { + const uniques = uniq(scopedItems.map((i) => i.split('.')[level])).filter(Boolean); + const children = uniques.map((name) => ({ id: generateId(), name, children: [] })); + for (const n of children) { + helper( + n, + `${prefix}${prefix ? '.' : ''}${n.name}`, + items.filter((i) => i.startsWith(`${prefix}${prefix ? '.' : ''}${n.name}`)), + level + 1 + ); + } + currentNode.children = children; + }; + + helper(dummyRoot, '', items, 0); + + return dummyRoot; +}; + +const getPath = (item: T, parents: Record) => { + const path: string[] = []; + let currentItem = item; + if (currentItem) { + while (currentItem) { + path.push(currentItem.name); + currentItem = parents[currentItem.id]; + while (currentItem && currentItem.id.indexOf('root') !== -1) { + currentItem = parents[currentItem.id]; + } + } + } + return path.reverse().join('.'); +}; + +/** + * Returns a flat list of nodes, their level by id, and the path from root to that node. + * @param root Root of the tree. + * @param options Options including current state of expanded nodes, and if the root should be skipped. + */ +export const getAllNodes = ( + root: T, + options?: Partial<{ expanded: Record; skipRoot: boolean }> +): { + nodes: T[]; + levels: Record; + paths: Record; +} => { + const nodes: T[] = []; + const levels: Record = {}; + const parents: Record = {}; + const paths: Record = {}; + + if (options?.skipRoot && options?.expanded) { + options.expanded[root.id] = true; + } + + const addNode = (node: T, parent: T | null, level = 0) => { + if (!options?.skipRoot || node.id !== root.id) { + nodes.push(node); + } + levels[node.id] = level; + if (parent) { + parents[node.id] = parent; + } + paths[node.id] = getPath(node, parents); + if (options?.expanded) { + if (!options.expanded[node.id]) { + return; + } + } + if (node?.children?.length) { + node.children.forEach((n) => addNode(n, node, level + 1)); + } + }; + + addNode(root, null); + + return { nodes, levels, paths }; +}; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/hooks/useNoSearchResultMenuItem.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/hooks/useNoSearchResultMenuItem.tsx new file mode 100644 index 0000000000..37ad7741b4 --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/hooks/useNoSearchResultMenuItem.tsx @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import formatMessage from 'format-message'; +import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu'; +import { Icon } from 'office-ui-fabric-react/lib/Icon'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import { Text } from 'office-ui-fabric-react/lib/Text'; +import * as React from 'react'; + +const searchEmptyMessageStyles = { root: { height: 32 } }; +const searchEmptyMessageTokens = { childrenGap: 8 }; + +/** + * Search empty view for contextual menu with search capability. + */ +export const useNoSearchResultMenuItem = (message?: string): IContextualMenuItem => { + message = message ?? formatMessage('no items found'); + return React.useMemo( + () => ({ + key: 'no_results', + onRender: () => ( + + + {message} + + ), + }), + [message] + ); +}; From f8ab5707df47c9093d5d5b75ba9497753bcafa09 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 16 Jun 2021 10:42:30 -0700 Subject: [PATCH 06/40] Made some of the variable names clearer --- .../WatchTab/WatchTabContent.tsx | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index a13be7a886..250860b14f 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -26,6 +26,7 @@ import { rootBotProjectIdSelector, webChatTrafficState } from '../../../../../re import { getDefaultFontSettings } from '../../../../../recoilModel/utils/fontUtil'; import { WatchVariablePicker } from '../../WatchVariableSelector/WatchVariablePicker'; import { getMemoryVariables } from '../../../../../recoilModel/dispatchers/utils/project'; +import { WatchDataPayload } from '../../WatchVariableSelector/utils/helpers'; const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); @@ -101,27 +102,25 @@ const getValueFromBotTraceScope = (delimitedProperty: string, botTrace: Activity export const WatchTabContent: React.FC = ({ isActive }) => { const currentProjectId = useRecoilValue(rootBotProjectIdSelector); const rawWebChatTraffic = useRecoilValue(webChatTrafficState(currentProjectId ?? '')); - const [watchVariables, setWatchVariable] = useState>({}); - - const [selectedItems, setSelectedItems] = useState(); + const [watchedVars, setWatchedVars] = useState>({}); + const [selectedVars, setSelectedVars] = useState(); + const [memoryVariablesPayload, setMemoryVariablesPayload] = useState({ + kind: 'property', + data: { properties: [] }, + }); - const selection = useMemo( + const watchedVarsSelection = useMemo( () => new Selection({ onSelectionChanged: () => { - //console.log('handle selection change',selection.getSelection()) - setSelectedItems(selection.getSelection()); + setSelectedVars(watchedVarsSelection.getSelection()); }, selectionMode: SelectionMode.multiple, }), [] ); - const [memoryVariablesPayload, setMemoryVariablesPayload] = useState({ - kind: 'property', - data: { properties: [] }, - }); - + // get memory scope variables for the bot useEffect(() => { if (currentProjectId) { const abortController = new AbortController(); @@ -147,23 +146,24 @@ export const WatchTabContent: React.FC = ({ isActive } const onSelectPath = useCallback( (variableId: string, path: string) => { - setWatchVariable({ - ...watchVariables, + console.log(`selecting path: ${variableId}, ${path}`); + setWatchedVars({ + ...watchedVars, [variableId]: path, }); }, - [watchVariables] + [watchedVars] ); // we need to refresh the details list every time a new bot state comes in - const refreshedWatchedProperties = useMemo(() => { - return Object.entries(watchVariables).map(([key, value]) => { + const refreshedWatchedVars = useMemo(() => { + return Object.entries(watchedVars).map(([key, value]) => { return { key, value, }; }); - }, [mostRecentBotState, watchVariables]); + }, [mostRecentBotState, watchedVars]); const renderColumn = useCallback( (item: { key: string; value: string }, index: number | undefined, column: IColumn | undefined) => { @@ -179,7 +179,6 @@ export const WatchTabContent: React.FC = ({ isActive } onSelectPath={onSelectPath} /> ); - //return {item}; } else if (column.key === ValueColumnKey) { // render the value display if (mostRecentBotState) { @@ -226,25 +225,25 @@ export const WatchTabContent: React.FC = ({ isActive } } return null; }, - [mostRecentBotState, memoryVariablesPayload, watchVariables] + [mostRecentBotState, memoryVariablesPayload, watchedVars] ); const onClickAdd = useCallback(() => { - setWatchVariable({ - ...watchVariables, + setWatchedVars({ + ...watchedVars, [uuidv4()]: '', }); - }, [watchVariables]); + }, [watchedVars]); const onClickRemove = () => { - const updated = produce(watchVariables, (draftState) => { - if (selectedItems?.length) { - selectedItems.map((item: IObjectWithKey) => { + const updated = produce(watchedVars, (draftState) => { + if (selectedVars?.length) { + selectedVars.map((item: IObjectWithKey) => { delete draftState[item.key as string]; }); } }); - setWatchVariable(updated); + setWatchedVars(updated); }; if (isActive) { @@ -258,9 +257,9 @@ export const WatchTabContent: React.FC = ({ isActive }
From 8f7781adff6e88b5f81933d70ef0bf61e3c3ac61 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 16 Jun 2021 11:13:32 -0700 Subject: [PATCH 07/40] Made table header sticky and body scroll --- .../WatchTab/WatchTabContent.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 250860b14f..efd6f4f446 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -16,6 +16,7 @@ import { SelectionMode, Selection, IObjectWithKey, + IDetailsListStyles, } from 'office-ui-fabric-react/lib/DetailsList'; import formatMessage from 'format-message'; import { JsonEditor } from '@bfc/code-editor'; @@ -40,6 +41,7 @@ const contentContainer = css` const toolbar = css` display: flex; flex-flow: row nowrap; + flex-shrink: 0; height: 24px; padding: 8px 16px; `; @@ -47,9 +49,8 @@ const toolbar = css` const content = css` display: flex; flex-flow: column nowrap; - height: 100%; + height: calc(100% - 40px); width: 100%; - overflow-y: scroll; `; const editorStyles = css` @@ -61,6 +62,21 @@ const objectCell = css` width: 360px; `; +const watchTableStyles = { + root: { + height: '100%', + selectors: { + '& > div[role="grid"]': { + height: '100%', + }, + }, + }, + contentWrapper: { + overflowY: 'auto' as 'auto', + height: 'calc(100% - 60px)', + }, +}; + const addIcon: IIconProps = { iconName: 'Add', }; @@ -261,6 +277,7 @@ export const WatchTabContent: React.FC = ({ isActive } layoutMode={watchTableLayout} selection={watchedVarsSelection} selectionMode={SelectionMode.multiple} + styles={watchTableStyles} onRenderItemColumn={renderColumn} />
From 8802fa0ad7c988b0e75646d176e400d7bcd73593 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 17 Jun 2021 10:44:29 -0700 Subject: [PATCH 08/40] Fixed some interactions with variable picker --- .../WatchTab/WatchTabContent.tsx | 52 ++++++++++++++++--- .../WatchVariablePicker.tsx | 33 +++++++++--- 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index efd6f4f446..cfb0444708 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -16,7 +16,8 @@ import { SelectionMode, Selection, IObjectWithKey, - IDetailsListStyles, + IDetailsRowProps, + DetailsRow, } from 'office-ui-fabric-react/lib/DetailsList'; import formatMessage from 'format-message'; import { JsonEditor } from '@bfc/code-editor'; @@ -62,6 +63,14 @@ const objectCell = css` width: 360px; `; +const undefinedValue = css` + font-family: Segoe UI; + font-size: 12px; + font-style: italic; + height: 16px; + line-height: 16px; +`; + const watchTableStyles = { root: { height: '100%', @@ -73,6 +82,7 @@ const watchTableStyles = { }, contentWrapper: { overflowY: 'auto' as 'auto', + // fill remaining space after table header row height: 'calc(100% - 60px)', }, }; @@ -162,7 +172,7 @@ export const WatchTabContent: React.FC = ({ isActive } const onSelectPath = useCallback( (variableId: string, path: string) => { - console.log(`selecting path: ${variableId}, ${path}`); + // TODO: if the variable path is already being watched, no-op setWatchedVars({ ...watchedVars, [variableId]: path, @@ -181,6 +191,30 @@ export const WatchTabContent: React.FC = ({ isActive } }); }, [mostRecentBotState, watchedVars]); + const renderRow = useCallback((props?: IDetailsRowProps) => { + if (props) { + return ( + div[role="checkbox"]': { + height: 32, + }, + }, + }, + root: { minHeight: 32 }, + }} + /> + ); + } + return null; + }, []); + const renderColumn = useCallback( (item: { key: string; value: string }, index: number | undefined, column: IColumn | undefined) => { if (column && index !== undefined) { @@ -199,10 +233,11 @@ export const WatchTabContent: React.FC = ({ isActive } // render the value display if (mostRecentBotState) { const value = getValueFromBotTraceScope(item.value, mostRecentBotState?.activity); - if (typeof value === 'object') { + if (value !== null && typeof value === 'object') { // render monaco view // TODO: is there some way we can expand the height of the cell based on the number of object keys? return ( + // TODO: || ?
= ({ isActive } fontWeight: 'normal', }, }} + // TODO: https://stackoverflow.com/questions/54373288/monaco-editor-hide-overview-ruler options={{ folding: true, minimap: { enabled: false, showSlider: 'mouseover' }, @@ -227,15 +263,16 @@ export const WatchTabContent: React.FC = ({ isActive }
); } else if (value === undefined) { - // don't render anything - return null; + // render undefined indicator + return undefined; } else { // render primitive view return {String(value)}; } } else { - // no bot trace available - return null; + // no bot trace available; + // render undefined indicator + return undefined; } } } @@ -279,6 +316,7 @@ export const WatchTabContent: React.FC = ({ isActive } selectionMode={SelectionMode.multiple} styles={watchTableStyles} onRenderItemColumn={renderColumn} + onRenderRow={renderRow} />
diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx index 95a00de62d..34d2f8d6f7 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx @@ -17,10 +17,14 @@ import { } from 'office-ui-fabric-react/lib/ContextualMenu'; import { NeutralColors } from '@uifabric/fluent-theme'; +import { getDefaultFontSettings } from '../../../../recoilModel/utils/fontUtil'; + import { PropertyTreeItem, PropertyItem } from './utils/components/PropertyTreeItem'; import { useNoSearchResultMenuItem } from './utils/hooks/useNoSearchResultMenuItem'; import { computePropertyItemTree, getAllNodes, WatchDataPayload } from './utils/helpers'; +const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); + type WatchVariablePickerProps = { payload: WatchDataPayload; disabled?: boolean; @@ -168,13 +172,18 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) onShowContextualMenu(event); }; - const onTextBoxKeyDown = (event: KeyboardEvent) => { - if (event.keyCode == 13) { - event.preventDefault(); - onHideContextualMenu(); - inputBoxElement.current?.blur(); - } - }; + const onTextBoxKeyDown = useCallback( + (event: KeyboardEvent) => { + // enter + if (event.keyCode == 13) { + event.preventDefault(); + onSelectPath(variableId, query); + onHideContextualMenu(); + inputBoxElement.current?.blur(); + } + }, + [variableId, query] + ); const onDismiss = useCallback(() => { setPropertyTreeExpanded({}); @@ -185,7 +194,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps)
@@ -194,6 +203,14 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) id={variableId} placeholder={uiStrings.searchPlaceholder} styles={{ + field: { + fontFamily: DEFAULT_FONT_SETTINGS.fontFamily, + fontSize: 12, + }, + fieldGroup: { + backgroundColor: 'transparent', + height: 16, + }, root: { selectors: { '.ms-TextField-fieldGroup': { From 306c7d61daf7d8cdae889949a1b90eff8a2da3b1 Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 17 Jun 2021 12:07:06 -0700 Subject: [PATCH 09/40] Refactored code and added smart height to object display --- .../WatchTab/WatchTabContent.tsx | 79 +++++-------------- .../WatchTab/WatchTabObjectValue.tsx | 58 ++++++++++++++ 2 files changed, 78 insertions(+), 59 deletions(-) create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index cfb0444708..5779cd0760 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -18,19 +18,19 @@ import { IObjectWithKey, IDetailsRowProps, DetailsRow, + IDetailsListStyles, + IDetailsRowStyles, } from 'office-ui-fabric-react/lib/DetailsList'; import formatMessage from 'format-message'; -import { JsonEditor } from '@bfc/code-editor'; import produce from 'immer'; import { DebugPanelTabHeaderProps } from '../types'; import { rootBotProjectIdSelector, webChatTrafficState } from '../../../../../recoilModel'; -import { getDefaultFontSettings } from '../../../../../recoilModel/utils/fontUtil'; import { WatchVariablePicker } from '../../WatchVariableSelector/WatchVariablePicker'; import { getMemoryVariables } from '../../../../../recoilModel/dispatchers/utils/project'; import { WatchDataPayload } from '../../WatchVariableSelector/utils/helpers'; -const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); +import { WatchTabObjectValue } from './WatchTabObjectValue'; const contentContainer = css` display: flex; @@ -54,15 +54,6 @@ const content = css` width: 100%; `; -const editorStyles = css` - border: none; -`; - -const objectCell = css` - height: 160px; - width: 360px; -`; - const undefinedValue = css` font-family: Segoe UI; font-size: 12px; @@ -71,7 +62,7 @@ const undefinedValue = css` line-height: 16px; `; -const watchTableStyles = { +const watchTableStyles: Partial = { root: { height: '100%', selectors: { @@ -87,6 +78,20 @@ const watchTableStyles = { }, }; +const rowStyles: Partial = { + cell: { minHeight: 32, padding: '8px 6px' }, + checkCell: { + height: 32, + minHeight: 32, + selectors: { + '& > div[role="checkbox"]': { + height: 32, + }, + }, + }, + root: { minHeight: 32 }, +}; + const addIcon: IIconProps = { iconName: 'Add', }; @@ -193,24 +198,7 @@ export const WatchTabContent: React.FC = ({ isActive } const renderRow = useCallback((props?: IDetailsRowProps) => { if (props) { - return ( - div[role="checkbox"]': { - height: 32, - }, - }, - }, - root: { minHeight: 32 }, - }} - /> - ); + return ; } return null; }, []); @@ -235,33 +223,7 @@ export const WatchTabContent: React.FC = ({ isActive } const value = getValueFromBotTraceScope(item.value, mostRecentBotState?.activity); if (value !== null && typeof value === 'object') { // render monaco view - // TODO: is there some way we can expand the height of the cell based on the number of object keys? - return ( - // TODO: || ? -
- null} - /> -
- ); + return ; } else if (value === undefined) { // render undefined indicator return undefined; @@ -302,7 +264,6 @@ export const WatchTabContent: React.FC = ({ isActive } if (isActive) { return (
- {/** TODO: factor toolbar and content out into own components? */}
diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx new file mode 100644 index 0000000000..3fd70e8e1a --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import { JsonEditor } from '@bfc/code-editor'; +import React, { useMemo } from 'react'; + +import { getDefaultFontSettings } from '../../../../../recoilModel/utils/fontUtil'; + +const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); + +const editorStyles = css` + border: none; +`; + +const objectCellStyle = (numLinesOfJson: number) => css` + height: ${numLinesOfJson * 18}px; + width: 360px; +`; + +type WatchTabObjectValueProps = { + value: object; +}; + +export const WatchTabObjectValue: React.FC = (props) => { + const { value } = props; + const objectCell = useMemo(() => { + const newLineMatches = JSON.stringify(value, null, 2).match(/\n/g) || []; + const numLinesOfJson = newLineMatches.length + 1; + return objectCellStyle(numLinesOfJson); + }, [value]); + + return ( +
+ null} + /> +
+ ); +}; From a74c12c13d40d0ca9a46ae4e12e309d655e80fea Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 17 Jun 2021 14:48:36 -0700 Subject: [PATCH 10/40] Refactored to make code more readable --- .../WatchVariablePicker.tsx | 123 +++++------------- .../components/PickerContextualMenuItem.tsx | 67 ++++++++++ 2 files changed, 101 insertions(+), 89 deletions(-) create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PickerContextualMenuItem.tsx diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx index 34d2f8d6f7..9a7e21e784 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx @@ -2,26 +2,19 @@ // Licensed under the MIT License. /** @jsx jsx */ -import { jsx } from '@emotion/core'; +import { css, jsx } from '@emotion/core'; import formatMessage from 'format-message'; import React, { useMemo, useCallback, useEffect, useRef, FocusEvent, KeyboardEvent, useState, FormEvent } from 'react'; -import { TextField, ITextField } from 'office-ui-fabric-react/lib/TextField'; -import { Text } from 'office-ui-fabric-react/lib/Text'; +import { TextField, ITextField, ITextFieldStyles } from 'office-ui-fabric-react/lib/TextField'; import debounce from 'lodash/debounce'; -import { IStackStyles, Stack } from 'office-ui-fabric-react/lib/Stack'; -import { - IContextualMenuItem, - IContextualMenuItemProps, - ContextualMenu, - DirectionalHint, -} from 'office-ui-fabric-react/lib/ContextualMenu'; -import { NeutralColors } from '@uifabric/fluent-theme'; +import { IContextualMenuItem, ContextualMenu, DirectionalHint } from 'office-ui-fabric-react/lib/ContextualMenu'; import { getDefaultFontSettings } from '../../../../recoilModel/utils/fontUtil'; -import { PropertyTreeItem, PropertyItem } from './utils/components/PropertyTreeItem'; +import { PropertyItem } from './utils/components/PropertyTreeItem'; import { useNoSearchResultMenuItem } from './utils/hooks/useNoSearchResultMenuItem'; import { computePropertyItemTree, getAllNodes, WatchDataPayload } from './utils/helpers'; +import { GetPickerContextualMenuItem } from './utils/components/PickerContextualMenuItem'; const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); @@ -40,12 +33,29 @@ const getStrings = () => { }; }; -const defaultTreeItemHeight = 36; - -const labelContainerStyle: IStackStyles = { - root: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', height: defaultTreeItemHeight }, +const textFieldStyles: Partial = { + field: { + fontFamily: DEFAULT_FONT_SETTINGS.fontFamily, + fontSize: 12, + }, + fieldGroup: { + backgroundColor: 'transparent', + height: 16, + }, + root: { + selectors: { + '.ms-TextField-fieldGroup': { + border: 'none', + }, + }, + }, }; +const pickerContainer = css` + margin: '0'; + width: '240px'; +`; + export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) => { const { payload, variableId, path, onSelectPath } = props; const [query, setQuery] = useState(path); @@ -190,37 +200,19 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) onHideContextualMenu(); }, []); + const contextualMenuItemRenderer = useMemo(() => { + return GetPickerContextualMenuItem(query, propertyTreeExpanded); + }, [query, propertyTreeExpanded]); + return ( -
+
, val: string | undefined) => { + onChange={(_e: FormEvent, val: string | undefined) => { setQuery(val ?? ''); }} onFocus={onTextBoxFocus} @@ -229,54 +221,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) { - const { - item: { secondaryText: path }, - } = itemProps; - - const { onToggleExpand, level, node } = itemProps.item.data as { - node: PropertyItem; - onToggleExpand: (itemId: string, expanded: boolean) => void; - level: number; - }; - - const renderLabel = () => { - const pathNodes = (path ?? '').split('.'); - return ( - - {pathNodes.map((pathNode, idx) => ( - - {`${pathNode}${idx === pathNodes.length - 1 && node.children.length === 0 ? '' : '.'}`} - - ))} - - ); - }; - - const renderSearchResultLabel = () => ( - - {path} - - ); - - return ( - - ); - }} + contextualMenuItemAs={contextualMenuItemRenderer} delayUpdateFocusOnHover={false} directionalHint={DirectionalHint.bottomLeftEdge} hidden={!showContextualMenu} diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PickerContextualMenuItem.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PickerContextualMenuItem.tsx new file mode 100644 index 0000000000..331ba83339 --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PickerContextualMenuItem.tsx @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import React from 'react'; +import { IContextualMenuItemProps } from 'office-ui-fabric-react/lib/ContextualMenu'; +import { IStackStyles, Stack } from 'office-ui-fabric-react/lib/Stack'; +import { Text } from 'office-ui-fabric-react/lib/Text'; +import { NeutralColors } from '@uifabric/fluent-theme'; + +import { PropertyItem, PropertyTreeItem } from './PropertyTreeItem'; + +const defaultTreeItemHeight = 36; + +const labelContainerStyle: IStackStyles = { + root: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', height: defaultTreeItemHeight }, +}; + +export const GetPickerContextualMenuItem = (query: string, propertyTreeExpanded: Record) => ( + itemProps: IContextualMenuItemProps +) => { + const { + item: { secondaryText: path }, + } = itemProps; + + const { onToggleExpand, level, node } = itemProps.item.data as { + node: PropertyItem; + onToggleExpand: (itemId: string, expanded: boolean) => void; + level: number; + }; + + const renderLabel = () => { + const pathNodes = (path ?? '').split('.'); + return ( + + {pathNodes.map((pathNode, idx) => ( + + {`${pathNode}${idx === pathNodes.length - 1 && node.children.length === 0 ? '' : '.'}`} + + ))} + + ); + }; + + const renderSearchResultLabel = () => ( + + {path} + + ); + + return ( + + ); +}; From 49ebc41782b6f97931b4648a3a26b376772607b8 Mon Sep 17 00:00:00 2001 From: Tony Date: Fri, 18 Jun 2021 11:14:10 -0700 Subject: [PATCH 11/40] Remove button disabled when nothing is selected --- .../TabExtensions/WatchTab/WatchTabContent.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 5779cd0760..ad520c03f7 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -261,12 +261,21 @@ export const WatchTabContent: React.FC = ({ isActive } setWatchedVars(updated); }; + const removeIsDisabled = useMemo(() => { + return selectedVars === undefined || selectedVars.length === 0; + }, [selectedVars]); + if (isActive) { return (
- +
Date: Mon, 21 Jun 2021 10:10:00 -0700 Subject: [PATCH 12/40] Added validation for already watched variables --- .../WatchTab/WatchTabContent.tsx | 29 ++++++++++++++----- .../WatchVariablePicker.tsx | 9 +++++- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index ad520c03f7..d866f5c326 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -134,6 +134,7 @@ export const WatchTabContent: React.FC = ({ isActive } const currentProjectId = useRecoilValue(rootBotProjectIdSelector); const rawWebChatTraffic = useRecoilValue(webChatTrafficState(currentProjectId ?? '')); const [watchedVars, setWatchedVars] = useState>({}); + const [pickerErrorMessages, setPickerErrorMessages] = useState>({}); const [selectedVars, setSelectedVars] = useState(); const [memoryVariablesPayload, setMemoryVariablesPayload] = useState({ kind: 'property', @@ -177,13 +178,24 @@ export const WatchTabContent: React.FC = ({ isActive } const onSelectPath = useCallback( (variableId: string, path: string) => { - // TODO: if the variable path is already being watched, no-op - setWatchedVars({ - ...watchedVars, - [variableId]: path, - }); + const watchedVar = Object.values(watchedVars).find((varPath) => varPath === path); + if (watchedVar) { + // the variable is already being watched, so display a validation error under the picker + setPickerErrorMessages({ + ...pickerErrorMessages, + [variableId]: formatMessage('You are already watching this property.'), + }); + } else { + setWatchedVars({ + ...watchedVars, + [variableId]: path, + }); + // clear any error messages for the variable + delete pickerErrorMessages[variableId]; + setPickerErrorMessages({ ...pickerErrorMessages }); + } }, - [watchedVars] + [pickerErrorMessages, watchedVars] ); // we need to refresh the details list every time a new bot state comes in @@ -194,7 +206,7 @@ export const WatchTabContent: React.FC = ({ isActive } value, }; }); - }, [mostRecentBotState, watchedVars]); + }, [mostRecentBotState, pickerErrorMessages, watchedVars]); const renderRow = useCallback((props?: IDetailsRowProps) => { if (props) { @@ -211,6 +223,7 @@ export const WatchTabContent: React.FC = ({ isActive } return ( = ({ isActive } } return null; }, - [mostRecentBotState, memoryVariablesPayload, watchedVars] + [pickerErrorMessages, mostRecentBotState, memoryVariablesPayload, watchedVars] ); const onClickAdd = useCallback(() => { diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx index 9a7e21e784..577c4f97de 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx @@ -8,6 +8,7 @@ import React, { useMemo, useCallback, useEffect, useRef, FocusEvent, KeyboardEve import { TextField, ITextField, ITextFieldStyles } from 'office-ui-fabric-react/lib/TextField'; import debounce from 'lodash/debounce'; import { IContextualMenuItem, ContextualMenu, DirectionalHint } from 'office-ui-fabric-react/lib/ContextualMenu'; +import { SharedColors } from '@uifabric/fluent-theme'; import { getDefaultFontSettings } from '../../../../recoilModel/utils/fontUtil'; @@ -21,6 +22,7 @@ const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); type WatchVariablePickerProps = { payload: WatchDataPayload; disabled?: boolean; + errorMessage?: string; variableId: string; path: string; onSelectPath: (id: string, selectedPath: string) => void; @@ -56,8 +58,12 @@ const pickerContainer = css` width: '240px'; `; +const redErrorMessage = css` + color: ${SharedColors.red20}; +`; + export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) => { - const { payload, variableId, path, onSelectPath } = props; + const { errorMessage, payload, variableId, path, onSelectPath } = props; const [query, setQuery] = useState(path); const inputBoxElement = useRef(null); const pickerContainerElement = useRef(null); @@ -238,6 +244,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) onDismiss={onDismiss} onItemClick={onHideContextualMenu} /> + {errorMessage ? {errorMessage} : null}
); }); From 8886ce8bfc7732e82a64bc17f71586ea18c7b4fa Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 21 Jun 2021 11:41:38 -0700 Subject: [PATCH 13/40] Fixed picker error styling and table col layout --- .../WatchTab/WatchTabContent.tsx | 22 ++++++++++++++++--- .../WatchVariablePicker.tsx | 9 ++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index d866f5c326..bcf2475f52 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -102,12 +102,28 @@ const removeIcon: IIconProps = { const NameColumnKey = 'column1'; const ValueColumnKey = 'column2'; +// TODO: update to office-ui-fabric-react@7.170.x to gain access to "flexGrow" column property to distribute proprotional column widths +// (name column takes up 1/3 of space and value column takes up the remaining 2/3) const watchTableColumns: IColumn[] = [ - { key: NameColumnKey, name: 'Name', fieldName: 'name', minWidth: 100, maxWidth: 200, isResizable: true }, - { key: ValueColumnKey, name: 'Value', fieldName: 'value', minWidth: 100, maxWidth: 360, isResizable: true }, + { + key: NameColumnKey, + name: 'Name', + fieldName: 'name', + minWidth: 100, + maxWidth: 600, + isResizable: true, + }, + { + key: ValueColumnKey, + name: 'Value', + fieldName: 'value', + minWidth: 100, + maxWidth: undefined, + isResizable: true, + }, ]; -const watchTableLayout: DetailsListLayoutMode = DetailsListLayoutMode.fixedColumns; +const watchTableLayout: DetailsListLayoutMode = DetailsListLayoutMode.justified; // this can be exported and used in other places const getValueFromBotTraceScope = (delimitedProperty: string, botTrace: Activity) => { diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx index 577c4f97de..2ea7c33de2 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx @@ -35,7 +35,7 @@ const getStrings = () => { }; }; -const textFieldStyles: Partial = { +const textFieldStyles = (errorMessage?: string): Partial => ({ field: { fontFamily: DEFAULT_FONT_SETTINGS.fontFamily, fontSize: 12, @@ -48,10 +48,11 @@ const textFieldStyles: Partial = { selectors: { '.ms-TextField-fieldGroup': { border: 'none', + outline: errorMessage ? `2px solid ${SharedColors.red20}` : 'none', }, }, }, -}; +}); const pickerContainer = css` margin: '0'; @@ -214,9 +215,10 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps)
, val: string | undefined) => { setQuery(val ?? ''); @@ -244,7 +246,6 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) onDismiss={onDismiss} onItemClick={onHideContextualMenu} /> - {errorMessage ? {errorMessage} : null}
); }); From 830851cb382886b7b04876313c9a125b43bc5f74 Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 21 Jun 2021 16:51:10 -0700 Subject: [PATCH 14/40] Improved the way we get values from the bot trace --- .../WatchTab/WatchTabContent.tsx | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index bcf2475f52..646073b539 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -125,25 +125,15 @@ const watchTableColumns: IColumn[] = [ const watchTableLayout: DetailsListLayoutMode = DetailsListLayoutMode.justified; -// this can be exported and used in other places +// Returns the specified property from the bot state trace if it exists. +// Ex. getValueFromBotTraceScope('user.address.city', trace) const getValueFromBotTraceScope = (delimitedProperty: string, botTrace: Activity) => { const propertySegments = delimitedProperty.split('.'); - const value = propertySegments.reduce( - (accumulator: object | string | number | boolean | undefined, segment, index) => { - // first try to grab the specified property off the root of the bot trace's memory - if (index === 0) { - return botTrace?.value[segment]; - } - // if we are not on the root, try accessing the next value of the desired property - if (typeof accumulator === 'object') { - return accumulator[segment]; - } else { - return undefined; - } - }, - undefined - ); - return value; + let returnValue = botTrace?.value; + for (const property of propertySegments) { + returnValue = returnValue?.[property]; + } + return returnValue; }; export const WatchTabContent: React.FC = ({ isActive }) => { From e640c2940d1cf579859d0adc373f4745c28a9b8d Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Wed, 23 Jun 2021 19:30:44 -0700 Subject: [PATCH 15/40] tab content Signed-off-by: Srinaath Ravichandran --- .../WatchTab/WatchTabContent.tsx | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index bcf2475f52..1d16fc12cc 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -9,6 +9,7 @@ import { v4 as uuidv4 } from 'uuid'; import { ConversationActivityTrafficItem, Activity } from '@botframework-composer/types'; import { IIconProps } from 'office-ui-fabric-react/lib/Icon'; import { CommandBarButton } from 'office-ui-fabric-react/lib/Button'; +import { Stack } from 'office-ui-fabric-react/lib/Stack'; import { DetailsList, DetailsListLayoutMode, @@ -23,6 +24,7 @@ import { } from 'office-ui-fabric-react/lib/DetailsList'; import formatMessage from 'format-message'; import produce from 'immer'; +import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane'; import { DebugPanelTabHeaderProps } from '../types'; import { rootBotProjectIdSelector, webChatTrafficState } from '../../../../../recoilModel'; @@ -32,6 +34,8 @@ import { WatchDataPayload } from '../../WatchVariableSelector/utils/helpers'; import { WatchTabObjectValue } from './WatchTabObjectValue'; +const toolbarHeight = 24; + const contentContainer = css` display: flex; flex-flow: column nowrap; @@ -64,12 +68,7 @@ const undefinedValue = css` const watchTableStyles: Partial = { root: { - height: '100%', - selectors: { - '& > div[role="grid"]': { - height: '100%', - }, - }, + maxHeight: `calc(100% - ${toolbarHeight}px)`, }, contentWrapper: { overflowY: 'auto' as 'auto', @@ -295,18 +294,30 @@ export const WatchTabContent: React.FC = ({ isActive } }, [selectedVars]); if (isActive) { - return ( -
-
- - -
-
+ + + + + + + = ({ isActive } onRenderItemColumn={renderColumn} onRenderRow={renderRow} /> -
-
- ); + + + ; } else { return null; } From e9cce1e735773daad609327b2f3ba4a1001b1220 Mon Sep 17 00:00:00 2001 From: Srinaath Ravichandran Date: Wed, 23 Jun 2021 22:47:14 -0700 Subject: [PATCH 16/40] resolve height issues Signed-off-by: Srinaath Ravichandran --- .../WatchTab/WatchTabContent.tsx | 110 +++++++++--------- 1 file changed, 52 insertions(+), 58 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 1d16fc12cc..0b6281e11d 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -25,6 +25,8 @@ import { import formatMessage from 'format-message'; import produce from 'immer'; import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane'; +import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; +import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; import { DebugPanelTabHeaderProps } from '../types'; import { rootBotProjectIdSelector, webChatTrafficState } from '../../../../../recoilModel'; @@ -36,28 +38,6 @@ import { WatchTabObjectValue } from './WatchTabObjectValue'; const toolbarHeight = 24; -const contentContainer = css` - display: flex; - flex-flow: column nowrap; - height: 100%; - width: 100%; -`; - -const toolbar = css` - display: flex; - flex-flow: row nowrap; - flex-shrink: 0; - height: 24px; - padding: 8px 16px; -`; - -const content = css` - display: flex; - flex-flow: column nowrap; - height: calc(100% - 40px); - width: 100%; -`; - const undefinedValue = css` font-family: Segoe UI; font-size: 12px; @@ -293,44 +273,58 @@ export const WatchTabContent: React.FC = ({ isActive } return selectedVars === undefined || selectedVars.length === 0; }, [selectedVars]); + function onRenderDetailsHeader(props, defaultRender) { + return ( + + {defaultRender({ + ...props, + onRenderColumnHeaderTooltip: (tooltipHostProps) => , + })} + + ); + } + if (isActive) { - - - - - - - - + + + - - - ; + + + + + + + + ); } else { return null; } From 73ce5a798e2a23c90b39a1d54a7a8fed5e574b0e Mon Sep 17 00:00:00 2001 From: Tony Date: Thu, 24 Jun 2021 10:12:18 -0700 Subject: [PATCH 17/40] Resolved a lot of PR comments --- .../WatchTab/WatchTabContent.tsx | 157 ++++++++---------- .../TabExtensions/WatchTab/index.ts | 4 - .../design/DebugPanel/TabExtensions/index.ts | 2 +- .../WatchVariablePicker.tsx | 33 ++-- .../components/PickerContextualMenuItem.tsx | 2 +- .../utils/components/PropertyTreeItem.tsx | 0 .../utils/helpers.ts | 0 .../utils/hooks/useNoSearchResultMenuItem.tsx | 0 .../src/recoilModel/dispatchers/webchat.ts | 1 - 9 files changed, 86 insertions(+), 113 deletions(-) delete mode 100644 Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/index.ts rename Composer/packages/client/src/pages/design/DebugPanel/{WatchVariableSelector => WatchVariablePicker}/WatchVariablePicker.tsx (91%) rename Composer/packages/client/src/pages/design/DebugPanel/{WatchVariableSelector => WatchVariablePicker}/utils/components/PickerContextualMenuItem.tsx (96%) rename Composer/packages/client/src/pages/design/DebugPanel/{WatchVariableSelector => WatchVariablePicker}/utils/components/PropertyTreeItem.tsx (100%) rename Composer/packages/client/src/pages/design/DebugPanel/{WatchVariableSelector => WatchVariablePicker}/utils/helpers.ts (100%) rename Composer/packages/client/src/pages/design/DebugPanel/{WatchVariableSelector => WatchVariablePicker}/utils/hooks/useNoSearchResultMenuItem.tsx (100%) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 646073b539..1156ce6574 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -3,7 +3,7 @@ /** @jsx jsx */ import { css, jsx } from '@emotion/core'; -import React, { useMemo, useCallback, useState, useEffect } from 'react'; +import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react'; import { useRecoilValue } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; import { ConversationActivityTrafficItem, Activity } from '@botframework-composer/types'; @@ -22,13 +22,13 @@ import { IDetailsRowStyles, } from 'office-ui-fabric-react/lib/DetailsList'; import formatMessage from 'format-message'; -import produce from 'immer'; +import get from 'lodash/get'; import { DebugPanelTabHeaderProps } from '../types'; import { rootBotProjectIdSelector, webChatTrafficState } from '../../../../../recoilModel'; -import { WatchVariablePicker } from '../../WatchVariableSelector/WatchVariablePicker'; +import { WatchVariablePicker } from '../../WatchVariablePicker/WatchVariablePicker'; import { getMemoryVariables } from '../../../../../recoilModel/dispatchers/utils/project'; -import { WatchDataPayload } from '../../WatchVariableSelector/utils/helpers'; +import { WatchDataPayload } from '../../WatchVariablePicker/utils/helpers'; import { WatchTabObjectValue } from './WatchTabObjectValue'; @@ -100,8 +100,8 @@ const removeIcon: IIconProps = { iconName: 'Cancel', }; -const NameColumnKey = 'column1'; -const ValueColumnKey = 'column2'; +const NameColumnKey = 'watchTabNameColumn'; +const ValueColumnKey = 'watchTabValueColumn'; // TODO: update to office-ui-fabric-react@7.170.x to gain access to "flexGrow" column property to distribute proprotional column widths // (name column takes up 1/3 of space and value column takes up the remaining 2/3) const watchTableColumns: IColumn[] = [ @@ -126,36 +126,29 @@ const watchTableColumns: IColumn[] = [ const watchTableLayout: DetailsListLayoutMode = DetailsListLayoutMode.justified; // Returns the specified property from the bot state trace if it exists. -// Ex. getValueFromBotTraceScope('user.address.city', trace) -const getValueFromBotTraceScope = (delimitedProperty: string, botTrace: Activity) => { - const propertySegments = delimitedProperty.split('.'); - let returnValue = botTrace?.value; - for (const property of propertySegments) { - returnValue = returnValue?.[property]; - } - return returnValue; +// Ex. getValueFromBotTraceMemory('user.address.city', trace) +const getValueFromBotTraceMemory = (valuePath: string, botTrace: Activity) => { + return get(botTrace?.value, valuePath, undefined); }; -export const WatchTabContent: React.FC = ({ isActive }) => { +export const WatchTabContent: React.FC = () => { const currentProjectId = useRecoilValue(rootBotProjectIdSelector); const rawWebChatTraffic = useRecoilValue(webChatTrafficState(currentProjectId ?? '')); - const [watchedVars, setWatchedVars] = useState>({}); + const [watchedVariables, setWatchedVariables] = useState>({}); const [pickerErrorMessages, setPickerErrorMessages] = useState>({}); - const [selectedVars, setSelectedVars] = useState(); + const [selectedVariables, setSelectedVariables] = useState(); const [memoryVariablesPayload, setMemoryVariablesPayload] = useState({ kind: 'property', data: { properties: [] }, }); - const watchedVarsSelection = useMemo( - () => - new Selection({ - onSelectionChanged: () => { - setSelectedVars(watchedVarsSelection.getSelection()); - }, - selectionMode: SelectionMode.multiple, - }), - [] + const watchedVariablesSelection = useRef( + new Selection({ + onSelectionChanged: () => { + setSelectedVariables(watchedVariablesSelection.current.getSelection()); + }, + selectionMode: SelectionMode.multiple, + }) ); // get memory scope variables for the bot @@ -184,7 +177,7 @@ export const WatchTabContent: React.FC = ({ isActive } const onSelectPath = useCallback( (variableId: string, path: string) => { - const watchedVar = Object.values(watchedVars).find((varPath) => varPath === path); + const watchedVar = Object.values(watchedVariables).find((varPath) => varPath === path); if (watchedVar) { // the variable is already being watched, so display a validation error under the picker setPickerErrorMessages({ @@ -192,8 +185,8 @@ export const WatchTabContent: React.FC = ({ isActive } [variableId]: formatMessage('You are already watching this property.'), }); } else { - setWatchedVars({ - ...watchedVars, + setWatchedVariables({ + ...watchedVariables, [variableId]: path, }); // clear any error messages for the variable @@ -201,24 +194,19 @@ export const WatchTabContent: React.FC = ({ isActive } setPickerErrorMessages({ ...pickerErrorMessages }); } }, - [pickerErrorMessages, watchedVars] + [pickerErrorMessages, watchedVariables] ); // we need to refresh the details list every time a new bot state comes in - const refreshedWatchedVars = useMemo(() => { - return Object.entries(watchedVars).map(([key, value]) => { - return { - key, - value, - }; - }); - }, [mostRecentBotState, pickerErrorMessages, watchedVars]); + const refreshedWatchedVariables = useMemo(() => { + return Object.entries(watchedVariables).map(([key, value]) => ({ + key, + value, + })); + }, [mostRecentBotState, pickerErrorMessages, watchedVariables]); const renderRow = useCallback((props?: IDetailsRowProps) => { - if (props) { - return ; - } - return null; + return props ? : null; }, []); const renderColumn = useCallback( @@ -239,13 +227,13 @@ export const WatchTabContent: React.FC = ({ isActive } } else if (column.key === ValueColumnKey) { // render the value display if (mostRecentBotState) { - const value = getValueFromBotTraceScope(item.value, mostRecentBotState?.activity); + const value = getValueFromBotTraceMemory(item.value, mostRecentBotState?.activity); if (value !== null && typeof value === 'object') { // render monaco view return ; } else if (value === undefined) { // render undefined indicator - return undefined; + return {formatMessage('undefined')}; } else { // render primitive view return {String(value)}; @@ -253,64 +241,59 @@ export const WatchTabContent: React.FC = ({ isActive } } else { // no bot trace available; // render undefined indicator - return undefined; + return {formatMessage('undefined')}; } } } return null; }, - [pickerErrorMessages, mostRecentBotState, memoryVariablesPayload, watchedVars] + [pickerErrorMessages, mostRecentBotState, memoryVariablesPayload, watchedVariables] ); const onClickAdd = useCallback(() => { - setWatchedVars({ - ...watchedVars, + setWatchedVariables({ + ...watchedVariables, [uuidv4()]: '', }); - }, [watchedVars]); + }, [watchedVariables]); const onClickRemove = () => { - const updated = produce(watchedVars, (draftState) => { - if (selectedVars?.length) { - selectedVars.map((item: IObjectWithKey) => { - delete draftState[item.key as string]; - }); - } - }); - setWatchedVars(updated); + const updated = { ...watchedVariables }; + if (selectedVariables?.length) { + selectedVariables.map((item: IObjectWithKey) => { + delete updated[item.key as string]; + }); + } + setWatchedVariables(updated); }; const removeIsDisabled = useMemo(() => { - return selectedVars === undefined || selectedVars.length === 0; - }, [selectedVars]); + return !selectedVariables?.length; + }, [selectedVariables]); - if (isActive) { - return ( -
-
- - -
-
- -
+ return ( +
+
+ +
- ); - } else { - return null; - } +
+ +
+
+ ); }; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/index.ts b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/index.ts deleted file mode 100644 index 11e13f2217..0000000000 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -export * from './config'; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/index.ts b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/index.ts index e541ab4376..c651bbe442 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/index.ts +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/index.ts @@ -5,7 +5,7 @@ import { TabExtensionConfig } from './types'; import { DiagnosticsTabConfig } from './DiagnosticsTab'; import { WebChatLogTabConfig } from './WebChatLog/config'; import { RuntimeOutputTabConfig } from './RuntimeOutputLog'; -import { WatchTabConfig } from './WatchTab'; +import { WatchTabConfig } from './WatchTab/config'; const implementedDebugExtensions: TabExtensionConfig[] = [ DiagnosticsTabConfig, diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx similarity index 91% rename from Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx rename to Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx index 2ea7c33de2..52d84a1550 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx @@ -15,7 +15,7 @@ import { getDefaultFontSettings } from '../../../../recoilModel/utils/fontUtil'; import { PropertyItem } from './utils/components/PropertyTreeItem'; import { useNoSearchResultMenuItem } from './utils/hooks/useNoSearchResultMenuItem'; import { computePropertyItemTree, getAllNodes, WatchDataPayload } from './utils/helpers'; -import { GetPickerContextualMenuItem } from './utils/components/PickerContextualMenuItem'; +import { getPickerContextualMenuItem } from './utils/components/PickerContextualMenuItem'; const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); @@ -59,15 +59,11 @@ const pickerContainer = css` width: '240px'; `; -const redErrorMessage = css` - color: ${SharedColors.red20}; -`; - export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) => { const { errorMessage, payload, variableId, path, onSelectPath } = props; const [query, setQuery] = useState(path); - const inputBoxElement = useRef(null); - const pickerContainerElement = useRef(null); + const inputBoxElementRef = useRef(null); + const pickerContainerElementRef = useRef(null); const [showContextualMenu, setShowContextualMenu] = React.useState(false); const [items, setItems] = useState([]); const [propertyTreeExpanded, setPropertyTreeExpanded] = React.useState>({}); @@ -84,6 +80,10 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) return { root: computePropertyItemTree(properties) }; }, [payload]); + const onHideContextualMenu = () => { + setShowContextualMenu(false); + }; + const getContextualMenuItems = (): IContextualMenuItem[] => { const { root } = propertyTreeConfig; const { nodes, levels, paths } = getAllNodes(root, { @@ -125,10 +125,6 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) setShowContextualMenu(true); }; - const onHideContextualMenu = () => { - setShowContextualMenu(false); - }; - const flatPropertyListItems = React.useMemo(() => { const { root } = propertyTreeConfig; const { nodes, paths } = getAllNodes(root, { skipRoot: true }); @@ -183,7 +179,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) useEffect(() => { handleDebouncedSearch(); - }, [menuItems, flatPropertyListItems, noSearchResultMenuItem, query]); + }, [handleDebouncedSearch]); const onTextBoxFocus = (event: FocusEvent) => { onShowContextualMenu(event); @@ -191,12 +187,11 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const onTextBoxKeyDown = useCallback( (event: KeyboardEvent) => { - // enter - if (event.keyCode == 13) { + if (event.key === 'Enter') { event.preventDefault(); onSelectPath(variableId, query); onHideContextualMenu(); - inputBoxElement.current?.blur(); + inputBoxElementRef.current?.blur(); } }, [variableId, query] @@ -208,13 +203,13 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) }, []); const contextualMenuItemRenderer = useMemo(() => { - return GetPickerContextualMenuItem(query, propertyTreeExpanded); + return getPickerContextualMenuItem(query, propertyTreeExpanded); }, [query, propertyTreeExpanded]); return ( -
+
diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PickerContextualMenuItem.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PickerContextualMenuItem.tsx similarity index 96% rename from Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PickerContextualMenuItem.tsx rename to Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PickerContextualMenuItem.tsx index 331ba83339..586db8b9fb 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PickerContextualMenuItem.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PickerContextualMenuItem.tsx @@ -15,7 +15,7 @@ const labelContainerStyle: IStackStyles = { root: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', height: defaultTreeItemHeight }, }; -export const GetPickerContextualMenuItem = (query: string, propertyTreeExpanded: Record) => ( +export const getPickerContextualMenuItem = (query: string, propertyTreeExpanded: Record) => ( itemProps: IContextualMenuItemProps ) => { const { diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PropertyTreeItem.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PropertyTreeItem.tsx similarity index 100% rename from Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/components/PropertyTreeItem.tsx rename to Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PropertyTreeItem.tsx diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/helpers.ts b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/helpers.ts similarity index 100% rename from Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/helpers.ts rename to Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/helpers.ts diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/hooks/useNoSearchResultMenuItem.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/hooks/useNoSearchResultMenuItem.tsx similarity index 100% rename from Composer/packages/client/src/pages/design/DebugPanel/WatchVariableSelector/utils/hooks/useNoSearchResultMenuItem.tsx rename to Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/hooks/useNoSearchResultMenuItem.tsx diff --git a/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts b/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts index bc7f4adf51..fb8415901d 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts @@ -18,7 +18,6 @@ export const webChatLogDispatcher = () => { const { set } = callbackHelpers; set(webChatTrafficState(projectId), []); set(webChatInspectionDataState(projectId), undefined); // clear the inspection panel - set(watchedVariablesState(projectId), []); // TODO: might not want to do this depending on how annoying it is for the user -- do you want to wipe variables when you restart convo? }); const setWebChatPanelVisibility = useRecoilCallback((callbackHelpers: CallbackInterface) => (value: boolean) => { From bb7793339cf57c60bdcf75f7649990c4c219c803 Mon Sep 17 00:00:00 2001 From: Tony Date: Fri, 25 Jun 2021 09:54:59 -0700 Subject: [PATCH 18/40] Addressed some PR comments and reworked state. --- .../WatchTab/WatchTabContent.tsx | 220 ++++++++++-------- .../WatchVariablePicker.tsx | 55 +++-- .../design/__tests__/WatchTabContent.test.tsx | 42 ++++ .../client/src/recoilModel/atoms/botState.ts | 4 +- .../src/recoilModel/dispatchers/webchat.ts | 4 +- 5 files changed, 201 insertions(+), 124 deletions(-) create mode 100644 Composer/packages/client/src/pages/design/__tests__/WatchTabContent.test.tsx diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 5db7086a64..3ee3c5fe4b 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -27,25 +27,41 @@ import get from 'lodash/get'; import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane'; import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; +import { CommunicationColors, FontSizes } from '@uifabric/fluent-theme'; import { DebugPanelTabHeaderProps } from '../types'; -import { rootBotProjectIdSelector, webChatTrafficState } from '../../../../../recoilModel'; +import { + dispatcherState, + rootBotProjectIdSelector, + watchedVariablesState, + webChatTrafficState, +} from '../../../../../recoilModel'; import { WatchVariablePicker } from '../../WatchVariablePicker/WatchVariablePicker'; import { getMemoryVariables } from '../../../../../recoilModel/dispatchers/utils/project'; import { WatchDataPayload } from '../../WatchVariablePicker/utils/helpers'; +import { getDefaultFontSettings } from '../../../../../recoilModel/utils/fontUtil'; import { WatchTabObjectValue } from './WatchTabObjectValue'; +const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); + const toolbarHeight = 24; -const undefinedValue = css` +const unavailbleValue = css` font-family: Segoe UI; - font-size: 12px; + font-size: ${FontSizes.size12}px; font-style: italic; height: 16px; line-height: 16px; `; +const primitiveValue = css` + font-family: ${DEFAULT_FONT_SETTINGS.fontFamily}; + color: ${CommunicationColors.shade10}; + height: 16px; + line-height: 16px; +`; + const watchTableStyles: Partial = { root: { maxHeight: `calc(100% - ${toolbarHeight}px)`, @@ -81,40 +97,32 @@ const removeIcon: IIconProps = { const NameColumnKey = 'watchTabNameColumn'; const ValueColumnKey = 'watchTabValueColumn'; -// TODO: update to office-ui-fabric-react@7.170.x to gain access to "flexGrow" column property to distribute proprotional column widths -// (name column takes up 1/3 of space and value column takes up the remaining 2/3) -const watchTableColumns: IColumn[] = [ - { - key: NameColumnKey, - name: 'Name', - fieldName: 'name', - minWidth: 100, - maxWidth: 600, - isResizable: true, - }, - { - key: ValueColumnKey, - name: 'Value', - fieldName: 'value', - minWidth: 100, - maxWidth: undefined, - isResizable: true, - }, -]; const watchTableLayout: DetailsListLayoutMode = DetailsListLayoutMode.justified; // Returns the specified property from the bot state trace if it exists. // Ex. getValueFromBotTraceMemory('user.address.city', trace) -const getValueFromBotTraceMemory = (valuePath: string, botTrace: Activity) => { - return get(botTrace?.value, valuePath, undefined); +export const getValueFromBotTraceMemory = ( + valuePath: string, + botTrace: Activity +): { value: any; propertyIsAvailable: boolean } => { + const pathSegments = valuePath.split('.'); + pathSegments.pop(); + const parentValuePath = pathSegments.join('.'); + const parentPropertyValue = get(botTrace?.value, parentValuePath, undefined); + return { + // if the parent key to the desired property is an object then the property is available + propertyIsAvailable: parentPropertyValue !== null && typeof parentPropertyValue === 'object', + value: get(botTrace?.value, valuePath, undefined), + }; }; -export const WatchTabContent: React.FC = () => { +export const WatchTabContent: React.FC = ({ isActive }) => { const currentProjectId = useRecoilValue(rootBotProjectIdSelector); const rawWebChatTraffic = useRecoilValue(webChatTrafficState(currentProjectId ?? '')); - const [watchedVariables, setWatchedVariables] = useState>({}); - const [pickerErrorMessages, setPickerErrorMessages] = useState>({}); + const watchedVariables = useRecoilValue(watchedVariablesState(currentProjectId ?? '')); + const { setWatchedVariables } = useRecoilValue(dispatcherState); + const [uncommittedWatchedVariables, setUncommittedWatchedVariables] = useState>({}); const [selectedVariables, setSelectedVariables] = useState(); const [memoryVariablesPayload, setMemoryVariablesPayload] = useState({ kind: 'property', @@ -154,97 +162,104 @@ export const WatchTabContent: React.FC = () => { } }, [rawWebChatTraffic]); - const onSelectPath = useCallback( - (variableId: string, path: string) => { - const watchedVar = Object.values(watchedVariables).find((varPath) => varPath === path); - if (watchedVar) { - // the variable is already being watched, so display a validation error under the picker - setPickerErrorMessages({ - ...pickerErrorMessages, - [variableId]: formatMessage('You are already watching this property.'), - }); + const onRenderVariableName = useCallback( + (item: { key: string; value: string }, index: number | undefined, column: IColumn | undefined) => { + return ( + + ); + }, + [memoryVariablesPayload] + ); + + const onRenderVariableValue = useCallback( + (item: { key: string; value: string }, index: number | undefined, column: IColumn | undefined) => { + if (mostRecentBotState) { + const variable = watchedVariables[item.key]; + if (variable === undefined) { + // the variable never passed the picker's validation so it is unavailable + return {formatMessage('unavailable')}; + } + // try to determine the value and render it accordingly + const { propertyIsAvailable, value } = getValueFromBotTraceMemory(variable, mostRecentBotState?.activity); + console.log(`${variable}: { isAvailable: ${propertyIsAvailable}, value: ${value} }`); + if (propertyIsAvailable) { + if (value !== null && typeof value === 'object') { + // render monaco view + return ; + } else if (value === undefined) { + return {formatMessage('undefined')}; + } else { + // render primitive view + return {typeof value === 'string' ? `"${value}"` : String(value)}; + } + } else { + // the value is not available + return {formatMessage('unavailable')}; + } } else { - setWatchedVariables({ - ...watchedVariables, - [variableId]: path, - }); - // clear any error messages for the variable - delete pickerErrorMessages[variableId]; - setPickerErrorMessages({ ...pickerErrorMessages }); + // no bot trace available + return {formatMessage('unavailable')}; } }, - [pickerErrorMessages, watchedVariables] + [mostRecentBotState, watchedVariables] ); - // we need to refresh the details list every time a new bot state comes in + // TODO: update to office-ui-fabric-react@7.170.x to gain access to "flexGrow" column property to distribute proprotional column widths + // (name column takes up 1/3 of space and value column takes up the remaining 2/3) + const watchTableColumns: IColumn[] = [ + { + key: NameColumnKey, + name: 'Name', + fieldName: 'name', + minWidth: 100, + maxWidth: 600, + isResizable: true, + onRender: onRenderVariableName, + }, + { + key: ValueColumnKey, + name: 'Value', + fieldName: 'value', + minWidth: 100, + maxWidth: undefined, + isResizable: true, + onRender: onRenderVariableValue, + }, + ]; + + // we need to refresh the details list when we get a new bot state, add a new row, or submit a variable to watch const refreshedWatchedVariables = useMemo(() => { - return Object.entries(watchedVariables).map(([key, value]) => ({ + return Object.entries(uncommittedWatchedVariables).map(([key, value]) => ({ key, value, })); - }, [mostRecentBotState, pickerErrorMessages, watchedVariables]); + }, [mostRecentBotState, uncommittedWatchedVariables, watchedVariables]); const renderRow = useCallback((props?: IDetailsRowProps) => { return props ? : null; }, []); - const renderColumn = useCallback( - (item: { key: string; value: string }, index: number | undefined, column: IColumn | undefined) => { - if (column && index !== undefined) { - if (column.key === NameColumnKey) { - // render picker - return ( - - ); - } else if (column.key === ValueColumnKey) { - // render the value display - if (mostRecentBotState) { - const value = getValueFromBotTraceMemory(item.value, mostRecentBotState?.activity); - if (value !== null && typeof value === 'object') { - // render monaco view - return ; - } else if (value === undefined) { - // render undefined indicator - return {formatMessage('undefined')}; - } else { - // render primitive view - return {String(value)}; - } - } else { - // no bot trace available; - // render undefined indicator - return {formatMessage('undefined')}; - } - } - } - return null; - }, - [pickerErrorMessages, mostRecentBotState, memoryVariablesPayload, watchedVariables] - ); - const onClickAdd = useCallback(() => { - setWatchedVariables({ - ...watchedVariables, + setUncommittedWatchedVariables({ + ...uncommittedWatchedVariables, [uuidv4()]: '', }); - }, [watchedVariables]); + }, [uncommittedWatchedVariables]); - const onClickRemove = () => { - const updated = { ...watchedVariables }; - if (selectedVariables?.length) { - selectedVariables.map((item: IObjectWithKey) => { - delete updated[item.key as string]; - }); + const onClickRemove = useCallback(() => { + if (currentProjectId) { + const updatedUncommitted = { ...uncommittedWatchedVariables }; + const updatedCommitted = { ...watchedVariables }; + if (selectedVariables?.length) { + selectedVariables.map((item: IObjectWithKey) => { + delete updatedUncommitted[item.key as string]; + delete updatedCommitted[item.key as string]; + }); + } + setWatchedVariables(currentProjectId, updatedCommitted); + setUncommittedWatchedVariables(updatedUncommitted); } - setWatchedVariables(updated); - }; + }, [currentProjectId, selectedVariables, setWatchedVariables, setUncommittedWatchedVariables]); const removeIsDisabled = useMemo(() => { return !selectedVariables?.length; @@ -261,6 +276,10 @@ export const WatchTabContent: React.FC = () => { ); } + if (!isActive) { + return null; + } + return ( = () => { selectionMode={SelectionMode.multiple} styles={watchTableStyles} onRenderDetailsHeader={onRenderDetailsHeader} - onRenderItemColumn={renderColumn} onRenderRow={renderRow} /> diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx index 52d84a1550..c55d2be696 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx @@ -8,9 +8,11 @@ import React, { useMemo, useCallback, useEffect, useRef, FocusEvent, KeyboardEve import { TextField, ITextField, ITextFieldStyles } from 'office-ui-fabric-react/lib/TextField'; import debounce from 'lodash/debounce'; import { IContextualMenuItem, ContextualMenu, DirectionalHint } from 'office-ui-fabric-react/lib/ContextualMenu'; -import { SharedColors } from '@uifabric/fluent-theme'; +import { FontSizes, SharedColors } from '@uifabric/fluent-theme'; +import { useRecoilValue } from 'recoil'; import { getDefaultFontSettings } from '../../../../recoilModel/utils/fontUtil'; +import { dispatcherState, rootBotProjectIdSelector, watchedVariablesState } from '../../../../recoilModel'; import { PropertyItem } from './utils/components/PropertyTreeItem'; import { useNoSearchResultMenuItem } from './utils/hooks/useNoSearchResultMenuItem'; @@ -22,10 +24,8 @@ const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); type WatchVariablePickerProps = { payload: WatchDataPayload; disabled?: boolean; - errorMessage?: string; variableId: string; path: string; - onSelectPath: (id: string, selectedPath: string) => void; }; const getStrings = () => { @@ -38,7 +38,7 @@ const getStrings = () => { const textFieldStyles = (errorMessage?: string): Partial => ({ field: { fontFamily: DEFAULT_FONT_SETTINGS.fontFamily, - fontSize: 12, + fontSize: FontSizes.size12, }, fieldGroup: { backgroundColor: 'transparent', @@ -60,7 +60,11 @@ const pickerContainer = css` `; export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) => { - const { errorMessage, payload, variableId, path, onSelectPath } = props; + const currentProjectId = useRecoilValue(rootBotProjectIdSelector); + const watchedVariables = useRecoilValue(watchedVariablesState(currentProjectId ?? '')); + const { setWatchedVariables } = useRecoilValue(dispatcherState); + const { payload, variableId, path } = props; + const [errorMessage, setErrorMessage] = useState(''); const [query, setQuery] = useState(path); const inputBoxElementRef = useRef(null); const pickerContainerElementRef = useRef(null); @@ -104,11 +108,14 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) event?.preventDefault(); onToggleExpand(node.id, !propertyTreeExpanded[node.id]); } else { - const path = paths[node.id]; - setQuery(path); - event?.preventDefault(); - onHideContextualMenu(); - onSelectPath(variableId, path); + if (currentProjectId) { + const path = paths[node.id]; + setQuery(path); + event?.preventDefault(); + setErrorMessage(''); + setWatchedVariables(currentProjectId, { ...watchedVariables, [variableId]: path }); + onHideContextualMenu(); + } } }, data: { @@ -135,9 +142,12 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) secondaryText: paths[node.id], onClick: (event) => { event?.preventDefault(); - const path = paths[node.id]; - onSelectPath(variableId, path); - onHideContextualMenu(); + if (currentProjectId) { + const path = paths[node.id]; + setErrorMessage(''); + setWatchedVariables(currentProjectId, { ...watchedVariables, [variableId]: path }); + onHideContextualMenu(); + } }, data: { node, @@ -145,7 +155,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) level: 0, }, })) as IContextualMenuItem[]; - }, [payload, propertyTreeConfig]); + }, [currentProjectId, payload, propertyTreeConfig, variableId, watchedVariables]); const getFilterPredicate = useCallback((q: string) => { return (item: IContextualMenuItem) => @@ -187,14 +197,21 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const onTextBoxKeyDown = useCallback( (event: KeyboardEvent) => { - if (event.key === 'Enter') { + if (currentProjectId && event.key === 'Enter') { event.preventDefault(); - onSelectPath(variableId, query); - onHideContextualMenu(); - inputBoxElementRef.current?.blur(); + if (Object.values(watchedVariables).find((variable) => variable === query)) { + // variable is already being watched + setErrorMessage(formatMessage('You are already watching this property.')); + } else { + // watch the variable + setErrorMessage(''); + setWatchedVariables(currentProjectId, { ...watchedVariables, [variableId]: query }); + onHideContextualMenu(); + inputBoxElementRef.current?.blur(); + } } }, - [variableId, query] + [currentProjectId, variableId, query, watchedVariables] ); const onDismiss = useCallback(() => { diff --git a/Composer/packages/client/src/pages/design/__tests__/WatchTabContent.test.tsx b/Composer/packages/client/src/pages/design/__tests__/WatchTabContent.test.tsx new file mode 100644 index 0000000000..5eaa28c4ea --- /dev/null +++ b/Composer/packages/client/src/pages/design/__tests__/WatchTabContent.test.tsx @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { getValueFromBotTraceMemory } from '../DebugPanel/TabExtensions/WatchTab/WatchTabContent'; + +describe('', () => { + describe('getValueFromBotTraceMemory', () => { + const botTrace: any = { + // memory scopes + value: { + user: { + outer: { + inner: { + someNum: 123, + someString: 'blah', + }, + }, + }, + }, + }; + it("should get an existing property from the bot trace's memory scope", () => { + const result = getValueFromBotTraceMemory('user.outer.inner.someNum', botTrace); + + expect(result.propertyIsAvailable).toBe(true); + expect(result.value).toBe(123); + }); + + it('should be able to tell if the property does not exist', () => { + const result = getValueFromBotTraceMemory('user.outer.nonExistent.someProp', botTrace); + + expect(result.propertyIsAvailable).toBe(false); + expect(result.value).toBe(undefined); + }); + + it('should get an undefined property on an existing key', () => { + const result = getValueFromBotTraceMemory('user.outer.inner.someProp', botTrace); + + expect(result.propertyIsAvailable).toBe(true); + expect(result.value).toBe(undefined); + }); + }); +}); diff --git a/Composer/packages/client/src/recoilModel/atoms/botState.ts b/Composer/packages/client/src/recoilModel/atoms/botState.ts index b7d77b7437..5f5a8b3519 100644 --- a/Composer/packages/client/src/recoilModel/atoms/botState.ts +++ b/Composer/packages/client/src/recoilModel/atoms/botState.ts @@ -447,9 +447,9 @@ export const projectIndexingState = atomFamily({ default: false, }); -export const watchedVariablesState = atomFamily({ +export const watchedVariablesState = atomFamily, string>({ key: getFullyQualifiedKey('watchedVariables'), - default: [], + default: {}, }); export const runtimeStandardOutputDataState = atomFamily({ diff --git a/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts b/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts index fb8415901d..f62cab07fc 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/webchat.ts @@ -49,9 +49,9 @@ export const webChatLogDispatcher = () => { ); const setWatchedVariables = useRecoilCallback( - (callbackHelpers: CallbackInterface) => (projectId: string, variables: string[]) => { + (callbackHelpers: CallbackInterface) => (projectId: string, variables: Record) => { const { set } = callbackHelpers; - set(watchedVariablesState(projectId), [...variables]); + set(watchedVariablesState(projectId), variables); } ); From 290ae39fcbc610e21224a4ab260bc311eedc955c Mon Sep 17 00:00:00 2001 From: Tony Date: Fri, 25 Jun 2021 10:29:52 -0700 Subject: [PATCH 19/40] Watched variables are now hidden from the picker --- .../DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx | 8 ++++++-- .../WatchVariablePicker/WatchVariablePicker.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 3ee3c5fe4b..fe3355123c 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -144,14 +144,18 @@ export const WatchTabContent: React.FC = ({ isActive } const abortController = new AbortController(); (async () => { try { - const variables = await getMemoryVariables(currentProjectId, { signal: abortController.signal }); + const watched = Object.values(watchedVariables); + let variables = await getMemoryVariables(currentProjectId, { signal: abortController.signal }); + // we don't want to show variables that are already being watched + variables = variables.filter((v) => !watched.find((watchedV) => watchedV === v)); + setMemoryVariablesPayload({ kind: 'property', data: { properties: variables } }); } catch (e) { // error can be due to abort } })(); } - }, [currentProjectId]); + }, [currentProjectId, watchedVariables]); const mostRecentBotState = useMemo(() => { const botStateTraffic = rawWebChatTraffic.filter( diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx index c55d2be696..4c55591e70 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx @@ -80,7 +80,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const noSearchResultMenuItem = useNoSearchResultMenuItem(uiStrings.emptyMessage); const propertyTreeConfig = useMemo(() => { - const { properties } = (payload as WatchDataPayload).data; + const { properties } = payload.data; return { root: computePropertyItemTree(properties) }; }, [payload]); From 250f37d64509df2280fc9741e2639d3a2897bcba Mon Sep 17 00:00:00 2001 From: Tony Date: Fri, 25 Jun 2021 11:17:01 -0700 Subject: [PATCH 20/40] Fixed telemetry type --- Composer/packages/types/src/telemetry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/types/src/telemetry.ts b/Composer/packages/types/src/telemetry.ts index 7860f2d3e2..2333d61994 100644 --- a/Composer/packages/types/src/telemetry.ts +++ b/Composer/packages/types/src/telemetry.ts @@ -193,7 +193,7 @@ type WebChatEvents = { WebChatConversationRestarted: { restartType: 'SameUserId' | 'NewUserId' }; DrawerPaneOpened: undefined; DrawerPaneClosed: undefined; - DrawerPaneTabOpened: { tabType: 'Diagnostics' | 'WebChatInspector' | 'RuntimeLog' }; + DrawerPaneTabOpened: { tabType: 'Diagnostics' | 'WebChatInspector' | 'RuntimeLog' | 'Watch' }; SaveTranscriptClicked: undefined; }; From 9488e5304d22292445ad546c1ca67fcf34660e55 Mon Sep 17 00:00:00 2001 From: Tony Date: Fri, 25 Jun 2021 16:24:32 -0700 Subject: [PATCH 21/40] Added tests for watch tab --- .../WatchTab/WatchTabContent.tsx | 1 - .../design/__tests__/WatchTabContent.test.tsx | 53 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index fe3355123c..e5b48e6032 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -185,7 +185,6 @@ export const WatchTabContent: React.FC = ({ isActive } } // try to determine the value and render it accordingly const { propertyIsAvailable, value } = getValueFromBotTraceMemory(variable, mostRecentBotState?.activity); - console.log(`${variable}: { isAvailable: ${propertyIsAvailable}, value: ${value} }`); if (propertyIsAvailable) { if (value !== null && typeof value === 'object') { // render monaco view diff --git a/Composer/packages/client/src/pages/design/__tests__/WatchTabContent.test.tsx b/Composer/packages/client/src/pages/design/__tests__/WatchTabContent.test.tsx index 5eaa28c4ea..8632d655c7 100644 --- a/Composer/packages/client/src/pages/design/__tests__/WatchTabContent.test.tsx +++ b/Composer/packages/client/src/pages/design/__tests__/WatchTabContent.test.tsx @@ -1,9 +1,60 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { getValueFromBotTraceMemory } from '../DebugPanel/TabExtensions/WatchTab/WatchTabContent'; +import { act, fireEvent } from '@botframework-composer/test-utils'; +import * as React from 'react'; + +import { renderWithRecoil } from '../../../../__tests__/testUtils/renderWithRecoil'; +import { botProjectIdsState, projectMetaDataState } from '../../../recoilModel'; +import { getValueFromBotTraceMemory, WatchTabContent } from '../DebugPanel/TabExtensions/WatchTab/WatchTabContent'; describe('', () => { + describe('rendering', () => { + it('should add a row to the table', async () => { + const { findByText } = renderWithRecoil(, () => {}); + const addButton = await findByText('Add property'); + + act(() => { + fireEvent.click(addButton); + }); + + // value for unset watched variable + await findByText('unavailable'); + }); + + it('should remove a row from the table', async () => { + const rootBotId = '123-adc'; + const { findByText, queryByText } = renderWithRecoil(, ({ set }) => { + set(botProjectIdsState, [rootBotId]); + set(projectMetaDataState(rootBotId), { + isRootBot: true, + isRemote: false, + }); + }); + const addButton = await findByText('Add property'); + const removeButton = await findByText('Remove from list'); + + // add a new row + act(() => { + fireEvent.click(addButton); + }); + + // select the row + await act(async () => { + const newRow = await findByText('unavailable'); + fireEvent.click(newRow); + }); + + // remove the row + await act(async () => { + fireEvent.click(removeButton); + }); + + const nonexistentRow = queryByText('unavailable'); + expect(nonexistentRow).toBeNull(); + }); + }); + describe('getValueFromBotTraceMemory', () => { const botTrace: any = { // memory scopes From b52a21853827d31f6f8990ee9c6c8ef88e200570 Mon Sep 17 00:00:00 2001 From: Tony Date: Fri, 25 Jun 2021 16:26:38 -0700 Subject: [PATCH 22/40] PR comments --- .../DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx index 4c55591e70..788a353d85 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx @@ -169,11 +169,9 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const handleDebouncedSearch: () => void = useCallback( debounce(() => { if (query) { - const searchableItems = flatPropertyListItems; - const predicate = getFilterPredicate(query); - const filteredItems = searchableItems.filter(predicate); + const filteredItems = flatPropertyListItems.filter(predicate); if (!filteredItems || !filteredItems.length) { filteredItems.push(noSearchResultMenuItem); From d559423f5f68b39a278b98ecf978e87c59090be5 Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 28 Jun 2021 11:18:00 -0700 Subject: [PATCH 23/40] Addressed more PR comments --- .../WatchTab/WatchTabContent.tsx | 53 +++++++++++-------- .../WatchVariablePicker.tsx | 4 +- .../components/PickerContextualMenuItem.tsx | 5 +- .../utils/components/PropertyTreeItem.tsx | 3 +- .../utils/components/constants.ts | 4 ++ 5 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/constants.ts diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index e5b48e6032..6aba309933 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -67,7 +67,7 @@ const watchTableStyles: Partial = { maxHeight: `calc(100% - ${toolbarHeight}px)`, }, contentWrapper: { - overflowY: 'auto' as 'auto', + overflowY: 'auto' as any, // fill remaining space after table header row height: 'calc(100% - 60px)', }, @@ -154,6 +154,10 @@ export const WatchTabContent: React.FC = ({ isActive } // error can be due to abort } })(); + + return () => { + abortController.abort(); + }; } }, [currentProjectId, watchedVariables]); @@ -209,26 +213,29 @@ export const WatchTabContent: React.FC = ({ isActive } // TODO: update to office-ui-fabric-react@7.170.x to gain access to "flexGrow" column property to distribute proprotional column widths // (name column takes up 1/3 of space and value column takes up the remaining 2/3) - const watchTableColumns: IColumn[] = [ - { - key: NameColumnKey, - name: 'Name', - fieldName: 'name', - minWidth: 100, - maxWidth: 600, - isResizable: true, - onRender: onRenderVariableName, - }, - { - key: ValueColumnKey, - name: 'Value', - fieldName: 'value', - minWidth: 100, - maxWidth: undefined, - isResizable: true, - onRender: onRenderVariableValue, - }, - ]; + const watchTableColumns: IColumn[] = useMemo( + () => [ + { + key: NameColumnKey, + name: 'Name', + fieldName: 'name', + minWidth: 100, + maxWidth: 600, + isResizable: true, + onRender: onRenderVariableName, + }, + { + key: ValueColumnKey, + name: 'Value', + fieldName: 'value', + minWidth: 100, + maxWidth: undefined, + isResizable: true, + onRender: onRenderVariableValue, + }, + ], + [onRenderVariableName] + ); // we need to refresh the details list when we get a new bot state, add a new row, or submit a variable to watch const refreshedWatchedVariables = useMemo(() => { @@ -268,7 +275,7 @@ export const WatchTabContent: React.FC = ({ isActive } return !selectedVariables?.length; }, [selectedVariables]); - function onRenderDetailsHeader(props, defaultRender) { + const onRenderDetailsHeader = (props, defaultRender) => { return ( {defaultRender({ @@ -277,7 +284,7 @@ export const WatchTabContent: React.FC = ({ isActive } })} ); - } + }; if (!isActive) { return null; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx index 788a353d85..d4dcb5f742 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx @@ -155,7 +155,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) level: 0, }, })) as IContextualMenuItem[]; - }, [currentProjectId, payload, propertyTreeConfig, variableId, watchedVariables]); + }, [currentProjectId, onHideContextualMenu, payload, propertyTreeConfig, variableId, watchedVariables]); const getFilterPredicate = useCallback((q: string) => { return (item: IContextualMenuItem) => @@ -215,7 +215,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const onDismiss = useCallback(() => { setPropertyTreeExpanded({}); onHideContextualMenu(); - }, []); + }, [onHideContextualMenu]); const contextualMenuItemRenderer = useMemo(() => { return getPickerContextualMenuItem(query, propertyTreeExpanded); diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PickerContextualMenuItem.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PickerContextualMenuItem.tsx index 586db8b9fb..e97cc7fb04 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PickerContextualMenuItem.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PickerContextualMenuItem.tsx @@ -8,11 +8,10 @@ import { Text } from 'office-ui-fabric-react/lib/Text'; import { NeutralColors } from '@uifabric/fluent-theme'; import { PropertyItem, PropertyTreeItem } from './PropertyTreeItem'; - -const defaultTreeItemHeight = 36; +import { DEFAULT_TREE_ITEM_HEIGHT } from './constants'; const labelContainerStyle: IStackStyles = { - root: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', height: defaultTreeItemHeight }, + root: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', height: DEFAULT_TREE_ITEM_HEIGHT }, }; export const getPickerContextualMenuItem = (query: string, propertyTreeExpanded: Record) => ( diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PropertyTreeItem.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PropertyTreeItem.tsx index 8bf839297c..3e034aad2f 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PropertyTreeItem.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/PropertyTreeItem.tsx @@ -7,13 +7,14 @@ import { Icon, IIconStyles } from 'office-ui-fabric-react/lib/Icon'; import { Stack } from 'office-ui-fabric-react/lib/Stack'; import * as React from 'react'; +import { DEFAULT_TREE_ITEM_HEIGHT } from './constants'; + export type PropertyItem = { id: string; name: string; children: PropertyItem[]; }; -const DEFAULT_TREE_ITEM_HEIGHT = 36; const DEFAULT_INDENTATION_PADDING = 16; const expandIconWidth = 16; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/constants.ts b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/constants.ts new file mode 100644 index 0000000000..63cc766b2e --- /dev/null +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/components/constants.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export const DEFAULT_TREE_ITEM_HEIGHT = 36; From 4b00eb2aedd1f07337a6972c1a280d6a0a6dfd25 Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 28 Jun 2021 14:54:44 -0700 Subject: [PATCH 24/40] More PR comment changes --- .../WatchTab/WatchTabContent.tsx | 70 ++++++++++--------- .../WatchTab/WatchTabObjectValue.tsx | 14 ++-- .../WatchVariablePicker.tsx | 56 +++++++-------- 3 files changed, 69 insertions(+), 71 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 6aba309933..87ba2f65c8 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -8,8 +8,8 @@ import { useRecoilValue } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; import { ConversationActivityTrafficItem, Activity } from '@botframework-composer/types'; import { IIconProps } from 'office-ui-fabric-react/lib/Icon'; -import { CommandBarButton } from 'office-ui-fabric-react/lib/Button'; import { Stack } from 'office-ui-fabric-react/lib/Stack'; +import { CommandBar, ICommandBarStyles } from 'office-ui-fabric-react/lib/CommandBar'; import { DetailsList, DetailsListLayoutMode, @@ -21,13 +21,14 @@ import { DetailsRow, IDetailsListStyles, IDetailsRowStyles, + IDetailsHeaderStyles, } from 'office-ui-fabric-react/lib/DetailsList'; import formatMessage from 'format-message'; import get from 'lodash/get'; import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane'; import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; -import { CommunicationColors, FontSizes } from '@uifabric/fluent-theme'; +import { CommunicationColors, FluentTheme } from '@uifabric/fluent-theme'; import { DebugPanelTabHeaderProps } from '../types'; import { @@ -39,24 +40,22 @@ import { import { WatchVariablePicker } from '../../WatchVariablePicker/WatchVariablePicker'; import { getMemoryVariables } from '../../../../../recoilModel/dispatchers/utils/project'; import { WatchDataPayload } from '../../WatchVariablePicker/utils/helpers'; -import { getDefaultFontSettings } from '../../../../../recoilModel/utils/fontUtil'; import { WatchTabObjectValue } from './WatchTabObjectValue'; -const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); - const toolbarHeight = 24; const unavailbleValue = css` - font-family: Segoe UI; - font-size: ${FontSizes.size12}px; + font-family: ${FluentTheme.fonts.small.fontFamily}; + font-size: ${FluentTheme.fonts.small.fontSize}; font-style: italic; height: 16px; line-height: 16px; `; const primitiveValue = css` - font-family: ${DEFAULT_FONT_SETTINGS.fontFamily}; + font-family: ${FluentTheme.fonts.small.fontFamily}; + font-size: ${FluentTheme.fonts.small.fontSize}; color: ${CommunicationColors.shade10}; height: 16px; line-height: 16px; @@ -87,12 +86,11 @@ const rowStyles: Partial = { root: { minHeight: 32 }, }; -const addIcon: IIconProps = { - iconName: 'Add', -}; - -const removeIcon: IIconProps = { - iconName: 'Cancel', +const commandBarStyles: Partial = { root: { height: toolbarHeight, padding: 0 } }; +const detailsHeaderStyles: Partial = { + root: { + paddingTop: 0, + }, }; const NameColumnKey = 'watchTabNameColumn'; @@ -172,9 +170,7 @@ export const WatchTabContent: React.FC = ({ isActive } const onRenderVariableName = useCallback( (item: { key: string; value: string }, index: number | undefined, column: IColumn | undefined) => { - return ( - - ); + return ; }, [memoryVariablesPayload] ); @@ -185,7 +181,7 @@ export const WatchTabContent: React.FC = ({ isActive } const variable = watchedVariables[item.key]; if (variable === undefined) { // the variable never passed the picker's validation so it is unavailable - return {formatMessage('unavailable')}; + return {formatMessage('not available')}; } // try to determine the value and render it accordingly const { propertyIsAvailable, value } = getValueFromBotTraceMemory(variable, mostRecentBotState?.activity); @@ -201,11 +197,11 @@ export const WatchTabContent: React.FC = ({ isActive } } } else { // the value is not available - return {formatMessage('unavailable')}; + return {formatMessage('not available')}; } } else { // no bot trace available - return {formatMessage('unavailable')}; + return {formatMessage('not available')}; } }, [mostRecentBotState, watchedVariables] @@ -234,7 +230,7 @@ export const WatchTabContent: React.FC = ({ isActive } onRender: onRenderVariableValue, }, ], - [onRenderVariableName] + [onRenderVariableName, onRenderVariableValue] ); // we need to refresh the details list when we get a new bot state, add a new row, or submit a variable to watch @@ -281,6 +277,7 @@ export const WatchTabContent: React.FC = ({ isActive } {defaultRender({ ...props, onRenderColumnHeaderTooltip: (tooltipHostProps) => , + styles: detailsHeaderStyles, })} ); @@ -292,22 +289,29 @@ export const WatchTabContent: React.FC = ({ isActive } return ( - - - - + items={[ + { + key: 'addProperty', + text: formatMessage('Add property'), + iconProps: { iconName: 'Add' }, + onClick: onClickAdd, + }, + { + disabled: removeIsDisabled, + key: 'removeProperty', + text: formatMessage('Remove from list'), + iconProps: { iconName: 'Cancel' }, + onClick: onClickRemove, + }, + ]} + styles={commandBarStyles} + /> css` `; type WatchTabObjectValueProps = { - value: object; + value: any; }; export const WatchTabObjectValue: React.FC = (props) => { const { value } = props; + const userSettings = useRecoilValue(userSettingsState); const objectCell = useMemo(() => { const newLineMatches = JSON.stringify(value, null, 2).match(/\n/g) || []; const numLinesOfJson = newLineMatches.length + 1; @@ -36,11 +36,7 @@ export const WatchTabObjectValue: React.FC = (props) = { return { emptyMessage: formatMessage('No properties found'), - searchPlaceholder: formatMessage('Add a property'), + searchPlaceholder: formatMessage('Add property path to watch'), }; }; @@ -63,9 +62,9 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const currentProjectId = useRecoilValue(rootBotProjectIdSelector); const watchedVariables = useRecoilValue(watchedVariablesState(currentProjectId ?? '')); const { setWatchedVariables } = useRecoilValue(dispatcherState); - const { payload, variableId, path } = props; + const { payload, variableId } = props; const [errorMessage, setErrorMessage] = useState(''); - const [query, setQuery] = useState(path); + const [query, setQuery] = useState(''); const inputBoxElementRef = useRef(null); const pickerContainerElementRef = useRef(null); const [showContextualMenu, setShowContextualMenu] = React.useState(false); @@ -73,10 +72,6 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const [propertyTreeExpanded, setPropertyTreeExpanded] = React.useState>({}); const uiStrings = useMemo(() => getStrings(), []); - useEffect(() => { - setQuery(path); - }, [path]); - const noSearchResultMenuItem = useNoSearchResultMenuItem(uiStrings.emptyMessage); const propertyTreeConfig = useMemo(() => { @@ -166,28 +161,33 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) useEffect(() => setItems(menuItems), [menuItems]); - const handleDebouncedSearch: () => void = useCallback( - debounce(() => { - if (query) { - const predicate = getFilterPredicate(query); + const performDebouncedSearch = useMemo( + () => + debounce((passedQuery?: string) => { + if (passedQuery) { + const predicate = getFilterPredicate(passedQuery); - const filteredItems = flatPropertyListItems.filter(predicate); + const filteredItems = flatPropertyListItems.filter(predicate); - if (!filteredItems || !filteredItems.length) { - filteredItems.push(noSearchResultMenuItem); - } + if (!filteredItems || !filteredItems.length) { + filteredItems.push(noSearchResultMenuItem); + } - setItems(filteredItems); - } else { - setItems(menuItems); - } - }, 500), - [menuItems, flatPropertyListItems, noSearchResultMenuItem, query] + setItems(filteredItems); + } else { + setItems(menuItems); + } + }, 500), + [getFilterPredicate, menuItems] ); - useEffect(() => { - handleDebouncedSearch(); - }, [handleDebouncedSearch]); + const onInputChange = useCallback( + (_e: FormEvent, val: string | undefined) => { + setQuery(val ?? ''); + performDebouncedSearch(val); + }, + [performDebouncedSearch] + ); const onTextBoxFocus = (event: FocusEvent) => { onShowContextualMenu(event); @@ -209,7 +209,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) } } }, - [currentProjectId, variableId, query, watchedVariables] + [currentProjectId, onHideContextualMenu, variableId, query, watchedVariables] ); const onDismiss = useCallback(() => { @@ -230,9 +230,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) placeholder={uiStrings.searchPlaceholder} styles={textFieldStyles(errorMessage)} value={query} - onChange={(_e: FormEvent, val: string | undefined) => { - setQuery(val ?? ''); - }} + onChange={onInputChange} onFocus={onTextBoxFocus} onKeyDown={onTextBoxKeyDown} /> From 8abe76be92310bf27edfd51fc26ed83074199cec Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 28 Jun 2021 17:23:46 -0700 Subject: [PATCH 25/40] Fixed state persistence --- .../TabExtensions/WatchTab/WatchTabContent.tsx | 16 +++++++++++----- .../WatchVariablePicker/WatchVariablePicker.tsx | 7 ++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 87ba2f65c8..13c61e6685 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -170,7 +170,9 @@ export const WatchTabContent: React.FC = ({ isActive } const onRenderVariableName = useCallback( (item: { key: string; value: string }, index: number | undefined, column: IColumn | undefined) => { - return ; + return ( + + ); }, [memoryVariablesPayload] ); @@ -235,10 +237,14 @@ export const WatchTabContent: React.FC = ({ isActive } // we need to refresh the details list when we get a new bot state, add a new row, or submit a variable to watch const refreshedWatchedVariables = useMemo(() => { - return Object.entries(uncommittedWatchedVariables).map(([key, value]) => ({ - key, - value, - })); + // merge any committed variables into the uncommitted list so that state + // is saved when the user collapses the panel or switches to another tab + return Object.entries(uncommittedWatchedVariables).map(([key, value]) => { + return { + key, + value: watchedVariables[key] ?? value, + }; + }); }, [mostRecentBotState, uncommittedWatchedVariables, watchedVariables]); const renderRow = useCallback((props?: IDetailsRowProps) => { diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx index 935ea95530..757711fea2 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx @@ -22,6 +22,7 @@ import { getPickerContextualMenuItem } from './utils/components/PickerContextual const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); type WatchVariablePickerProps = { + path: string; payload: WatchDataPayload; disabled?: boolean; variableId: string; @@ -62,9 +63,9 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const currentProjectId = useRecoilValue(rootBotProjectIdSelector); const watchedVariables = useRecoilValue(watchedVariablesState(currentProjectId ?? '')); const { setWatchedVariables } = useRecoilValue(dispatcherState); - const { payload, variableId } = props; + const { path, payload, variableId } = props; const [errorMessage, setErrorMessage] = useState(''); - const [query, setQuery] = useState(''); + const [query, setQuery] = useState(null); const inputBoxElementRef = useRef(null); const pickerContainerElementRef = useRef(null); const [showContextualMenu, setShowContextualMenu] = React.useState(false); @@ -229,7 +230,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) id={variableId} placeholder={uiStrings.searchPlaceholder} styles={textFieldStyles(errorMessage)} - value={query} + value={query ?? path} onChange={onInputChange} onFocus={onTextBoxFocus} onKeyDown={onTextBoxKeyDown} From 2b9e51dd23cf32836208ea007d5fc2e6c9782504 Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 28 Jun 2021 17:30:46 -0700 Subject: [PATCH 26/40] Removed 'not available' message from empty state --- .../DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 13c61e6685..5d99de16a3 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -182,8 +182,8 @@ export const WatchTabContent: React.FC = ({ isActive } if (mostRecentBotState) { const variable = watchedVariables[item.key]; if (variable === undefined) { - // the variable never passed the picker's validation so it is unavailable - return {formatMessage('not available')}; + // the variable has not been committed yet + return null; } // try to determine the value and render it accordingly const { propertyIsAvailable, value } = getValueFromBotTraceMemory(variable, mostRecentBotState?.activity); From 8ae35f5db3f5f52e26a3fc10025edf432ad8bd8f Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 08:51:08 -0700 Subject: [PATCH 27/40] Added max height to object view in table --- .../DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx index 03890b4bb2..bea7da5f12 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx @@ -13,8 +13,10 @@ const editorStyles = css` border: none; `; +const maxJsonHeight = 80; + const objectCellStyle = (numLinesOfJson: number) => css` - height: ${numLinesOfJson * 18}px; + height: ${Math.min(numLinesOfJson * 18, maxJsonHeight)}px; width: 360px; `; From 87adbf20628775ec24b4953b6be64dffb5c94665 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 09:23:42 -0700 Subject: [PATCH 28/40] State now resets when opening a new project --- .../WatchTab/WatchTabContent.tsx | 72 ++++++++++--------- .../src/recoilModel/dispatchers/project.ts | 4 +- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 5d99de16a3..ce71ebe4e4 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -7,7 +7,6 @@ import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react' import { useRecoilValue } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; import { ConversationActivityTrafficItem, Activity } from '@botframework-composer/types'; -import { IIconProps } from 'office-ui-fabric-react/lib/Icon'; import { Stack } from 'office-ui-fabric-react/lib/Stack'; import { CommandBar, ICommandBarStyles } from 'office-ui-fabric-react/lib/CommandBar'; import { @@ -32,8 +31,8 @@ import { CommunicationColors, FluentTheme } from '@uifabric/fluent-theme'; import { DebugPanelTabHeaderProps } from '../types'; import { + currentProjectIdState, dispatcherState, - rootBotProjectIdSelector, watchedVariablesState, webChatTrafficState, } from '../../../../../recoilModel'; @@ -116,9 +115,9 @@ export const getValueFromBotTraceMemory = ( }; export const WatchTabContent: React.FC = ({ isActive }) => { - const currentProjectId = useRecoilValue(rootBotProjectIdSelector); - const rawWebChatTraffic = useRecoilValue(webChatTrafficState(currentProjectId ?? '')); - const watchedVariables = useRecoilValue(watchedVariablesState(currentProjectId ?? '')); + const currentProjectId = useRecoilValue(currentProjectIdState); + const rawWebChatTraffic = useRecoilValue(webChatTrafficState(currentProjectId)); + const watchedVariables = useRecoilValue(watchedVariablesState(currentProjectId)); const { setWatchedVariables } = useRecoilValue(dispatcherState); const [uncommittedWatchedVariables, setUncommittedWatchedVariables] = useState>({}); const [selectedVariables, setSelectedVariables] = useState(); @@ -136,27 +135,30 @@ export const WatchTabContent: React.FC = ({ isActive } }) ); + // reset state when switching to a new project + useEffect(() => { + setUncommittedWatchedVariables([]); + }, [currentProjectId]); + // get memory scope variables for the bot useEffect(() => { - if (currentProjectId) { - const abortController = new AbortController(); - (async () => { - try { - const watched = Object.values(watchedVariables); - let variables = await getMemoryVariables(currentProjectId, { signal: abortController.signal }); - // we don't want to show variables that are already being watched - variables = variables.filter((v) => !watched.find((watchedV) => watchedV === v)); + const abortController = new AbortController(); + (async () => { + try { + const watched = Object.values(watchedVariables); + let variables = await getMemoryVariables(currentProjectId, { signal: abortController.signal }); + // we don't want to show variables that are already being watched + variables = variables.filter((v) => !watched.find((watchedV) => watchedV === v)); - setMemoryVariablesPayload({ kind: 'property', data: { properties: variables } }); - } catch (e) { - // error can be due to abort - } - })(); + setMemoryVariablesPayload({ kind: 'property', data: { properties: variables } }); + } catch (e) { + // error can be due to abort + } + })(); - return () => { - abortController.abort(); - }; - } + return () => { + abortController.abort(); + }; }, [currentProjectId, watchedVariables]); const mostRecentBotState = useMemo(() => { @@ -202,8 +204,10 @@ export const WatchTabContent: React.FC = ({ isActive } return {formatMessage('not available')}; } } else { - // no bot trace available - return {formatMessage('not available')}; + // no bot trace available - render "not available" for committed variables, and nothing for uncommitted variables + return watchedVariables[item.key] !== undefined ? ( + {formatMessage('not available')} + ) : null; } }, [mostRecentBotState, watchedVariables] @@ -259,18 +263,16 @@ export const WatchTabContent: React.FC = ({ isActive } }, [uncommittedWatchedVariables]); const onClickRemove = useCallback(() => { - if (currentProjectId) { - const updatedUncommitted = { ...uncommittedWatchedVariables }; - const updatedCommitted = { ...watchedVariables }; - if (selectedVariables?.length) { - selectedVariables.map((item: IObjectWithKey) => { - delete updatedUncommitted[item.key as string]; - delete updatedCommitted[item.key as string]; - }); - } - setWatchedVariables(currentProjectId, updatedCommitted); - setUncommittedWatchedVariables(updatedUncommitted); + const updatedUncommitted = { ...uncommittedWatchedVariables }; + const updatedCommitted = { ...watchedVariables }; + if (selectedVariables?.length) { + selectedVariables.map((item: IObjectWithKey) => { + delete updatedUncommitted[item.key as string]; + delete updatedCommitted[item.key as string]; + }); } + setWatchedVariables(currentProjectId, updatedCommitted); + setUncommittedWatchedVariables(updatedUncommitted); }, [currentProjectId, selectedVariables, setWatchedVariables, setUncommittedWatchedVariables]); const removeIsDisabled = useMemo(() => { diff --git a/Composer/packages/client/src/recoilModel/dispatchers/project.ts b/Composer/packages/client/src/recoilModel/dispatchers/project.ts index 015f0c5677..d70539ad44 100644 --- a/Composer/packages/client/src/recoilModel/dispatchers/project.ts +++ b/Composer/packages/client/src/recoilModel/dispatchers/project.ts @@ -41,6 +41,7 @@ import { creationFlowStatusState, orchestratorForSkillsDialogState, selectedTemplateVersionState, + watchedVariablesState, } from '../atoms'; import { botRuntimeOperationsSelector, rootBotProjectIdSelector } from '../selectors'; import { mergePropertiesManagedByRootBot, postRootBotCreation } from '../../recoilModel/dispatchers/utils/project'; @@ -227,7 +228,7 @@ export const projectDispatcher = () => { absData?: any, callback?: (projectId: string) => void ) => { - const { set, snapshot } = callbackHelpers; + const { reset, set, snapshot } = callbackHelpers; try { set(botOpeningState, true); @@ -237,6 +238,7 @@ export const projectDispatcher = () => { path, storageId ); + reset(watchedVariablesState(projectId)); if (requiresMigrate) { await forceMigrate(projectId, hasOldCustomRuntime); From 030d500469ae303bbd1b6005189a10ecba7323fd Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 09:27:24 -0700 Subject: [PATCH 29/40] Fixed type --- .../DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index ce71ebe4e4..3f19a4672b 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -137,7 +137,7 @@ export const WatchTabContent: React.FC = ({ isActive } // reset state when switching to a new project useEffect(() => { - setUncommittedWatchedVariables([]); + setUncommittedWatchedVariables({}); }, [currentProjectId]); // get memory scope variables for the bot From 62c21cdd28e0ef56927b42294d387e51bcadb000 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 11:51:47 -0700 Subject: [PATCH 30/40] Can now watch the root of a memory scope --- .../WatchTab/WatchTabContent.tsx | 7 ++++ .../design/__tests__/WatchTabContent.test.tsx | 35 ++++++++++++------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 3f19a4672b..e01d567fb0 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -104,6 +104,13 @@ export const getValueFromBotTraceMemory = ( botTrace: Activity ): { value: any; propertyIsAvailable: boolean } => { const pathSegments = valuePath.split('.'); + if (pathSegments.length === 1) { + // this is the root level of a memory scope + return { + propertyIsAvailable: true, + value: get(botTrace?.value, valuePath, undefined), + }; + } pathSegments.pop(); const parentValuePath = pathSegments.join('.'); const parentPropertyValue = get(botTrace?.value, parentValuePath, undefined); diff --git a/Composer/packages/client/src/pages/design/__tests__/WatchTabContent.test.tsx b/Composer/packages/client/src/pages/design/__tests__/WatchTabContent.test.tsx index 8632d655c7..f62348cee5 100644 --- a/Composer/packages/client/src/pages/design/__tests__/WatchTabContent.test.tsx +++ b/Composer/packages/client/src/pages/design/__tests__/WatchTabContent.test.tsx @@ -11,26 +11,28 @@ import { getValueFromBotTraceMemory, WatchTabContent } from '../DebugPanel/TabEx describe('', () => { describe('rendering', () => { it('should add a row to the table', async () => { - const { findByText } = renderWithRecoil(, () => {}); + const { findByPlaceholderText, findByText } = renderWithRecoil(, () => {}); const addButton = await findByText('Add property'); act(() => { fireEvent.click(addButton); }); - // value for unset watched variable - await findByText('unavailable'); + await findByPlaceholderText('Add property path to watch'); }); it('should remove a row from the table', async () => { const rootBotId = '123-adc'; - const { findByText, queryByText } = renderWithRecoil(, ({ set }) => { - set(botProjectIdsState, [rootBotId]); - set(projectMetaDataState(rootBotId), { - isRootBot: true, - isRemote: false, - }); - }); + const { findByPlaceholderText, findByText, queryByPlaceholderText } = renderWithRecoil( + , + ({ set }) => { + set(botProjectIdsState, [rootBotId]); + set(projectMetaDataState(rootBotId), { + isRootBot: true, + isRemote: false, + }); + } + ); const addButton = await findByText('Add property'); const removeButton = await findByText('Remove from list'); @@ -41,8 +43,8 @@ describe('', () => { // select the row await act(async () => { - const newRow = await findByText('unavailable'); - fireEvent.click(newRow); + const newRow = await findByPlaceholderText('Add property path to watch'); + newRow.parentElement && fireEvent.click(newRow.parentElement); }); // remove the row @@ -50,7 +52,7 @@ describe('', () => { fireEvent.click(removeButton); }); - const nonexistentRow = queryByText('unavailable'); + const nonexistentRow = queryByPlaceholderText('Add property path to watch'); expect(nonexistentRow).toBeNull(); }); }); @@ -89,5 +91,12 @@ describe('', () => { expect(result.propertyIsAvailable).toBe(true); expect(result.value).toBe(undefined); }); + + it('should get the root of a memory scope', () => { + const result = getValueFromBotTraceMemory('user', botTrace); + + expect(result.propertyIsAvailable).toBe(true); + expect(result.value).toEqual(botTrace.value.user); + }); }); }); From 73aee8161a1d3440641ce1c28e6f3177642dc0dd Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 12:18:33 -0700 Subject: [PATCH 31/40] Scoped state to current project ID instead of root --- .../WatchVariablePicker.tsx | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx index 757711fea2..91376f4dd8 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx @@ -12,7 +12,7 @@ import { FontSizes, SharedColors } from '@uifabric/fluent-theme'; import { useRecoilValue } from 'recoil'; import { getDefaultFontSettings } from '../../../../recoilModel/utils/fontUtil'; -import { dispatcherState, rootBotProjectIdSelector, watchedVariablesState } from '../../../../recoilModel'; +import { currentProjectIdState, dispatcherState, watchedVariablesState } from '../../../../recoilModel'; import { PropertyItem } from './utils/components/PropertyTreeItem'; import { useNoSearchResultMenuItem } from './utils/hooks/useNoSearchResultMenuItem'; @@ -60,12 +60,12 @@ const pickerContainer = css` `; export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) => { - const currentProjectId = useRecoilValue(rootBotProjectIdSelector); - const watchedVariables = useRecoilValue(watchedVariablesState(currentProjectId ?? '')); + const currentProjectId = useRecoilValue(currentProjectIdState); + const watchedVariables = useRecoilValue(watchedVariablesState(currentProjectId)); const { setWatchedVariables } = useRecoilValue(dispatcherState); const { path, payload, variableId } = props; const [errorMessage, setErrorMessage] = useState(''); - const [query, setQuery] = useState(null); + const [query, setQuery] = useState(null); const inputBoxElementRef = useRef(null); const pickerContainerElementRef = useRef(null); const [showContextualMenu, setShowContextualMenu] = React.useState(false); @@ -104,14 +104,12 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) event?.preventDefault(); onToggleExpand(node.id, !propertyTreeExpanded[node.id]); } else { - if (currentProjectId) { - const path = paths[node.id]; - setQuery(path); - event?.preventDefault(); - setErrorMessage(''); - setWatchedVariables(currentProjectId, { ...watchedVariables, [variableId]: path }); - onHideContextualMenu(); - } + const path = paths[node.id]; + setQuery(path); + event?.preventDefault(); + setErrorMessage(''); + setWatchedVariables(currentProjectId, { ...watchedVariables, [variableId]: path }); + onHideContextualMenu(); } }, data: { @@ -138,12 +136,10 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) secondaryText: paths[node.id], onClick: (event) => { event?.preventDefault(); - if (currentProjectId) { - const path = paths[node.id]; - setErrorMessage(''); - setWatchedVariables(currentProjectId, { ...watchedVariables, [variableId]: path }); - onHideContextualMenu(); - } + const path = paths[node.id]; + setErrorMessage(''); + setWatchedVariables(currentProjectId, { ...watchedVariables, [variableId]: path }); + onHideContextualMenu(); }, data: { node, @@ -196,7 +192,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const onTextBoxKeyDown = useCallback( (event: KeyboardEvent) => { - if (currentProjectId && event.key === 'Enter') { + if (query && event.key === 'Enter') { event.preventDefault(); if (Object.values(watchedVariables).find((variable) => variable === query)) { // variable is already being watched @@ -219,7 +215,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) }, [onHideContextualMenu]); const contextualMenuItemRenderer = useMemo(() => { - return getPickerContextualMenuItem(query, propertyTreeExpanded); + return getPickerContextualMenuItem(query ?? '', propertyTreeExpanded); }, [query, propertyTreeExpanded]); return ( From d2370f58e3b8f4c672b80bca835e978b1f43476a Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 13:28:06 -0700 Subject: [PATCH 32/40] Watch picker now gets updated memory variables --- .../WatchTab/WatchTabContent.tsx | 33 ++----------------- .../WatchVariablePicker.tsx | 15 +++++++-- .../WatchVariablePicker/utils/helpers.ts | 17 ++++++++++ 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index e01d567fb0..0018bbbbf5 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -37,8 +37,6 @@ import { webChatTrafficState, } from '../../../../../recoilModel'; import { WatchVariablePicker } from '../../WatchVariablePicker/WatchVariablePicker'; -import { getMemoryVariables } from '../../../../../recoilModel/dispatchers/utils/project'; -import { WatchDataPayload } from '../../WatchVariablePicker/utils/helpers'; import { WatchTabObjectValue } from './WatchTabObjectValue'; @@ -128,10 +126,6 @@ export const WatchTabContent: React.FC = ({ isActive } const { setWatchedVariables } = useRecoilValue(dispatcherState); const [uncommittedWatchedVariables, setUncommittedWatchedVariables] = useState>({}); const [selectedVariables, setSelectedVariables] = useState(); - const [memoryVariablesPayload, setMemoryVariablesPayload] = useState({ - kind: 'property', - data: { properties: [] }, - }); const watchedVariablesSelection = useRef( new Selection({ @@ -147,27 +141,6 @@ export const WatchTabContent: React.FC = ({ isActive } setUncommittedWatchedVariables({}); }, [currentProjectId]); - // get memory scope variables for the bot - useEffect(() => { - const abortController = new AbortController(); - (async () => { - try { - const watched = Object.values(watchedVariables); - let variables = await getMemoryVariables(currentProjectId, { signal: abortController.signal }); - // we don't want to show variables that are already being watched - variables = variables.filter((v) => !watched.find((watchedV) => watchedV === v)); - - setMemoryVariablesPayload({ kind: 'property', data: { properties: variables } }); - } catch (e) { - // error can be due to abort - } - })(); - - return () => { - abortController.abort(); - }; - }, [currentProjectId, watchedVariables]); - const mostRecentBotState = useMemo(() => { const botStateTraffic = rawWebChatTraffic.filter( (t) => t.trafficType === 'activity' && t.activity.type === 'trace' && t.activity.name === 'BotState' @@ -179,11 +152,9 @@ export const WatchTabContent: React.FC = ({ isActive } const onRenderVariableName = useCallback( (item: { key: string; value: string }, index: number | undefined, column: IColumn | undefined) => { - return ( - - ); + return ; }, - [memoryVariablesPayload] + [] ); const onRenderVariableValue = useCallback( diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx index 91376f4dd8..05e037a5c5 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx @@ -18,12 +18,12 @@ import { PropertyItem } from './utils/components/PropertyTreeItem'; import { useNoSearchResultMenuItem } from './utils/hooks/useNoSearchResultMenuItem'; import { computePropertyItemTree, getAllNodes, WatchDataPayload } from './utils/helpers'; import { getPickerContextualMenuItem } from './utils/components/PickerContextualMenuItem'; +import { getMemoryVariablesForProject } from './utils/helpers'; const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); type WatchVariablePickerProps = { path: string; - payload: WatchDataPayload; disabled?: boolean; variableId: string; }; @@ -63,8 +63,9 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const currentProjectId = useRecoilValue(currentProjectIdState); const watchedVariables = useRecoilValue(watchedVariablesState(currentProjectId)); const { setWatchedVariables } = useRecoilValue(dispatcherState); - const { path, payload, variableId } = props; + const { path, variableId } = props; const [errorMessage, setErrorMessage] = useState(''); + const [payload, setPayload] = useState({ kind: 'property', data: { properties: [] } }); const [query, setQuery] = useState(null); const inputBoxElementRef = useRef(null); const pickerContainerElementRef = useRef(null); @@ -75,6 +76,16 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const noSearchResultMenuItem = useNoSearchResultMenuItem(uiStrings.emptyMessage); + useEffect(() => { + if (showContextualMenu) { + // fetch the bot's available memory variables when the picker is opened + (async () => { + const memoryVariables = await getMemoryVariablesForProject(currentProjectId, watchedVariables); + setPayload(memoryVariables); + })(); + } + }, [currentProjectId, showContextualMenu, watchedVariables]); + const propertyTreeConfig = useMemo(() => { const { properties } = payload.data; return { root: computePropertyItemTree(properties) }; diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/helpers.ts b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/helpers.ts index 18c09a8b26..c1d58498ff 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/helpers.ts +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/helpers.ts @@ -3,6 +3,8 @@ import uniq from 'lodash/uniq'; +import { getMemoryVariables } from '../../../../../recoilModel/dispatchers/utils/project'; + export type PropertyItem = { id: string; name: string; @@ -109,3 +111,18 @@ export const getAllNodes = ) => { + const abortController = new AbortController(); + try { + const watched = Object.values(watchedVariables); + let variables = await getMemoryVariables(projectId, { signal: abortController.signal }); + // we don't want to show variables that are already being watched + variables = variables.filter((v) => !watched.find((watchedV) => watchedV === v)); + + return { kind: 'property', data: { properties: variables } }; + } catch (e) { + // error can be due to abort + return { kind: 'property', data: { properties: [] } }; + } +}; From dd118c1619c332150c35ccea437d46b539e5a977 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 13:31:21 -0700 Subject: [PATCH 33/40] Got rid of watch picker "no results" state --- .../WatchVariablePicker.tsx | 9 ++--- .../utils/hooks/useNoSearchResultMenuItem.tsx | 38 ------------------- 2 files changed, 3 insertions(+), 44 deletions(-) delete mode 100644 Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/hooks/useNoSearchResultMenuItem.tsx diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx index 05e037a5c5..6c09f5b2af 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx @@ -15,7 +15,6 @@ import { getDefaultFontSettings } from '../../../../recoilModel/utils/fontUtil'; import { currentProjectIdState, dispatcherState, watchedVariablesState } from '../../../../recoilModel'; import { PropertyItem } from './utils/components/PropertyTreeItem'; -import { useNoSearchResultMenuItem } from './utils/hooks/useNoSearchResultMenuItem'; import { computePropertyItemTree, getAllNodes, WatchDataPayload } from './utils/helpers'; import { getPickerContextualMenuItem } from './utils/components/PickerContextualMenuItem'; import { getMemoryVariablesForProject } from './utils/helpers'; @@ -74,8 +73,6 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const [propertyTreeExpanded, setPropertyTreeExpanded] = React.useState>({}); const uiStrings = useMemo(() => getStrings(), []); - const noSearchResultMenuItem = useNoSearchResultMenuItem(uiStrings.emptyMessage); - useEffect(() => { if (showContextualMenu) { // fetch the bot's available memory variables when the picker is opened @@ -177,9 +174,9 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const filteredItems = flatPropertyListItems.filter(predicate); - if (!filteredItems || !filteredItems.length) { - filteredItems.push(noSearchResultMenuItem); - } + // if (!filteredItems || !filteredItems.length) { + // filteredItems.push(noSearchResultMenuItem); + // } setItems(filteredItems); } else { diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/hooks/useNoSearchResultMenuItem.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/hooks/useNoSearchResultMenuItem.tsx deleted file mode 100644 index 37ad7741b4..0000000000 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/hooks/useNoSearchResultMenuItem.tsx +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import formatMessage from 'format-message'; -import { IContextualMenuItem } from 'office-ui-fabric-react/lib/ContextualMenu'; -import { Icon } from 'office-ui-fabric-react/lib/Icon'; -import { Stack } from 'office-ui-fabric-react/lib/Stack'; -import { Text } from 'office-ui-fabric-react/lib/Text'; -import * as React from 'react'; - -const searchEmptyMessageStyles = { root: { height: 32 } }; -const searchEmptyMessageTokens = { childrenGap: 8 }; - -/** - * Search empty view for contextual menu with search capability. - */ -export const useNoSearchResultMenuItem = (message?: string): IContextualMenuItem => { - message = message ?? formatMessage('no items found'); - return React.useMemo( - () => ({ - key: 'no_results', - onRender: () => ( - - - {message} - - ), - }), - [message] - ); -}; From 699d95bc05071d4b3d34cc0c8dd33b583ee32834 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 13:57:21 -0700 Subject: [PATCH 34/40] Added empty state blurb to watch tab --- .../WatchTab/WatchTabContent.tsx | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 0018bbbbf5..a36c28ee5a 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -28,6 +28,7 @@ import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane'; import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; import { CommunicationColors, FluentTheme } from '@uifabric/fluent-theme'; +import { Link } from 'office-ui-fabric-react/lib/Link'; import { DebugPanelTabHeaderProps } from '../types'; import { @@ -58,6 +59,13 @@ const primitiveValue = css` line-height: 16px; `; +const emptyState = css` + font-family: ${FluentTheme.fonts.small.fontFamily}; + font-size: ${FluentTheme.fonts.small.fontSize}; + line-height: ${FluentTheme.fonts.small.lineHeight}; + padding-left: 16px; +`; + const watchTableStyles: Partial = { root: { maxHeight: `calc(100% - ${toolbarHeight}px)`, @@ -305,16 +313,31 @@ export const WatchTabContent: React.FC = ({ isActive } }} > - + {refreshedWatchedVariables.length ? ( + + ) : ( + + {formatMessage.rich( + 'Add properties to watch while testing your bot in the Web Chat pane. Learn more.', + { + a: ({ children }) => ( + + {children} + + ), + } + )} + + )} From 7484d38078c4ef3d1740323eef24fa6e65674972 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 14:02:32 -0700 Subject: [PATCH 35/40] Added missing "target" attribute to link --- .../DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index a36c28ee5a..f9812958a0 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -330,7 +330,7 @@ export const WatchTabContent: React.FC = ({ isActive } 'Add properties to watch while testing your bot in the Web Chat pane. Learn more.', { a: ({ children }) => ( - + {children} ), From e8ed9dcb4fa10ae2c2c896ff28f4df33c557455e Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 14:03:21 -0700 Subject: [PATCH 36/40] Increased JSON view max height --- .../DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx index bea7da5f12..2a7c079d58 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx @@ -13,7 +13,7 @@ const editorStyles = css` border: none; `; -const maxJsonHeight = 80; +const maxJsonHeight = 100; const objectCellStyle = (numLinesOfJson: number) => css` height: ${Math.min(numLinesOfJson * 18, maxJsonHeight)}px; From 101da0b2228882b01da35736d18261f0866a00ba Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 15:21:45 -0700 Subject: [PATCH 37/40] Font and padding improvements. --- .../WatchTab/WatchTabContent.tsx | 30 +++++++---- .../WatchVariablePicker.tsx | 54 +++++++++++-------- 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index f9812958a0..3a9e779c51 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -6,7 +6,7 @@ import { css, jsx } from '@emotion/core'; import React, { useMemo, useCallback, useState, useEffect, useRef } from 'react'; import { useRecoilValue } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; -import { ConversationActivityTrafficItem, Activity } from '@botframework-composer/types'; +import { ConversationActivityTrafficItem, Activity, UserSettings } from '@botframework-composer/types'; import { Stack } from 'office-ui-fabric-react/lib/Stack'; import { CommandBar, ICommandBarStyles } from 'office-ui-fabric-react/lib/CommandBar'; import { @@ -34,6 +34,7 @@ import { DebugPanelTabHeaderProps } from '../types'; import { currentProjectIdState, dispatcherState, + userSettingsState, watchedVariablesState, webChatTrafficState, } from '../../../../../recoilModel'; @@ -49,14 +50,18 @@ const unavailbleValue = css` font-style: italic; height: 16px; line-height: 16px; + display: inline-block; + padding: 8px 0; `; -const primitiveValue = css` - font-family: ${FluentTheme.fonts.small.fontFamily}; - font-size: ${FluentTheme.fonts.small.fontSize}; +const primitiveValue = (userSettings: UserSettings) => css` + font-family: ${userSettings.codeEditor.fontSettings.fontFamily}; + font-size: ${userSettings.codeEditor.fontSettings.fontSize}; color: ${CommunicationColors.shade10}; height: 16px; line-height: 16px; + display: inline-block; + padding: 8px 0; `; const emptyState = css` @@ -77,8 +82,8 @@ const watchTableStyles: Partial = { }, }; -const rowStyles: Partial = { - cell: { minHeight: 32, padding: '8px 6px' }, +const rowStyles = (): Partial => ({ + cell: { minHeight: 32, padding: '0 6px' }, checkCell: { height: 32, minHeight: 32, @@ -89,7 +94,7 @@ const rowStyles: Partial = { }, }, root: { minHeight: 32 }, -}; +}); const commandBarStyles: Partial = { root: { height: toolbarHeight, padding: 0 } }; const detailsHeaderStyles: Partial = { @@ -134,6 +139,7 @@ export const WatchTabContent: React.FC = ({ isActive } const { setWatchedVariables } = useRecoilValue(dispatcherState); const [uncommittedWatchedVariables, setUncommittedWatchedVariables] = useState>({}); const [selectedVariables, setSelectedVariables] = useState(); + const userSettings = useRecoilValue(userSettingsState); const watchedVariablesSelection = useRef( new Selection({ @@ -180,10 +186,12 @@ export const WatchTabContent: React.FC = ({ isActive } // render monaco view return ; } else if (value === undefined) { - return {formatMessage('undefined')}; + return {formatMessage('undefined')}; } else { // render primitive view - return {typeof value === 'string' ? `"${value}"` : String(value)}; + return ( + {typeof value === 'string' ? `"${value}"` : String(value)} + ); } } else { // the value is not available @@ -196,7 +204,7 @@ export const WatchTabContent: React.FC = ({ isActive } ) : null; } }, - [mostRecentBotState, watchedVariables] + [mostRecentBotState, userSettings, watchedVariables] ); // TODO: update to office-ui-fabric-react@7.170.x to gain access to "flexGrow" column property to distribute proprotional column widths @@ -238,7 +246,7 @@ export const WatchTabContent: React.FC = ({ isActive } }, [mostRecentBotState, uncommittedWatchedVariables, watchedVariables]); const renderRow = useCallback((props?: IDetailsRowProps) => { - return props ? : null; + return props ? : null; }, []); const onClickAdd = useCallback(() => { diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx index 6c09f5b2af..c8327d31a5 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/WatchVariablePicker.tsx @@ -7,20 +7,28 @@ import formatMessage from 'format-message'; import React, { useMemo, useCallback, useEffect, useRef, FocusEvent, KeyboardEvent, useState, FormEvent } from 'react'; import { TextField, ITextField, ITextFieldStyles } from 'office-ui-fabric-react/lib/TextField'; import debounce from 'lodash/debounce'; -import { IContextualMenuItem, ContextualMenu, DirectionalHint } from 'office-ui-fabric-react/lib/ContextualMenu'; -import { FontSizes, SharedColors } from '@uifabric/fluent-theme'; +import { + IContextualMenuItem, + ContextualMenu, + DirectionalHint, + IContextualMenuStyles, +} from 'office-ui-fabric-react/lib/ContextualMenu'; +import { SharedColors } from '@uifabric/fluent-theme'; import { useRecoilValue } from 'recoil'; +import { UserSettings } from '@botframework-composer/types'; -import { getDefaultFontSettings } from '../../../../recoilModel/utils/fontUtil'; -import { currentProjectIdState, dispatcherState, watchedVariablesState } from '../../../../recoilModel'; +import { + currentProjectIdState, + dispatcherState, + userSettingsState, + watchedVariablesState, +} from '../../../../recoilModel'; import { PropertyItem } from './utils/components/PropertyTreeItem'; import { computePropertyItemTree, getAllNodes, WatchDataPayload } from './utils/helpers'; import { getPickerContextualMenuItem } from './utils/components/PickerContextualMenuItem'; import { getMemoryVariablesForProject } from './utils/helpers'; -const DEFAULT_FONT_SETTINGS = getDefaultFontSettings(); - type WatchVariablePickerProps = { path: string; disabled?: boolean; @@ -34,16 +42,18 @@ const getStrings = () => { }; }; -const textFieldStyles = (errorMessage?: string): Partial => ({ +const textFieldStyles = (userSettings: UserSettings, errorMessage?: string): Partial => ({ field: { - fontFamily: DEFAULT_FONT_SETTINGS.fontFamily, - fontSize: FontSizes.size12, + fontFamily: userSettings.codeEditor.fontSettings.fontFamily, + fontSize: userSettings.codeEditor.fontSettings.fontSize, + fontWeight: userSettings.codeEditor.fontSettings.fontWeight as any, }, fieldGroup: { backgroundColor: 'transparent', - height: 16, + height: 28, // row is 32px high with 2px padding on top and bottom }, root: { + padding: '2px 0', selectors: { '.ms-TextField-fieldGroup': { border: 'none', @@ -53,6 +63,14 @@ const textFieldStyles = (errorMessage?: string): Partial => ({ }, }); +const contextualMenuStyles: Partial = { + root: { + maxHeight: '200px', + overflowY: 'auto', + width: '240px', + }, +}; + const pickerContainer = css` margin: '0'; width: '240px'; @@ -72,6 +90,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) const [items, setItems] = useState([]); const [propertyTreeExpanded, setPropertyTreeExpanded] = React.useState>({}); const uiStrings = useMemo(() => getStrings(), []); + const userSettings = useRecoilValue(userSettingsState); useEffect(() => { if (showContextualMenu) { @@ -171,13 +190,8 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) debounce((passedQuery?: string) => { if (passedQuery) { const predicate = getFilterPredicate(passedQuery); - const filteredItems = flatPropertyListItems.filter(predicate); - // if (!filteredItems || !filteredItems.length) { - // filteredItems.push(noSearchResultMenuItem); - // } - setItems(filteredItems); } else { setItems(menuItems); @@ -233,7 +247,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) errorMessage={errorMessage} id={variableId} placeholder={uiStrings.searchPlaceholder} - styles={textFieldStyles(errorMessage)} + styles={textFieldStyles(userSettings, errorMessage)} value={query ?? path} onChange={onInputChange} onFocus={onTextBoxFocus} @@ -248,13 +262,7 @@ export const WatchVariablePicker = React.memo((props: WatchVariablePickerProps) hidden={!showContextualMenu} items={items} shouldFocusOnMount={false} - styles={{ - root: { - maxHeight: '200px', - overflowY: 'auto', - width: '240px', - }, - }} + styles={contextualMenuStyles} target={pickerContainerElementRef.current} onDismiss={onDismiss} onItemClick={onHideContextualMenu} From 6118f355cf23d2956471fb571771f21408da68b3 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 15:22:15 -0700 Subject: [PATCH 38/40] Monaco now fills entire row when rendering JSON --- .../DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx index 2a7c079d58..67db19c2d9 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabObjectValue.tsx @@ -17,7 +17,7 @@ const maxJsonHeight = 100; const objectCellStyle = (numLinesOfJson: number) => css` height: ${Math.min(numLinesOfJson * 18, maxJsonHeight)}px; - width: 360px; + width: 100%; `; type WatchTabObjectValueProps = { From 0b0904d5e6dae752127e9fe5406e9ffba215ec05 Mon Sep 17 00:00:00 2001 From: Tony Date: Tue, 29 Jun 2021 15:28:21 -0700 Subject: [PATCH 39/40] Got rid of unnecessary abort controller --- .../design/DebugPanel/WatchVariablePicker/utils/helpers.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/helpers.ts b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/helpers.ts index c1d58498ff..1e3696297a 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/helpers.ts +++ b/Composer/packages/client/src/pages/design/DebugPanel/WatchVariablePicker/utils/helpers.ts @@ -113,10 +113,9 @@ export const getAllNodes = ) => { - const abortController = new AbortController(); try { const watched = Object.values(watchedVariables); - let variables = await getMemoryVariables(projectId, { signal: abortController.signal }); + let variables = await getMemoryVariables(projectId); // we don't want to show variables that are already being watched variables = variables.filter((v) => !watched.find((watchedV) => watchedV === v)); From 4c7fa8c55d1ac8958516155fea551e7f2083aef0 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 30 Jun 2021 10:34:44 -0700 Subject: [PATCH 40/40] Fixed table padding --- .../WatchTab/WatchTabContent.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx index 3a9e779c51..e5ab0143fe 100644 --- a/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx +++ b/Composer/packages/client/src/pages/design/DebugPanel/TabExtensions/WatchTab/WatchTabContent.tsx @@ -24,11 +24,12 @@ import { } from 'office-ui-fabric-react/lib/DetailsList'; import formatMessage from 'format-message'; import get from 'lodash/get'; -import { ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane'; +import { IScrollablePaneStyles, ScrollablePane } from 'office-ui-fabric-react/lib/ScrollablePane'; import { Sticky, StickyPositionType } from 'office-ui-fabric-react/lib/Sticky'; import { TooltipHost } from 'office-ui-fabric-react/lib/Tooltip'; import { CommunicationColors, FluentTheme } from '@uifabric/fluent-theme'; import { Link } from 'office-ui-fabric-react/lib/Link'; +import { IButtonStyles } from 'office-ui-fabric-react/lib/Button'; import { DebugPanelTabHeaderProps } from '../types'; import { @@ -103,6 +104,21 @@ const detailsHeaderStyles: Partial = { }, }; +const scrollingContainerStyles: Partial = { + contentContainer: { + padding: '0 16px', + }, +}; + +const addButtonStyles: Partial = { + root: { + paddingLeft: 0, + }, + icon: { + marginLeft: 0, + }, +}; + const NameColumnKey = 'watchTabNameColumn'; const ValueColumnKey = 'watchTabValueColumn'; @@ -303,6 +319,7 @@ export const WatchTabContent: React.FC = ({ isActive } text: formatMessage('Add property'), iconProps: { iconName: 'Add' }, onClick: onClickAdd, + buttonStyles: addButtonStyles, }, { disabled: removeIsDisabled, @@ -320,7 +337,7 @@ export const WatchTabContent: React.FC = ({ isActive } position: 'relative', }} > - + {refreshedWatchedVariables.length ? (