Add more Telemetry to Data Explorer (#242)
* Add Telemetry to command bar buttons * Count and report # of files/notebooks/directories in myNotebook to telemetry * Add resource tree clicks to Telemetry * Log to Telemetry: opened notebook cell counts by type, kernelspec name * Fix unit test * Move Telemetry processor call in notebook traceNotebookTelemetry action from reducer to epic. Use action to trace other info. * Fix react duplicate key error * Log notebook cell context menu actions * Reformat and cleanup * Move resource tree tracing code out of render(). Only call once when tree is updated * Fix build issues
This commit is contained in:
parent
ff03c79399
commit
b69174788d
|
@ -18,6 +18,8 @@ import {
|
||||||
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
import TriangleDownIcon from "../../../../images/Triangle-down.svg";
|
||||||
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
import TriangleRightIcon from "../../../../images/Triangle-right.svg";
|
||||||
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
|
import LoadingIndicator_3Squares from "../../../../images/LoadingIndicator_3Squares.gif";
|
||||||
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
|
||||||
export interface TreeNodeMenuItem {
|
export interface TreeNodeMenuItem {
|
||||||
label: string;
|
label: string;
|
||||||
|
@ -276,7 +278,12 @@ export class TreeNodeComponent extends React.Component<TreeNodeComponentProps, T
|
||||||
text: menuItem.label,
|
text: menuItem.label,
|
||||||
disabled: menuItem.isDisabled,
|
disabled: menuItem.isDisabled,
|
||||||
className: menuItem.styleClass,
|
className: menuItem.styleClass,
|
||||||
onClick: menuItem.onClick,
|
onClick: () => {
|
||||||
|
menuItem.onClick();
|
||||||
|
TelemetryProcessor.trace(Action.ClickResourceTreeNodeContextMenuItem, ActionModifiers.Mark, {
|
||||||
|
label: menuItem.label
|
||||||
|
});
|
||||||
|
},
|
||||||
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
onRenderIcon: (props: any) => <img src={menuItem.iconSrc} alt="" />
|
||||||
}))
|
}))
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -191,7 +191,7 @@ exports[`TreeNodeComponent renders a simple node (sorted children, expanded) 1`]
|
||||||
"className": undefined,
|
"className": undefined,
|
||||||
"disabled": true,
|
"disabled": true,
|
||||||
"key": "menuLabel",
|
"key": "menuLabel",
|
||||||
"onClick": undefined,
|
"onClick": [Function],
|
||||||
"onRenderIcon": [Function],
|
"onRenderIcon": [Function],
|
||||||
"text": "menuLabel",
|
"text": "menuLabel",
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,7 +10,7 @@ import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
|
import { CommandBarComponentButtonFactory } from "./CommandBarComponentButtonFactory";
|
||||||
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
import { CommandBar, ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
||||||
import { StyleConstants } from "../../../Common/Constants";
|
import { StyleConstants } from "../../../Common/Constants";
|
||||||
import { CommandBarUtil } from "./CommandBarUtil";
|
import * as CommandBarUtil from "./CommandBarUtil";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CommandBarUtil } from "./CommandBarUtil";
|
import * as CommandBarUtil from "./CommandBarUtil";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
import { ICommandBarItemProps } from "office-ui-fabric-react/lib/CommandBar";
|
||||||
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
@ -8,7 +8,7 @@ describe("CommandBarUtil tests", () => {
|
||||||
return {
|
return {
|
||||||
iconSrc: "icon",
|
iconSrc: "icon",
|
||||||
iconAlt: "label",
|
iconAlt: "label",
|
||||||
onCommandClick: (e: React.SyntheticEvent): void => {},
|
onCommandClick: jest.fn(),
|
||||||
commandButtonLabel: "label",
|
commandButtonLabel: "label",
|
||||||
ariaLabel: "ariaLabel",
|
ariaLabel: "ariaLabel",
|
||||||
hasPopup: true,
|
hasPopup: true,
|
||||||
|
@ -29,11 +29,14 @@ describe("CommandBarUtil tests", () => {
|
||||||
expect(!converted.split);
|
expect(!converted.split);
|
||||||
expect(converted.iconProps.imageProps.src).toEqual(btn.iconSrc);
|
expect(converted.iconProps.imageProps.src).toEqual(btn.iconSrc);
|
||||||
expect(converted.iconProps.imageProps.alt).toEqual(btn.iconAlt);
|
expect(converted.iconProps.imageProps.alt).toEqual(btn.iconAlt);
|
||||||
expect(converted.onClick).toEqual(btn.onCommandClick);
|
|
||||||
expect(converted.text).toEqual(btn.commandButtonLabel);
|
expect(converted.text).toEqual(btn.commandButtonLabel);
|
||||||
expect(converted.ariaLabel).toEqual(btn.ariaLabel);
|
expect(converted.ariaLabel).toEqual(btn.ariaLabel);
|
||||||
expect(converted.disabled).toEqual(btn.disabled);
|
expect(converted.disabled).toEqual(btn.disabled);
|
||||||
expect(converted.className).toEqual(btn.className);
|
expect(converted.className).toEqual(btn.className);
|
||||||
|
|
||||||
|
// Click gets called
|
||||||
|
converted.onClick();
|
||||||
|
expect(btn.onCommandClick).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should convert NavbarButtonConfig to split button", () => {
|
it("should convert NavbarButtonConfig to split button", () => {
|
||||||
|
|
|
@ -11,177 +11,187 @@ import ChevronDownIcon from "../../../../images/Chevron_down.svg";
|
||||||
import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker";
|
import { ArcadiaMenuPicker } from "../../Controls/Arcadia/ArcadiaMenuPicker";
|
||||||
import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
|
import { MemoryTrackerComponent } from "./MemoryTrackerComponent";
|
||||||
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
|
import { MemoryUsageInfo } from "../../../Contracts/DataModels";
|
||||||
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utilities for CommandBar
|
* Convert our NavbarButtonConfig to UI Fabric buttons
|
||||||
|
* @param btns
|
||||||
*/
|
*/
|
||||||
export class CommandBarUtil {
|
export const convertButton = (btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] => {
|
||||||
/**
|
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
||||||
* Convert our NavbarButtonConfig to UI Fabric buttons
|
|
||||||
* @param btns
|
|
||||||
*/
|
|
||||||
public static convertButton(btns: CommandButtonComponentProps[], backgroundColor: string): ICommandBarItemProps[] {
|
|
||||||
const buttonHeightPx = StyleConstants.CommandBarButtonHeight;
|
|
||||||
|
|
||||||
return btns
|
return btns
|
||||||
.filter(btn => btn)
|
.filter(btn => btn)
|
||||||
.map(
|
.map(
|
||||||
(btn: CommandButtonComponentProps, index: number): ICommandBarItemProps => {
|
(btn: CommandButtonComponentProps, index: number): ICommandBarItemProps => {
|
||||||
if (btn.isDivider) {
|
if (btn.isDivider) {
|
||||||
return CommandBarUtil.createDivider(btn.commandButtonLabel);
|
return createDivider(btn.commandButtonLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSplit = !!btn.children && btn.children.length > 0;
|
const isSplit = !!btn.children && btn.children.length > 0;
|
||||||
|
const label = btn.commandButtonLabel || btn.tooltipText;
|
||||||
const result: ICommandBarItemProps = {
|
const result: ICommandBarItemProps = {
|
||||||
iconProps: {
|
iconProps: {
|
||||||
style: {
|
style: {
|
||||||
width: StyleConstants.CommandBarIconWidth, // 16
|
width: StyleConstants.CommandBarIconWidth, // 16
|
||||||
alignSelf: btn.iconName ? "baseline" : undefined
|
alignSelf: btn.iconName ? "baseline" : undefined
|
||||||
},
|
|
||||||
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
|
||||||
iconName: btn.iconName
|
|
||||||
},
|
},
|
||||||
onClick: btn.onCommandClick,
|
imageProps: btn.iconSrc ? { src: btn.iconSrc, alt: btn.iconAlt } : undefined,
|
||||||
key: `${btn.commandButtonLabel}${index}`,
|
iconName: btn.iconName
|
||||||
text: btn.commandButtonLabel || btn.tooltipText,
|
},
|
||||||
"data-test": btn.commandButtonLabel || btn.tooltipText,
|
onClick: (ev?: React.MouseEvent<HTMLElement, MouseEvent> | React.KeyboardEvent<HTMLElement>) => {
|
||||||
title: btn.tooltipText,
|
btn.onCommandClick(ev);
|
||||||
name: btn.commandButtonLabel || btn.tooltipText,
|
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label });
|
||||||
disabled: btn.disabled,
|
},
|
||||||
ariaLabel: btn.ariaLabel,
|
key: `${btn.commandButtonLabel}${index}`,
|
||||||
buttonStyles: {
|
text: label,
|
||||||
root: {
|
"data-test": label,
|
||||||
backgroundColor: backgroundColor,
|
title: btn.tooltipText,
|
||||||
height: buttonHeightPx,
|
name: label,
|
||||||
paddingRight: 0,
|
disabled: btn.disabled,
|
||||||
paddingLeft: 0,
|
ariaLabel: btn.ariaLabel,
|
||||||
minWidth: 24,
|
buttonStyles: {
|
||||||
marginLeft: isSplit ? 0 : 5,
|
root: {
|
||||||
marginRight: isSplit ? 0 : 5
|
backgroundColor: backgroundColor,
|
||||||
|
height: buttonHeightPx,
|
||||||
|
paddingRight: 0,
|
||||||
|
paddingLeft: 0,
|
||||||
|
minWidth: 24,
|
||||||
|
marginLeft: isSplit ? 0 : 5,
|
||||||
|
marginRight: isSplit ? 0 : 5
|
||||||
|
},
|
||||||
|
rootDisabled: {
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
pointerEvents: "auto"
|
||||||
|
},
|
||||||
|
splitButtonMenuButton: {
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
selectors: {
|
||||||
|
":hover": { backgroundColor: StyleConstants.AccentLight }
|
||||||
},
|
},
|
||||||
rootDisabled: {
|
width: 16
|
||||||
backgroundColor: backgroundColor,
|
},
|
||||||
pointerEvents: "auto"
|
label: { fontSize: StyleConstants.mediumFontSize },
|
||||||
},
|
rootHovered: { backgroundColor: StyleConstants.AccentLight },
|
||||||
splitButtonMenuButton: {
|
rootPressed: { backgroundColor: StyleConstants.AccentLight },
|
||||||
backgroundColor: backgroundColor,
|
splitButtonMenuButtonExpanded: {
|
||||||
selectors: {
|
backgroundColor: StyleConstants.AccentExtra,
|
||||||
":hover": { backgroundColor: StyleConstants.AccentLight }
|
selectors: {
|
||||||
},
|
":hover": { backgroundColor: StyleConstants.AccentLight }
|
||||||
width: 16
|
|
||||||
},
|
|
||||||
label: { fontSize: StyleConstants.mediumFontSize },
|
|
||||||
rootHovered: { backgroundColor: StyleConstants.AccentLight },
|
|
||||||
rootPressed: { backgroundColor: StyleConstants.AccentLight },
|
|
||||||
splitButtonMenuButtonExpanded: {
|
|
||||||
backgroundColor: StyleConstants.AccentExtra,
|
|
||||||
selectors: {
|
|
||||||
":hover": { backgroundColor: StyleConstants.AccentLight }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
splitButtonDivider: {
|
|
||||||
display: "none"
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
paddingLeft: 0,
|
|
||||||
paddingRight: 0
|
|
||||||
},
|
|
||||||
splitButtonContainer: {
|
|
||||||
marginLeft: 5,
|
|
||||||
marginRight: 5
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
className: btn.className,
|
splitButtonDivider: {
|
||||||
id: btn.id
|
display: "none"
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0
|
||||||
|
},
|
||||||
|
splitButtonContainer: {
|
||||||
|
marginLeft: 5,
|
||||||
|
marginRight: 5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
className: btn.className,
|
||||||
|
id: btn.id
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isSplit) {
|
||||||
|
// It's a split button
|
||||||
|
result.split = true;
|
||||||
|
|
||||||
|
result.subMenuProps = {
|
||||||
|
items: convertButton(btn.children, backgroundColor),
|
||||||
|
styles: {
|
||||||
|
list: {
|
||||||
|
// TODO Figure out how to do it the proper way with subComponentStyles.
|
||||||
|
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
||||||
|
selectors: {
|
||||||
|
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
|
||||||
|
".ms-ContextualMenu-link:hover": { backgroundColor: StyleConstants.AccentLight },
|
||||||
|
".ms-ContextualMenu-icon": { width: 16, height: 16 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isSplit) {
|
result.menuIconProps = {
|
||||||
// It's a split button
|
iconType: IconType.image,
|
||||||
result.split = true;
|
style: {
|
||||||
|
width: 12,
|
||||||
result.subMenuProps = {
|
paddingLeft: 1,
|
||||||
items: CommandBarUtil.convertButton(btn.children, backgroundColor),
|
paddingTop: 6
|
||||||
styles: {
|
},
|
||||||
list: {
|
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt }
|
||||||
// TODO Figure out how to do it the proper way with subComponentStyles.
|
};
|
||||||
// TODO Remove all this crazy styling once we adopt Ui-Fabric Azure themes
|
|
||||||
selectors: {
|
|
||||||
".ms-ContextualMenu-itemText": { fontSize: StyleConstants.mediumFontSize },
|
|
||||||
".ms-ContextualMenu-link:hover": { backgroundColor: StyleConstants.AccentLight },
|
|
||||||
".ms-ContextualMenu-icon": { width: 16, height: 16 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
result.menuIconProps = {
|
|
||||||
iconType: IconType.image,
|
|
||||||
style: {
|
|
||||||
width: 12,
|
|
||||||
paddingLeft: 1,
|
|
||||||
paddingTop: 6
|
|
||||||
},
|
|
||||||
imageProps: { src: ChevronDownIcon, alt: btn.iconAlt }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (btn.isDropdown) {
|
|
||||||
const selectedChild = _.find(btn.children, child => child.dropdownItemKey === btn.dropdownSelectedKey);
|
|
||||||
result.name = selectedChild?.commandButtonLabel || btn.dropdownPlaceholder;
|
|
||||||
|
|
||||||
const dropdownStyles: Partial<IDropdownStyles> = {
|
|
||||||
root: { margin: 5 },
|
|
||||||
dropdown: { width: btn.dropdownWidth },
|
|
||||||
title: { fontSize: 12, height: 30, lineHeight: 28 },
|
|
||||||
dropdownItem: { fontSize: 12, lineHeight: 28, minHeight: 30 },
|
|
||||||
dropdownItemSelected: { fontSize: 12, lineHeight: 28, minHeight: 30 }
|
|
||||||
};
|
|
||||||
|
|
||||||
result.commandBarButtonAs = (props: IComponentAsProps<ICommandBarItemProps>) => {
|
|
||||||
return (
|
|
||||||
<Dropdown
|
|
||||||
placeholder={btn.dropdownPlaceholder}
|
|
||||||
defaultSelectedKey={btn.dropdownSelectedKey}
|
|
||||||
onChange={(event: React.FormEvent<HTMLDivElement>, option?: IDropdownOption, index?: number): void =>
|
|
||||||
btn.children[index].onCommandClick(event)
|
|
||||||
}
|
|
||||||
options={btn.children.map((child: CommandButtonComponentProps) => ({
|
|
||||||
key: child.dropdownItemKey,
|
|
||||||
text: child.commandButtonLabel
|
|
||||||
}))}
|
|
||||||
styles={dropdownStyles}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (btn.isArcadiaPicker && btn.arcadiaProps) {
|
|
||||||
result.commandBarButtonAs = () => <ArcadiaMenuPicker {...btn.arcadiaProps} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createDivider(key: string): ICommandBarItemProps {
|
if (btn.isDropdown) {
|
||||||
return {
|
const selectedChild = _.find(btn.children, child => child.dropdownItemKey === btn.dropdownSelectedKey);
|
||||||
onRender: () => (
|
result.name = selectedChild?.commandButtonLabel || btn.dropdownPlaceholder;
|
||||||
<div className="dividerContainer">
|
|
||||||
<span />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
iconOnly: true,
|
|
||||||
disabled: true,
|
|
||||||
key: key
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createMemoryTracker(key: string, memoryUsageInfo: Observable<MemoryUsageInfo>): ICommandBarItemProps {
|
const dropdownStyles: Partial<IDropdownStyles> = {
|
||||||
return {
|
root: { margin: 5 },
|
||||||
key,
|
dropdown: { width: btn.dropdownWidth },
|
||||||
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />
|
title: { fontSize: 12, height: 30, lineHeight: 28 },
|
||||||
};
|
dropdownItem: { fontSize: 12, lineHeight: 28, minHeight: 30 },
|
||||||
}
|
dropdownItemSelected: { fontSize: 12, lineHeight: 28, minHeight: 30 }
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const onDropdownChange = (
|
||||||
|
event: React.FormEvent<HTMLDivElement>,
|
||||||
|
option?: IDropdownOption,
|
||||||
|
index?: number
|
||||||
|
): void => {
|
||||||
|
btn.children[index].onCommandClick(event);
|
||||||
|
TelemetryProcessor.trace(Action.ClickCommandBarButton, ActionModifiers.Mark, { label: option.text });
|
||||||
|
};
|
||||||
|
|
||||||
|
result.commandBarButtonAs = (props: IComponentAsProps<ICommandBarItemProps>) => {
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
placeholder={btn.dropdownPlaceholder}
|
||||||
|
defaultSelectedKey={btn.dropdownSelectedKey}
|
||||||
|
onChange={onDropdownChange}
|
||||||
|
options={btn.children.map((child: CommandButtonComponentProps) => ({
|
||||||
|
key: child.dropdownItemKey,
|
||||||
|
text: child.commandButtonLabel
|
||||||
|
}))}
|
||||||
|
styles={dropdownStyles}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btn.isArcadiaPicker && btn.arcadiaProps) {
|
||||||
|
result.commandBarButtonAs = () => <ArcadiaMenuPicker {...btn.arcadiaProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDivider = (key: string): ICommandBarItemProps => {
|
||||||
|
return {
|
||||||
|
onRender: () => (
|
||||||
|
<div className="dividerContainer">
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
iconOnly: true,
|
||||||
|
disabled: true,
|
||||||
|
key: key
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createMemoryTracker = (
|
||||||
|
key: string,
|
||||||
|
memoryUsageInfo: Observable<MemoryUsageInfo>
|
||||||
|
): ICommandBarItemProps => {
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
onRender: () => <MemoryTrackerComponent memoryUsageInfo={memoryUsageInfo} />
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -32,24 +32,27 @@ import {
|
||||||
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
|
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
|
||||||
import { sessions, kernels } from "rx-jupyter";
|
import { sessions, kernels } from "rx-jupyter";
|
||||||
import { RecordOf } from "immutable";
|
import { RecordOf } from "immutable";
|
||||||
|
import { AnyAction } from "redux";
|
||||||
|
|
||||||
import * as Constants from "../../../Common/Constants";
|
import * as Constants from "../../../Common/Constants";
|
||||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import * as CdbActions from "./actions";
|
import * as CdbActions from "./actions";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import { Action as TelemetryAction } from "../../../Shared/Telemetry/TelemetryConstants";
|
import { Action as TelemetryAction, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
import { CdbAppState } from "./types";
|
import { CdbAppState } from "./types";
|
||||||
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
|
import { decryptJWTToken } from "../../../Utils/AuthorizationUtils";
|
||||||
import * as TextFile from "./contents/file/text-file";
|
import * as TextFile from "./contents/file/text-file";
|
||||||
import { NotebookUtil } from "../NotebookUtil";
|
import { NotebookUtil } from "../NotebookUtil";
|
||||||
import { FileSystemUtil } from "../FileSystemUtil";
|
import { FileSystemUtil } from "../FileSystemUtil";
|
||||||
|
import * as cdbActions from "../NotebookComponent/actions";
|
||||||
|
import { Areas } from "../../../Common/Constants";
|
||||||
|
|
||||||
interface NotebookServiceConfig extends JupyterServerConfig {
|
interface NotebookServiceConfig extends JupyterServerConfig {
|
||||||
userPuid?: string;
|
userPuid?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const logToTelemetry = (state: CdbAppState, title: string, error?: string) => {
|
const logFailureToTelemetry = (state: CdbAppState, title: string, error?: string) => {
|
||||||
TelemetryProcessor.traceFailure(TelemetryAction.NotebookErrorNotification, {
|
TelemetryProcessor.traceFailure(TelemetryAction.NotebookErrorNotification, {
|
||||||
databaseAccountName: state.cdb.databaseAccountName,
|
databaseAccountName: state.cdb.databaseAccountName,
|
||||||
defaultExperience: state.cdb.defaultExperience,
|
defaultExperience: state.cdb.defaultExperience,
|
||||||
|
@ -311,7 +314,7 @@ export const launchWebSocketKernelEpic = (
|
||||||
kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
|
kernelSpecToLaunch = currentKernelspecs.defaultKernelName;
|
||||||
const msg = `No kernelspec name specified to launch, using default kernel: ${kernelSpecToLaunch}`;
|
const msg = `No kernelspec name specified to launch, using default kernel: ${kernelSpecToLaunch}`;
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||||
logToTelemetry(state$.value, "Launching alternate kernel", msg);
|
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
|
||||||
} else {
|
} else {
|
||||||
return of(
|
return of(
|
||||||
actions.launchKernelFailed({
|
actions.launchKernelFailed({
|
||||||
|
@ -337,7 +340,7 @@ export const launchWebSocketKernelEpic = (
|
||||||
msg += ` Using default kernel: ${kernelSpecToLaunch}`;
|
msg += ` Using default kernel: ${kernelSpecToLaunch}`;
|
||||||
}
|
}
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||||
logToTelemetry(state$.value, "Launching alternate kernel", msg);
|
logFailureToTelemetry(state$.value, "Launching alternate kernel", msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionPayload = {
|
const sessionPayload = {
|
||||||
|
@ -634,7 +637,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
|
||||||
const title = "Kernel restart";
|
const title = "Kernel restart";
|
||||||
const msg = "Kernel successfully restarted";
|
const msg = "Kernel successfully restarted";
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, msg);
|
||||||
logToTelemetry(state$.value, title, msg);
|
logFailureToTelemetry(state$.value, title, msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case actions.RESTART_KERNEL_FAILED:
|
case actions.RESTART_KERNEL_FAILED:
|
||||||
|
@ -645,7 +648,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
|
||||||
const title = "Save failure";
|
const title = "Save failure";
|
||||||
const msg = `Failed to save notebook: ${(action as actions.SaveFailed).payload.error}`;
|
const msg = `Failed to save notebook: ${(action as actions.SaveFailed).payload.error}`;
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||||
logToTelemetry(state$.value, title, msg);
|
logFailureToTelemetry(state$.value, title, msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case actions.FETCH_CONTENT_FAILED: {
|
case actions.FETCH_CONTENT_FAILED: {
|
||||||
|
@ -654,7 +657,7 @@ const notificationsToUserEpic = (action$: Observable<any>, state$: StateObservab
|
||||||
const title = "Fetching content failure";
|
const title = "Fetching content failure";
|
||||||
const msg = `Failed to fetch notebook content: ${filepath}, error: ${typedAction.payload.error}`;
|
const msg = `Failed to fetch notebook content: ${filepath}, error: ${typedAction.payload.error}`;
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||||
logToTelemetry(state$.value, title, msg);
|
logFailureToTelemetry(state$.value, title, msg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -679,7 +682,7 @@ const handleKernelConnectionLostEpic = (
|
||||||
|
|
||||||
const msg = "Notebook was disconnected from kernel";
|
const msg = "Notebook was disconnected from kernel";
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||||
logToTelemetry(state, "Error", "Kernel connection error");
|
logFailureToTelemetry(state, "Error", "Kernel connection error");
|
||||||
|
|
||||||
const host = selectors.currentHost(state);
|
const host = selectors.currentHost(state);
|
||||||
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host as RecordOf<JupyterHostRecordProps>);
|
const serverConfig: NotebookServiceConfig = selectors.serverConfig(host as RecordOf<JupyterHostRecordProps>);
|
||||||
|
@ -692,7 +695,7 @@ const handleKernelConnectionLostEpic = (
|
||||||
const msg =
|
const msg =
|
||||||
"Restarted kernel too many times. Please reload the page to enable Data Explorer to restart the kernel automatically.";
|
"Restarted kernel too many times. Please reload the page to enable Data Explorer to restart the kernel automatically.";
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, msg);
|
||||||
logToTelemetry(state, "Kernel restart error", msg);
|
logFailureToTelemetry(state, "Kernel restart error", msg);
|
||||||
|
|
||||||
const explorer = window.dataExplorer;
|
const explorer = window.dataExplorer;
|
||||||
if (explorer) {
|
if (explorer) {
|
||||||
|
@ -844,6 +847,105 @@ const closeContentFailedToFetchEpic = (
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const traceNotebookTelemetryEpic = (
|
||||||
|
action$: Observable<cdbActions.TraceNotebookTelemetryAction>,
|
||||||
|
state$: StateObservable<CdbAppState>
|
||||||
|
): Observable<{}> => {
|
||||||
|
return action$.pipe(
|
||||||
|
ofType(cdbActions.TRACE_NOTEBOOK_TELEMETRY),
|
||||||
|
mergeMap((action: cdbActions.TraceNotebookTelemetryAction) => {
|
||||||
|
const state = state$.value;
|
||||||
|
|
||||||
|
TelemetryProcessor.trace(action.payload.action, action.payload.actionModifier, {
|
||||||
|
...action.payload.data,
|
||||||
|
databaseAccountName: state.cdb.databaseAccountName,
|
||||||
|
defaultExperience: state.cdb.defaultExperience,
|
||||||
|
dataExplorerArea: Areas.Notebook
|
||||||
|
});
|
||||||
|
return EMPTY;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log notebook information to telemetry
|
||||||
|
* # raw cells, # markdown cells, # code cells, total
|
||||||
|
* @param action$
|
||||||
|
* @param state$
|
||||||
|
*/
|
||||||
|
const traceNotebookInfoEpic = (
|
||||||
|
action$: Observable<actions.FetchContentFulfilled>,
|
||||||
|
state$: StateObservable<AppState>
|
||||||
|
): Observable<{} | cdbActions.TraceNotebookTelemetryAction> => {
|
||||||
|
return action$.pipe(
|
||||||
|
ofType(actions.FETCH_CONTENT_FULFILLED),
|
||||||
|
mergeMap((action: { payload: any }) => {
|
||||||
|
const state = state$.value;
|
||||||
|
const contentRef = action.payload.contentRef;
|
||||||
|
const model = selectors.model(state, { contentRef });
|
||||||
|
|
||||||
|
// If it's not a notebook, we shouldn't be here
|
||||||
|
if (!model || model.type !== "notebook") {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataToLog = {
|
||||||
|
nbCodeCells: 0,
|
||||||
|
nbRawCells: 0,
|
||||||
|
nbMarkdownCells: 0,
|
||||||
|
nbCells: 0
|
||||||
|
};
|
||||||
|
for (let [id, cell] of selectors.notebook.cellMap(model)) {
|
||||||
|
switch (cell.cell_type) {
|
||||||
|
case "code":
|
||||||
|
dataToLog.nbCodeCells++;
|
||||||
|
break;
|
||||||
|
case "markdown":
|
||||||
|
dataToLog.nbMarkdownCells++;
|
||||||
|
break;
|
||||||
|
case "raw":
|
||||||
|
dataToLog.nbRawCells++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dataToLog.nbCells++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return of(
|
||||||
|
cdbActions.traceNotebookTelemetry({
|
||||||
|
action: TelemetryAction.NotebooksFetched,
|
||||||
|
actionModifier: ActionModifiers.Mark,
|
||||||
|
data: dataToLog
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log Kernel spec to start
|
||||||
|
* @param action$
|
||||||
|
* @param state$
|
||||||
|
*/
|
||||||
|
const traceNotebookKernelEpic = (
|
||||||
|
action$: Observable<AnyAction>,
|
||||||
|
state$: StateObservable<AppState>
|
||||||
|
): Observable<cdbActions.TraceNotebookTelemetryAction> => {
|
||||||
|
return action$.pipe(
|
||||||
|
ofType(actions.LAUNCH_KERNEL_SUCCESSFUL),
|
||||||
|
mergeMap((action: { payload: any; type: string }) => {
|
||||||
|
return of(
|
||||||
|
cdbActions.traceNotebookTelemetry({
|
||||||
|
action: TelemetryAction.NotebooksKernelSpecName,
|
||||||
|
actionModifier: ActionModifiers.Mark,
|
||||||
|
data: {
|
||||||
|
kernelSpecName: action.payload.kernel.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const allEpics = [
|
export const allEpics = [
|
||||||
addInitialCodeCellEpic,
|
addInitialCodeCellEpic,
|
||||||
focusInitialCodeCellEpic,
|
focusInitialCodeCellEpic,
|
||||||
|
@ -856,5 +958,8 @@ export const allEpics = [
|
||||||
executeFocusedCellAndFocusNextEpic,
|
executeFocusedCellAndFocusNextEpic,
|
||||||
closeUnsupportedMimetypesEpic,
|
closeUnsupportedMimetypesEpic,
|
||||||
closeContentFailedToFetchEpic,
|
closeContentFailedToFetchEpic,
|
||||||
restartWebSocketKernelEpic
|
restartWebSocketKernelEpic,
|
||||||
|
traceNotebookTelemetryEpic,
|
||||||
|
traceNotebookInfoEpic,
|
||||||
|
traceNotebookKernelEpic
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
|
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
|
||||||
import { Action } from "redux";
|
import { Action } from "redux";
|
||||||
import { Areas } from "../../../Common/Constants";
|
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
|
||||||
import * as cdbActions from "./actions";
|
import * as cdbActions from "./actions";
|
||||||
import { CdbRecord } from "./types";
|
import { CdbRecord } from "./types";
|
||||||
|
|
||||||
|
@ -72,17 +70,6 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
|
||||||
return state.set("hoveredCellId", typedAction.payload.cellId);
|
return state.set("hoveredCellId", typedAction.payload.cellId);
|
||||||
}
|
}
|
||||||
|
|
||||||
case cdbActions.TRACE_NOTEBOOK_TELEMETRY: {
|
|
||||||
const typedAction = action as cdbActions.TraceNotebookTelemetryAction;
|
|
||||||
TelemetryProcessor.trace(typedAction.payload.action, typedAction.payload.actionModifier, {
|
|
||||||
...typedAction.payload.data,
|
|
||||||
databaseAccountName: state.databaseAccountName,
|
|
||||||
defaultExperience: state.defaultExperience,
|
|
||||||
dataExplorerArea: Areas.Notebook
|
|
||||||
});
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
case cdbActions.UPDATE_NOTEBOOK_PARENT_DOM_ELTS: {
|
case cdbActions.UPDATE_NOTEBOOK_PARENT_DOM_ELTS: {
|
||||||
const typedAction = action as cdbActions.UpdateNotebookParentDomEltAction;
|
const typedAction = action as cdbActions.UpdateNotebookParentDomEltAction;
|
||||||
var parentEltsMap = state.get("currentNotebookParentElements");
|
var parentEltsMap = state.get("currentNotebookParentElements");
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { CellToolbarContext } from "@nteract/stateful-components";
|
||||||
import { CellType, CellId } from "@nteract/commutable";
|
import { CellType, CellId } from "@nteract/commutable";
|
||||||
import * as selectors from "@nteract/selectors";
|
import * as selectors from "@nteract/selectors";
|
||||||
import { RecordOf } from "immutable";
|
import { RecordOf } from "immutable";
|
||||||
|
import * as cdbActions from "../NotebookComponent/actions";
|
||||||
|
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
|
||||||
|
|
||||||
export interface ComponentProps {
|
export interface ComponentProps {
|
||||||
contentRef: ContentRef;
|
contentRef: ContentRef;
|
||||||
|
@ -29,6 +31,7 @@ interface DispatchProps {
|
||||||
moveCell: (destinationId: CellId, above: boolean) => void;
|
moveCell: (destinationId: CellId, above: boolean) => void;
|
||||||
clearOutputs: () => void;
|
clearOutputs: () => void;
|
||||||
deleteCell: () => void;
|
deleteCell: () => void;
|
||||||
|
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StateProps {
|
interface StateProps {
|
||||||
|
@ -48,12 +51,18 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||||
{
|
{
|
||||||
key: "Run",
|
key: "Run",
|
||||||
text: "Run",
|
text: "Run",
|
||||||
onClick: this.props.executeCell
|
onClick: () => {
|
||||||
|
this.props.executeCell();
|
||||||
|
this.props.traceNotebookTelemetry(Action.NotebooksExecuteCellFromMenu, ActionModifiers.Mark);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Clear Outputs",
|
key: "Clear Outputs",
|
||||||
text: "Clear Outputs",
|
text: "Clear Outputs",
|
||||||
onClick: this.props.clearOutputs
|
onClick: () => {
|
||||||
|
this.props.clearOutputs();
|
||||||
|
this.props.traceNotebookTelemetry(Action.NotebooksClearOutputsFromMenu, ActionModifiers.Mark);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Divider",
|
key: "Divider",
|
||||||
|
@ -64,31 +73,43 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||||
|
|
||||||
items = items.concat([
|
items = items.concat([
|
||||||
{
|
{
|
||||||
key: "Divider",
|
key: "Divider2",
|
||||||
itemType: ContextualMenuItemType.Divider
|
itemType: ContextualMenuItemType.Divider
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Insert Code Cell Above",
|
key: "Insert Code Cell Above",
|
||||||
text: "Insert Code Cell Above",
|
text: "Insert Code Cell Above",
|
||||||
onClick: this.props.insertCodeCellAbove
|
onClick: () => {
|
||||||
|
this.props.insertCodeCellAbove();
|
||||||
|
this.props.traceNotebookTelemetry(Action.NotebooksInsertCodeCellAboveFromMenu, ActionModifiers.Mark);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Insert Code Cell Below",
|
key: "Insert Code Cell Below",
|
||||||
text: "Insert Code Cell Below",
|
text: "Insert Code Cell Below",
|
||||||
onClick: this.props.insertCodeCellBelow
|
onClick: () => {
|
||||||
|
this.props.insertCodeCellBelow();
|
||||||
|
this.props.traceNotebookTelemetry(Action.NotebooksInsertCodeCellBelowFromMenu, ActionModifiers.Mark);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Insert Text Cell Above",
|
key: "Insert Text Cell Above",
|
||||||
text: "Insert Text Cell Above",
|
text: "Insert Text Cell Above",
|
||||||
onClick: this.props.insertTextCellAbove
|
onClick: () => {
|
||||||
|
this.props.insertTextCellAbove();
|
||||||
|
this.props.traceNotebookTelemetry(Action.NotebooksInsertTextCellAboveFromMenu, ActionModifiers.Mark);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Insert Text Cell Below",
|
key: "Insert Text Cell Below",
|
||||||
text: "Insert Text Cell Below",
|
text: "Insert Text Cell Below",
|
||||||
onClick: this.props.insertTextCellBelow
|
onClick: () => {
|
||||||
|
this.props.insertTextCellBelow();
|
||||||
|
this.props.traceNotebookTelemetry(Action.NotebooksInsertTextCellBelowFromMenu, ActionModifiers.Mark);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Divider",
|
key: "Divider3",
|
||||||
itemType: ContextualMenuItemType.Divider
|
itemType: ContextualMenuItemType.Divider
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
@ -98,7 +119,10 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||||
moveItems.push({
|
moveItems.push({
|
||||||
key: "Move Cell Up",
|
key: "Move Cell Up",
|
||||||
text: "Move Cell Up",
|
text: "Move Cell Up",
|
||||||
onClick: () => this.props.moveCell(this.props.cellIdAbove, true)
|
onClick: () => {
|
||||||
|
this.props.moveCell(this.props.cellIdAbove, true);
|
||||||
|
this.props.traceNotebookTelemetry(Action.NotebooksMoveCellUpFromMenu, ActionModifiers.Mark);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,13 +130,16 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||||
moveItems.push({
|
moveItems.push({
|
||||||
key: "Move Cell Down",
|
key: "Move Cell Down",
|
||||||
text: "Move Cell Down",
|
text: "Move Cell Down",
|
||||||
onClick: () => this.props.moveCell(this.props.cellIdBelow, false)
|
onClick: () => {
|
||||||
|
this.props.moveCell(this.props.cellIdBelow, false);
|
||||||
|
this.props.traceNotebookTelemetry(Action.NotebooksMoveCellDownFromMenu, ActionModifiers.Mark);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (moveItems.length > 0) {
|
if (moveItems.length > 0) {
|
||||||
moveItems.push({
|
moveItems.push({
|
||||||
key: "Divider",
|
key: "Divider4",
|
||||||
itemType: ContextualMenuItemType.Divider
|
itemType: ContextualMenuItemType.Divider
|
||||||
});
|
});
|
||||||
items = items.concat(moveItems);
|
items = items.concat(moveItems);
|
||||||
|
@ -121,7 +148,10 @@ class BaseToolbar extends React.PureComponent<ComponentProps & DispatchProps & S
|
||||||
items.push({
|
items.push({
|
||||||
key: "Delete Cell",
|
key: "Delete Cell",
|
||||||
text: "Delete Cell",
|
text: "Delete Cell",
|
||||||
onClick: this.props.deleteCell
|
onClick: () => {
|
||||||
|
this.props.deleteCell();
|
||||||
|
this.props.traceNotebookTelemetry(Action.DeleteCellFromMenu, ActionModifiers.Mark);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const menuItemLabel = "More";
|
const menuItemLabel = "More";
|
||||||
|
@ -156,7 +186,9 @@ const mapDispatchToProps = (
|
||||||
moveCell: (destinationId: CellId, above: boolean) =>
|
moveCell: (destinationId: CellId, above: boolean) =>
|
||||||
dispatch(actions.moveCell({ id, contentRef, destinationId, above })),
|
dispatch(actions.moveCell({ id, contentRef, destinationId, above })),
|
||||||
clearOutputs: () => dispatch(actions.clearOutputs({ id, contentRef })),
|
clearOutputs: () => dispatch(actions.clearOutputs({ id, contentRef })),
|
||||||
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef }))
|
deleteCell: () => dispatch(actions.deleteCell({ id, contentRef })),
|
||||||
|
traceNotebookTelemetry: (action: Action, actionModifier?: string, data?: any) =>
|
||||||
|
dispatch(cdbActions.traceNotebookTelemetry({ action, actionModifier, data }))
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {
|
const makeMapStateToProps = (state: AppState, ownProps: ComponentProps): ((state: AppState) => StateProps) => {
|
||||||
|
|
|
@ -74,6 +74,30 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
this.triggerRender();
|
this.triggerRender();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private traceMyNotebookTreeInfo() {
|
||||||
|
const myNotebooksTree = this.myNotebooksContentRoot;
|
||||||
|
if (myNotebooksTree.children) {
|
||||||
|
// Count 1st generation children (tree is lazy-loaded)
|
||||||
|
const nodeCounts = { files: 0, notebooks: 0, directories: 0 };
|
||||||
|
myNotebooksTree.children.forEach(treeNode => {
|
||||||
|
switch ((treeNode as NotebookContentItem).type) {
|
||||||
|
case NotebookContentItemType.File:
|
||||||
|
nodeCounts.files++;
|
||||||
|
break;
|
||||||
|
case NotebookContentItemType.Directory:
|
||||||
|
nodeCounts.directories++;
|
||||||
|
break;
|
||||||
|
case NotebookContentItemType.Notebook:
|
||||||
|
nodeCounts.notebooks++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
TelemetryProcessor.trace(Action.RefreshResourceTreeMyNotebooks, ActionModifiers.Mark, { ...nodeCounts });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public renderComponent(): JSX.Element {
|
public renderComponent(): JSX.Element {
|
||||||
const dataRootNode = this.buildDataTree();
|
const dataRootNode = this.buildDataTree();
|
||||||
const notebooksRootNode = this.buildNotebooksTrees();
|
const notebooksRootNode = this.buildNotebooksTrees();
|
||||||
|
@ -116,7 +140,10 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
// Only if notebook server is available we can refresh
|
// Only if notebook server is available we can refresh
|
||||||
if (this.container.notebookServerInfo().notebookServerEndpoint) {
|
if (this.container.notebookServerInfo().notebookServerEndpoint) {
|
||||||
refreshTasks.push(
|
refreshTasks.push(
|
||||||
this.container.refreshContentItem(this.myNotebooksContentRoot).then(() => this.triggerRender())
|
this.container.refreshContentItem(this.myNotebooksContentRoot).then(() => {
|
||||||
|
this.triggerRender();
|
||||||
|
this.traceMyNotebookTreeInfo();
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -71,8 +71,22 @@ export enum Action {
|
||||||
NotebooksGitHubManageRepo,
|
NotebooksGitHubManageRepo,
|
||||||
NotebooksGitHubCommit,
|
NotebooksGitHubCommit,
|
||||||
NotebooksGitHubDisconnect,
|
NotebooksGitHubDisconnect,
|
||||||
|
NotebooksFetched,
|
||||||
|
NotebooksKernelSpecName,
|
||||||
|
NotebooksExecuteCellFromMenu,
|
||||||
|
NotebooksClearOutputsFromMenu,
|
||||||
|
NotebooksInsertCodeCellAboveFromMenu,
|
||||||
|
NotebooksInsertCodeCellBelowFromMenu,
|
||||||
|
NotebooksInsertTextCellAboveFromMenu,
|
||||||
|
NotebooksInsertTextCellBelowFromMenu,
|
||||||
|
NotebooksMoveCellUpFromMenu,
|
||||||
|
NotebooksMoveCellDownFromMenu,
|
||||||
|
DeleteCellFromMenu,
|
||||||
OpenTerminal,
|
OpenTerminal,
|
||||||
CreateMongoCollectionWithWildcardIndex
|
CreateMongoCollectionWithWildcardIndex,
|
||||||
|
ClickCommandBarButton,
|
||||||
|
RefreshResourceTreeMyNotebooks,
|
||||||
|
ClickResourceTreeNodeContextMenuItem
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionModifiers = {
|
export const ActionModifiers = {
|
||||||
|
|
Loading…
Reference in New Issue