Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add "Watch" tab to the debug panel #8151

Merged
merged 48 commits into from
Jun 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
59e70a8
Started stubbing out bot state debug panel
tonyanziano Jun 7, 2021
dc6725c
Rough outline of bot state inspector pane
tonyanziano Jun 9, 2021
c5e2d20
Stubbed out new table view
tonyanziano Jun 10, 2021
48d68fb
Added placeholder input fields
tonyanziano Jun 10, 2021
3457bc8
Watch variable update
Jun 15, 2021
f8ab570
Made some of the variable names clearer
tonyanziano Jun 16, 2021
8f7781a
Made table header sticky and body scroll
tonyanziano Jun 16, 2021
8802fa0
Fixed some interactions with variable picker
tonyanziano Jun 17, 2021
306c7d6
Refactored code and added smart height to object display
tonyanziano Jun 17, 2021
a74c12c
Refactored to make code more readable
tonyanziano Jun 17, 2021
49ebc41
Remove button disabled when nothing is selected
tonyanziano Jun 18, 2021
d1f2448
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
tonyanziano Jun 18, 2021
9971971
Added validation for already watched variables
tonyanziano Jun 21, 2021
5825ce9
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
tonyanziano Jun 21, 2021
8886ce8
Fixed picker error styling and table col layout
tonyanziano Jun 21, 2021
830851c
Improved the way we get values from the bot trace
tonyanziano Jun 21, 2021
b0e6c7f
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
tonyanziano Jun 23, 2021
e640c29
tab content
Jun 24, 2021
e9cce1e
resolve height issues
Jun 24, 2021
73ce5a7
Resolved a lot of PR comments
tonyanziano Jun 24, 2021
80d293e
Merge branch 'toanzian/watch-tab' of https://github.com/microsoft/Bot…
tonyanziano Jun 24, 2021
bb77933
Addressed some PR comments and reworked state.
tonyanziano Jun 25, 2021
290ae39
Watched variables are now hidden from the picker
tonyanziano Jun 25, 2021
affb0ca
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
tonyanziano Jun 25, 2021
250f37d
Fixed telemetry type
tonyanziano Jun 25, 2021
9488e53
Added tests for watch tab
tonyanziano Jun 25, 2021
b52a218
PR comments
tonyanziano Jun 25, 2021
d893607
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
tonyanziano Jun 28, 2021
d559423
Addressed more PR comments
tonyanziano Jun 28, 2021
4b00eb2
More PR comment changes
tonyanziano Jun 28, 2021
b85e5c5
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
tonyanziano Jun 28, 2021
8abe76b
Fixed state persistence
tonyanziano Jun 29, 2021
2b9e51d
Removed 'not available' message from empty state
tonyanziano Jun 29, 2021
8ae35f5
Added max height to object view in table
tonyanziano Jun 29, 2021
87adbf2
State now resets when opening a new project
tonyanziano Jun 29, 2021
030d500
Fixed type
tonyanziano Jun 29, 2021
62c21cd
Can now watch the root of a memory scope
tonyanziano Jun 29, 2021
73aee81
Scoped state to current project ID instead of root
tonyanziano Jun 29, 2021
d2370f5
Watch picker now gets updated memory variables
tonyanziano Jun 29, 2021
dd118c1
Got rid of watch picker "no results" state
tonyanziano Jun 29, 2021
699d95b
Added empty state blurb to watch tab
tonyanziano Jun 29, 2021
7484d38
Added missing "target" attribute to link
tonyanziano Jun 29, 2021
e8ed9dc
Increased JSON view max height
tonyanziano Jun 29, 2021
101da0b
Font and padding improvements.
tonyanziano Jun 29, 2021
6118f35
Monaco now fills entire row when rendering JSON
tonyanziano Jun 29, 2021
0b0904d
Got rid of unnecessary abort controller
tonyanziano Jun 29, 2021
404fa3d
Merge branch 'main' of https://github.com/microsoft/BotFramework-Comp…
tonyanziano Jun 30, 2021
4c7fa8c
Fixed table padding
tonyanziano Jun 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const OutputsTabContent: React.FC<DebugPanelTabHeaderProps> = ({ isActive
splitterSize="5px"
>
<BotProjectsFilter currentProjectId={currentProjectId} onChangeProject={setProjectId} />

<RuntimeOutputLog projectId={currentProjectId} />
</Split>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,370 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

/** @jsx jsx */
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, 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 {
DetailsList,
DetailsListLayoutMode,
IColumn,
SelectionMode,
Selection,
IObjectWithKey,
IDetailsRowProps,
DetailsRow,
IDetailsListStyles,
IDetailsRowStyles,
IDetailsHeaderStyles,
} from 'office-ui-fabric-react/lib/DetailsList';
import formatMessage from 'format-message';
import get from 'lodash/get';
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 {
currentProjectIdState,
dispatcherState,
userSettingsState,
watchedVariablesState,
webChatTrafficState,
} from '../../../../../recoilModel';
import { WatchVariablePicker } from '../../WatchVariablePicker/WatchVariablePicker';

import { WatchTabObjectValue } from './WatchTabObjectValue';

const toolbarHeight = 24;

const unavailbleValue = css`
font-family: ${FluentTheme.fonts.small.fontFamily};
font-size: ${FluentTheme.fonts.small.fontSize};
font-style: italic;
height: 16px;
line-height: 16px;
display: inline-block;
padding: 8px 0;
`;

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`
font-family: ${FluentTheme.fonts.small.fontFamily};
font-size: ${FluentTheme.fonts.small.fontSize};
line-height: ${FluentTheme.fonts.small.lineHeight};
padding-left: 16px;
`;

const watchTableStyles: Partial<IDetailsListStyles> = {
root: {
maxHeight: `calc(100% - ${toolbarHeight}px)`,
},
contentWrapper: {
overflowY: 'auto' as any,
// fill remaining space after table header row
height: 'calc(100% - 60px)',
},
};

const rowStyles = (): Partial<IDetailsRowStyles> => ({
cell: { minHeight: 32, padding: '0 6px' },
checkCell: {
height: 32,
minHeight: 32,
selectors: {
'& > div[role="checkbox"]': {
height: 32,
},
},
},
root: { minHeight: 32 },
});

const commandBarStyles: Partial<ICommandBarStyles> = { root: { height: toolbarHeight, padding: 0 } };
const detailsHeaderStyles: Partial<IDetailsHeaderStyles> = {
root: {
paddingTop: 0,
},
};

const scrollingContainerStyles: Partial<IScrollablePaneStyles> = {
contentContainer: {
padding: '0 16px',
},
};

const addButtonStyles: Partial<IButtonStyles> = {
root: {
paddingLeft: 0,
},
icon: {
marginLeft: 0,
},
};

const NameColumnKey = 'watchTabNameColumn';
const ValueColumnKey = 'watchTabValueColumn';

const watchTableLayout: DetailsListLayoutMode = DetailsListLayoutMode.justified;

// Returns the specified property from the bot state trace if it exists.
// Ex. getValueFromBotTraceMemory('user.address.city', trace)
export const getValueFromBotTraceMemory = (
valuePath: string,
botTrace: Activity
): { value: any; propertyIsAvailable: boolean } => {
const pathSegments = valuePath.split('.');
tonyanziano marked this conversation as resolved.
Show resolved Hide resolved
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);
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<DebugPanelTabHeaderProps> = ({ isActive }) => {
tonyanziano marked this conversation as resolved.
Show resolved Hide resolved
const currentProjectId = useRecoilValue(currentProjectIdState);
const rawWebChatTraffic = useRecoilValue(webChatTrafficState(currentProjectId));
const watchedVariables = useRecoilValue(watchedVariablesState(currentProjectId));
const { setWatchedVariables } = useRecoilValue(dispatcherState);
const [uncommittedWatchedVariables, setUncommittedWatchedVariables] = useState<Record<string, string>>({});
const [selectedVariables, setSelectedVariables] = useState<IObjectWithKey[]>();
const userSettings = useRecoilValue(userSettingsState);

const watchedVariablesSelection = useRef(
new Selection({
onSelectionChanged: () => {
setSelectedVariables(watchedVariablesSelection.current.getSelection());
},
selectionMode: SelectionMode.multiple,
})
);

// reset state when switching to a new project
useEffect(() => {
setUncommittedWatchedVariables({});
}, [currentProjectId]);

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 onRenderVariableName = useCallback(
(item: { key: string; value: string }, index: number | undefined, column: IColumn | undefined) => {
return <WatchVariablePicker key={item.key} path={item.value} variableId={item.key} />;
},
[]
);

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 has not been committed yet
return null;
}
// try to determine the value and render it accordingly
const { propertyIsAvailable, value } = getValueFromBotTraceMemory(variable, mostRecentBotState?.activity);
if (propertyIsAvailable) {
if (value !== null && typeof value === 'object') {
// render monaco view
return <WatchTabObjectValue value={value} />;
} else if (value === undefined) {
return <span css={primitiveValue(userSettings)}>{formatMessage('undefined')}</span>;
} else {
// render primitive view
return (
<span css={primitiveValue(userSettings)}>{typeof value === 'string' ? `"${value}"` : String(value)}</span>
);
}
} else {
// the value is not available
return <span css={unavailbleValue}>{formatMessage('not available')}</span>;
}
} else {
// no bot trace available - render "not available" for committed variables, and nothing for uncommitted variables
return watchedVariables[item.key] !== undefined ? (
<span css={unavailbleValue}>{formatMessage('not available')}</span>
) : null;
}
},
[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
// (name column takes up 1/3 of space and value column takes up the remaining 2/3)
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, 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(() => {
// 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) => {
return props ? <DetailsRow {...props} styles={rowStyles()} /> : null;
}, []);

const onClickAdd = useCallback(() => {
setUncommittedWatchedVariables({
...uncommittedWatchedVariables,
[uuidv4()]: '',
});
}, [uncommittedWatchedVariables]);

const onClickRemove = useCallback(() => {
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(() => {
return !selectedVariables?.length;
}, [selectedVariables]);

const onRenderDetailsHeader = (props, defaultRender) => {
return (
<Sticky isScrollSynced stickyPosition={StickyPositionType.Header}>
{defaultRender({
...props,
onRenderColumnHeaderTooltip: (tooltipHostProps) => <TooltipHost {...tooltipHostProps} />,
styles: detailsHeaderStyles,
})}
</Sticky>
);
};

if (!isActive) {
return null;
}

return (
<Stack verticalFill>
<CommandBar
css={{
height: `${toolbarHeight}px`,
padding: '8px 16px 16px 16px',
alignItems: 'center',
}}
items={[
{
key: 'addProperty',
text: formatMessage('Add property'),
iconProps: { iconName: 'Add' },
onClick: onClickAdd,
buttonStyles: addButtonStyles,
},
{
disabled: removeIsDisabled,
key: 'removeProperty',
text: formatMessage('Remove from list'),
iconProps: { iconName: 'Cancel' },
onClick: onClickRemove,
},
]}
styles={commandBarStyles}
/>
<Stack.Item
css={{
height: `calc(100% - 55px)`,
position: 'relative',
}}
>
<ScrollablePane styles={scrollingContainerStyles}>
{refreshedWatchedVariables.length ? (
<DetailsList
columns={watchTableColumns}
items={refreshedWatchedVariables}
layoutMode={watchTableLayout}
selection={watchedVariablesSelection.current}
selectionMode={SelectionMode.multiple}
styles={watchTableStyles}
onRenderDetailsHeader={onRenderDetailsHeader}
onRenderRow={renderRow}
/>
) : (
<span css={emptyState}>
{formatMessage.rich(
'Add properties to watch while testing your bot in the Web Chat pane. <a>Learn more.</a>',
{
a: ({ children }) => (
<Link key="watch-table-empty-state-link" href="https://aka.ms/bfcomposer-2-watch" target="_blank">
{children}
</Link>
),
}
)}
</span>
)}
</ScrollablePane>
</Stack.Item>
</Stack>
);
};