Add telemetry for Notebooks Gallery and other updates (#413)

* Add telemetry for Notebooks Gallery

* More changes

* Address feedback and fix lint error

* Fix margins for My published work
This commit is contained in:
Tanuj Mittal 2021-02-03 14:48:50 +05:30 committed by GitHub
parent e0063c76d9
commit 5038a01079
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 329 additions and 67 deletions

View File

@ -79,12 +79,16 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
<Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
<Text variant="small" nowrap>
{this.props.data.tags?.map((tag, index, array) => (
{this.props.data.tags ? (
this.props.data.tags.map((tag, index, array) => (
<span key={tag}>
<Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
{index === array.length - 1 ? <></> : ", "}
</span>
))}
))
) : (
<br />
)}
</Text>
<Text

View File

@ -2,7 +2,9 @@ import * as React from "react";
import { JunoClient } from "../../../Juno/JunoClient";
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
import { trace, traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
export interface CodeOfConductComponentProps {
junoClient: JunoClient;
@ -14,11 +16,11 @@ interface CodeOfConductComponentState {
}
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
private viewCodeOfConductTraced: boolean;
private descriptionPara1: string;
private descriptionPara2: string;
private descriptionPara3: string;
private link1: { label: string; url: string };
private link2: { label: string; url: string };
constructor(props: CodeOfConductComponentProps) {
super(props);
@ -27,23 +29,34 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
readCodeOfConduct: false,
};
this.descriptionPara1 = "Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement";
this.descriptionPara2 =
"Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.";
this.descriptionPara3 = "In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the ";
this.link1 = { label: "code of conduct", url: CodeOfConductEndpoints.codeOfConduct };
this.link2 = { label: "privacy statement", url: CodeOfConductEndpoints.privacyStatement };
this.descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct";
this.descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.";
this.descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the ";
this.link1 = { label: "code of conduct.", url: CodeOfConductEndpoints.codeOfConduct };
}
private async acceptCodeOfConduct(): Promise<void> {
const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct);
try {
const response = await this.props.junoClient.acceptCodeOfConduct();
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when accepting code of conduct`);
}
traceSuccess(Action.NotebooksGalleryAcceptCodeOfConduct, startKey);
this.props.onAcceptCodeOfConduct(response.data);
} catch (error) {
traceFailure(
Action.NotebooksGalleryAcceptCodeOfConduct,
{
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "CodeOfConductComponent/acceptCodeOfConduct", "Failed to accept code of conduct");
}
}
@ -53,6 +66,11 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
};
public render(): JSX.Element {
if (!this.viewCodeOfConductTraced) {
this.viewCodeOfConductTraced = true;
trace(Action.NotebooksGalleryViewCodeOfConduct);
}
return (
<Stack tokens={{ childrenGap: 20 }}>
<Stack.Item>
@ -69,10 +87,6 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
<Link href={this.link1.url} target="_blank">
{this.link1.label}
</Link>
{" and "}
<Link href={this.link2.url} target="_blank">
{this.link2.label}
</Link>
</Text>
</Stack.Item>
@ -87,7 +101,7 @@ export class CodeOfConductComponent extends React.Component<CodeOfConductCompone
fontSize: 12,
},
}}
label="I have read and accepted the code of conduct and privacy statement"
label="I have read and accepted the code of conduct."
onChange={this.onChangeCheckbox}
/>
</Stack.Item>

View File

@ -9,6 +9,7 @@ import {
IPivotProps,
IRectangle,
Label,
Link,
List,
Overlay,
Pivot,
@ -28,6 +29,8 @@ import Explorer from "../../Explorer";
import { CodeOfConductComponent } from "./CodeOfConductComponent";
import { InfoComponent } from "./InfoComponent/InfoComponent";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
export interface GalleryViewerComponentProps {
container?: Explorer;
@ -87,6 +90,12 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private readonly sortingOptions: IDropdownOption[];
private viewGalleryTraced: boolean;
private viewOfficialSamplesTraced: boolean;
private viewPublicGalleryTraced: boolean;
private viewFavoritesTraced: boolean;
private viewPublishedNotebooksTraced: boolean;
private sampleNotebooks: IGalleryItem[];
private publicNotebooks: IGalleryItem[];
private favoriteNotebooks: IGalleryItem[];
@ -138,6 +147,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
}
public render(): JSX.Element {
this.traceViewGallery();
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
if (this.props.container?.isGalleryPublishEnabled()) {
@ -185,11 +196,58 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
);
}
private traceViewGallery = (): void => {
if (!this.viewGalleryTraced) {
this.viewGalleryTraced = true;
trace(Action.NotebooksGalleryViewGallery);
}
switch (this.state.selectedTab) {
case GalleryTab.OfficialSamples:
if (!this.viewOfficialSamplesTraced) {
this.resetViewGalleryTabTracedFlags();
this.viewOfficialSamplesTraced = true;
trace(Action.NotebooksGalleryViewOfficialSamples);
}
break;
case GalleryTab.PublicGallery:
if (!this.viewPublicGalleryTraced) {
this.resetViewGalleryTabTracedFlags();
this.viewPublicGalleryTraced = true;
trace(Action.NotebooksGalleryViewPublicGallery);
}
break;
case GalleryTab.Favorites:
if (!this.viewFavoritesTraced) {
this.resetViewGalleryTabTracedFlags();
this.viewFavoritesTraced = true;
trace(Action.NotebooksGalleryViewFavorites);
}
break;
case GalleryTab.Published:
if (!this.viewPublishedNotebooksTraced) {
this.resetViewGalleryTabTracedFlags();
this.viewPublishedNotebooksTraced = true;
trace(Action.NotebooksGalleryViewPublishedNotebooks);
}
break;
default:
throw new Error(`Unknown selected tab ${this.state.selectedTab}`);
}
};
private resetViewGalleryTabTracedFlags = (): void => {
this.viewOfficialSamplesTraced = false;
this.viewPublicGalleryTraced = false;
this.viewFavoritesTraced = false;
this.viewPublishedNotebooksTraced = false;
};
private isEmptyData = (data: IGalleryItem[]): boolean => {
return !data || data.length === 0;
};
private createEmptyTabContent = (iconName: string, line1: string, line2: string): JSX.Element => {
private createEmptyTabContent = (iconName: string, line1: JSX.Element, line2: JSX.Element): JSX.Element => {
return (
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} />
@ -223,8 +281,12 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
content: this.isEmptyData(data)
? this.createEmptyTabContent(
"ContactHeart",
"You have not favorited anything",
"Favorite any notebook from Official samples or Public gallery"
<>You don&apos;t have any favorites yet</>,
<>
Favorite any notebook from the{" "}
<Link onClick={() => this.setState({ selectedTab: GalleryTab.OfficialSamples })}>official samples</Link>{" "}
or <Link onClick={() => this.setState({ selectedTab: GalleryTab.PublicGallery })}>public gallery</Link>
</>
)
: this.createSearchBarHeader(this.createCardsTabContent(data)),
};
@ -236,8 +298,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
content: this.isEmptyData(data)
? this.createEmptyTabContent(
"Contact",
"You have not published anything",
"Publish your sample notebooks to share your published work with others"
<>
You have not published anything to the{" "}
<Link onClick={() => this.setState({ selectedTab: GalleryTab.PublicGallery })}>public gallery</Link> yet
</>,
<>Publish your notebooks to share your work with other users</>
)
: this.createPublishedNotebooksTabContent(data),
};
@ -250,7 +315,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
{published?.length > 0 &&
this.createPublishedNotebooksSectionContent(
undefined,
"You have successfully published the following notebook(s) to public gallery and shared with other Azure Cosmos DB users.",
"You have successfully published and shared the following notebook(s) to the public gallery.",
this.createCardsTabContent(published)
)}
{underReview?.length > 0 &&
@ -278,8 +343,10 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
): JSX.Element => {
return (
<Stack tokens={{ childrenGap: 10 }}>
{title && <Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{title}</Text>}
{description && <Text>{description}</Text>}
{title && (
<Text styles={{ root: { fontWeight: FontWeights.semibold, marginLeft: 10, marginRight: 10 } }}>{title}</Text>
)}
{description && <Text styles={{ root: { marginLeft: 10, marginRight: 10 } }}>{description}</Text>}
{content}
</Stack>
);
@ -344,7 +411,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private createPolicyViolationsListContent(data: IGalleryItem[]): JSX.Element {
return (
<table>
<table style={{ margin: 10 }}>
<tbody>
<tr>
<th>Name</th>

View File

@ -17,35 +17,28 @@ exports[`CodeOfConductComponent renders 1`] = `
}
}
>
Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement
Azure Cosmos DB Notebook Gallery - Code of Conduct
</Text>
</StackItem>
<StackItem>
<Text>
Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.
The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB.
</Text>
</StackItem>
<StackItem>
<Text>
In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the
In order to view and publish your samples to the gallery, you must accept the
<StyledLinkBase
href="https://aka.ms/cosmos-code-of-conduct"
target="_blank"
>
code of conduct
</StyledLinkBase>
and
<StyledLinkBase
href="https://aka.ms/ms-privacy-policy"
target="_blank"
>
privacy statement
code of conduct.
</StyledLinkBase>
</Text>
</StackItem>
<StackItem>
<StyledCheckboxBase
label="I have read and accepted the code of conduct and privacy statement"
label="I have read and accepted the code of conduct."
onChange={[Function]}
styles={
Object {

View File

@ -18,7 +18,9 @@ import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
import { DialogHost } from "../../../Utils/GalleryUtils";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils";
import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
export interface NotebookViewerComponentProps {
container?: Explorer;
@ -77,6 +79,11 @@ export class NotebookViewerComponent
}
private async loadNotebookContent(): Promise<void> {
const startKey = traceStart(Action.NotebooksGalleryViewNotebook, {
notebookUrl: this.props.notebookUrl,
notebookId: this.props.galleryItem?.id,
});
try {
const response = await fetch(this.props.notebookUrl);
if (!response.ok) {
@ -84,6 +91,12 @@ export class NotebookViewerComponent
throw new Error(`Received HTTP ${response.status} while fetching ${this.props.notebookUrl}`);
}
traceSuccess(
Action.NotebooksGalleryViewNotebook,
{ notebookUrl: this.props.notebookUrl, notebookId: this.props.galleryItem?.id },
startKey
);
const notebook: Notebook = await response.json();
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
this.notebookComponentBootstrapper.setContent("json", notebook);
@ -98,6 +111,17 @@ export class NotebookViewerComponent
SessionStorageUtility.setEntry(this.props.galleryItem?.id, "true");
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryViewNotebook,
{
notebookUrl: this.props.notebookUrl,
notebookId: this.props.galleryItem?.id,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
this.setState({ showProgressBar: false });
handleError(error, "NotebookViewerComponent/loadNotebookContent", "Failed to load notebook content");
}

View File

@ -2253,7 +2253,7 @@ export default class Explorer {
return Promise.resolve(false);
}
public async publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): Promise<void> {
public async publishNotebook(name: string, content: string | unknown, parentDomElement?: HTMLElement): Promise<void> {
if (this.notebookManager) {
await this.notebookManager.openPublishNotebookPane(
name,

View File

@ -10,8 +10,10 @@ import { ImmutableNotebook } from "@nteract/commutable/src";
import { toJS } from "@nteract/commutable";
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
import { HttpStatusCodes } from "../../Common/Constants";
import { handleError, getErrorMessage } from "../../Common/ErrorHandlingUtils";
import { handleError, getErrorMessage, getErrorStack } from "../../Common/ErrorHandlingUtils";
import { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
export class PublishNotebookPaneAdapter implements ReactAdapter {
parameters: ko.Observable<number>;
@ -141,11 +143,18 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
this.isExecuting = true;
this.triggerRender();
let startKey: number;
try {
if (!this.name || !this.description || !this.author) {
throw new Error("Name, description, and author are required");
}
startKey = traceStart(Action.NotebooksGalleryPublish, {
databaseAccountName: this.container.databaseAccount()?.name,
defaultExperience: this.container.defaultExperience(),
});
const response = await this.junoClient.publishNotebook(
this.name,
this.description,
@ -158,7 +167,10 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
const data = response.data;
if (data) {
let isPublishPending = false;
if (data.pendingScanJobIds?.length > 0) {
isPublishPending = true;
NotificationConsoleUtils.logConsoleInfo(
`Content of ${this.name} is currently being scanned for illegal content. It will not be available in the public gallery until the review is complete (may take a few days).`
);
@ -166,8 +178,30 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
NotificationConsoleUtils.logConsoleInfo(`Published ${this.name} to gallery`);
this.container.openGallery(GalleryTab.Published);
}
traceSuccess(
Action.NotebooksGalleryPublish,
{
databaseAccountName: this.container.databaseAccount()?.name,
defaultExperience: this.container.defaultExperience(),
notebookId: data.id,
isPublishPending,
},
startKey
);
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryPublish,
{
databaseAccountName: this.container.databaseAccount()?.name,
defaultExperience: this.container.defaultExperience(),
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
const errorMessage = getErrorMessage(error);
this.formError = `Failed to publish ${this.name} to gallery`;
this.formErrorDetail = `${errorMessage}`;

View File

@ -14,7 +14,7 @@ export interface PublishNotebookPaneProps {
notebookAuthor: string;
notebookCreatedDate: string;
notebookObject: ImmutableNotebook;
notebookParentDomElement: HTMLElement;
notebookParentDomElement?: HTMLElement;
onChangeName: (newValue: string) => void;
onChangeDescription: (newValue: string) => void;
onChangeTags: (newValue: string) => void;
@ -110,7 +110,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
};
this.descriptionPara1 =
"This notebook has your data. Please make sure you delete any sensitive data/output before publishing.";
"When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing.";
this.descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension(
this.props.notebookName,
@ -140,16 +140,20 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
this.props.onError(formError, formErrorDetail, area);
};
const options: ImageTypes[] = [ImageTypes.Url, ImageTypes.CustomImage];
if (this.props.notebookParentDomElement) {
options.push(ImageTypes.TakeScreenshot);
if (this.props.notebookObject) {
options.push(ImageTypes.UseFirstDisplayOutput);
}
}
this.thumbnailSelectorProps = {
label: "Cover image",
defaultSelectedKey: ImageTypes.Url,
ariaLabel: "Cover image",
options: [
ImageTypes.Url,
ImageTypes.CustomImage,
ImageTypes.TakeScreenshot,
ImageTypes.UseFirstDisplayOutput,
].map((value: string) => ({ text: value, key: value })),
options: options.map((value: string) => ({ text: value, key: value })),
onChange: async (event, options) => {
this.props.clearFormError();
if (options.text === ImageTypes.TakeScreenshot) {
@ -301,9 +305,9 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
policyViolations: undefined,
pendingScanJobIds: undefined,
}}
isFavorite={false}
showDownload={true}
showDelete={true}
isFavorite={undefined}
showDownload={false}
showDelete={false}
onClick={undefined}
onTagClick={undefined}
onFavoriteClick={undefined}

View File

@ -14,7 +14,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
>
<StackItem>
<Text>
This notebook has your data. Please make sure you delete any sensitive data/output before publishing.
When published, this notebook will appear in the Azure Cosmos DB notebooks public gallery. Make sure you have removed any sensitive data or output before publishing.
</Text>
</StackItem>
<StackItem>
@ -65,14 +65,6 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
"key": "Custom Image",
"text": "Custom Image",
},
Object {
"key": "Take Screenshot",
"text": "Take Screenshot",
},
Object {
"key": "Use First Display Output",
"text": "Use First Display Output",
},
]
}
/>
@ -112,9 +104,8 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
"views": 0,
}
}
isFavorite={false}
showDelete={true}
showDownload={true}
showDelete={false}
showDownload={false}
/>
</StackItem>
</Stack>

View File

@ -716,6 +716,19 @@ export class ResourceTreeAdapter implements ReactAdapter {
},
];
if (item.type === NotebookContentItemType.Notebook) {
items.push({
label: "Publish to gallery",
iconSrc: undefined, // TODO
onClick: async () => {
const content = await this.container.readFile(item);
if (content) {
await this.container.publishNotebook(item.name, content);
}
},
});
}
// "Copy to ..." isn't needed if github locations are not available
if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
items = items.filter((item) => item.label !== "Copy to ...");

View File

@ -92,6 +92,23 @@ export enum Action {
SettingsV2Updated,
SettingsV2Discarded,
MongoIndexUpdated,
NotebooksGalleryPublish,
NotebooksGalleryReportAbuse,
NotebooksGalleryClickReportAbuse,
NotebooksGalleryViewCodeOfConduct,
NotebooksGalleryAcceptCodeOfConduct,
NotebooksGalleryFavorite,
NotebooksGalleryUnfavorite,
NotebooksGalleryClickDelete,
NotebooksGalleryDelete,
NotebooksGalleryClickDownload,
NotebooksGalleryDownload,
NotebooksGalleryViewNotebook,
NotebooksGalleryViewGallery,
NotebooksGalleryViewOfficialSamples,
NotebooksGalleryViewPublicGallery,
NotebooksGalleryViewFavorites,
NotebooksGalleryViewPublishedNotebooks,
}
export const ActionModifiers = {

View File

@ -9,8 +9,10 @@ import {
import Explorer from "../Explorer/Explorer";
import { IChoiceGroupOption, IChoiceGroupProps, IProgressIndicatorProps } from "office-ui-fabric-react";
import { TextFieldProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
import { handleError } from "../Common/ErrorHandlingUtils";
import { getErrorMessage, getErrorStack, handleError } from "../Common/ErrorHandlingUtils";
import { HttpStatusCodes } from "../Common/Constants";
import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../Shared/Telemetry/TelemetryConstants";
const defaultSelectedAbuseCategory = "Other";
const abuseCategories: IChoiceGroupOption[] = [
@ -109,6 +111,8 @@ export function reportAbuse(
dialogHost: DialogHost,
onComplete: (success: boolean) => void
): void {
trace(Action.NotebooksGalleryClickReportAbuse, ActionModifiers.Mark, { notebookId: data.id });
const notebookId = data.id;
let abuseCategory = defaultSelectedAbuseCategory;
let additionalDetails: string;
@ -131,6 +135,8 @@ export function reportAbuse(
true
);
const startKey = traceStart(Action.NotebooksGalleryReportAbuse, { notebookId: data.id });
try {
const response = await junoClient.reportAbuse(notebookId, abuseCategory, additionalDetails);
if (response.status !== HttpStatusCodes.Accepted) {
@ -147,8 +153,20 @@ export function reportAbuse(
}
);
traceSuccess(Action.NotebooksGalleryReportAbuse, { notebookId: data.id, abuseCategory }, startKey);
onComplete(response.data);
} catch (error) {
traceFailure(
Action.NotebooksGalleryReportAbuse,
{
notebookId: data.id,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(
error,
"GalleryUtils/reportAbuse",
@ -195,6 +213,12 @@ export function downloadItem(
data: IGalleryItem,
onComplete: (item: IGalleryItem) => void
): void {
trace(Action.NotebooksGalleryClickDownload, ActionModifiers.Mark, {
notebookId: data.id,
downloadCount: data.downloads,
isSample: data.isSample,
});
const name = data.name;
container.showOkCancelModalDialog(
"Download to My Notebooks",
@ -206,6 +230,11 @@ export function downloadItem(
`Downloading ${name} to My Notebooks`
);
const startKey = traceStart(Action.NotebooksGalleryDownload, {
notebookId: data.id,
downloadCount: data.downloads,
});
try {
const response = await junoClient.getNotebookContent(data.id);
if (!response.data) {
@ -220,9 +249,25 @@ export function downloadItem(
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
if (increaseDownloadResponse.data) {
traceSuccess(
Action.NotebooksGalleryDownload,
{ notebookId: data.id, downloadCount: increaseDownloadResponse.data.downloads },
startKey
);
onComplete(increaseDownloadResponse.data);
}
} catch (error) {
traceFailure(
Action.NotebooksGalleryDownload,
{
notebookId: data.id,
downloadCount: data.downloads,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "GalleryUtils/downloadItem", `Failed to download ${data.name}`);
}
@ -240,14 +285,36 @@ export async function favoriteItem(
onComplete: (item: IGalleryItem) => void
): Promise<void> {
if (container) {
const startKey = traceStart(Action.NotebooksGalleryFavorite, {
notebookId: data.id,
favoriteCount: data.favorites,
});
try {
const response = await junoClient.favoriteNotebook(data.id);
if (!response.data) {
throw new Error(`Received HTTP ${response.status} when favoriting ${data.name}`);
}
traceSuccess(
Action.NotebooksGalleryFavorite,
{ notebookId: data.id, favoriteCount: response.data.favorites },
startKey
);
onComplete(response.data);
} catch (error) {
traceFailure(
Action.NotebooksGalleryFavorite,
{
notebookId: data.id,
favoriteCount: data.favorites,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "GalleryUtils/favoriteItem", `Failed to favorite ${data.name}`);
}
}
@ -260,14 +327,36 @@ export async function unfavoriteItem(
onComplete: (item: IGalleryItem) => void
): Promise<void> {
if (container) {
const startKey = traceStart(Action.NotebooksGalleryUnfavorite, {
notebookId: data.id,
favoriteCount: data.favorites,
});
try {
const response = await junoClient.unfavoriteNotebook(data.id);
if (!response.data) {
throw new Error(`Received HTTP ${response.status} when unfavoriting ${data.name}`);
}
traceSuccess(
Action.NotebooksGalleryUnfavorite,
{ notebookId: data.id, favoriteCount: response.data.favorites },
startKey
);
onComplete(response.data);
} catch (error) {
traceFailure(
Action.NotebooksGalleryUnfavorite,
{
notebookId: data.id,
favoriteCount: data.favorites,
error: getErrorMessage(error),
errorStack: getErrorStack(error),
},
startKey
);
handleError(error, "GalleryUtils/unfavoriteItem", `Failed to unfavorite ${data.name}`);
}
}
@ -280,6 +369,8 @@ export function deleteItem(
onComplete: (item: IGalleryItem) => void
): void {
if (container) {
trace(Action.NotebooksGalleryClickDelete, ActionModifiers.Mark, { notebookId: data.id });
container.showOkCancelModalDialog(
"Remove published notebook",
`Would you like to remove ${data.name} from the gallery?`,
@ -291,15 +382,25 @@ export function deleteItem(
`Removing ${name} from gallery`
);
const startKey = traceStart(Action.NotebooksGalleryDelete, { notebookId: data.id });
try {
const response = await junoClient.deleteNotebook(data.id);
if (!response.data) {
throw new Error(`Received HTTP ${response.status} while removing ${name}`);
}
traceSuccess(Action.NotebooksGalleryDelete, { notebookId: data.id }, startKey);
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Successfully removed ${name} from gallery`);
onComplete(response.data);
} catch (error) {
traceFailure(
Action.NotebooksGalleryDelete,
{ notebookId: data.id, error: getErrorMessage(error), errorStack: getErrorStack(error) },
startKey
);
handleError(error, "GalleryUtils/deleteItem", `Failed to remove ${name} from gallery`);
}