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 } }}> <Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
<Text variant="small" nowrap> <Text variant="small" nowrap>
{this.props.data.tags?.map((tag, index, array) => ( {this.props.data.tags ? (
<span key={tag}> this.props.data.tags.map((tag, index, array) => (
<Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link> <span key={tag}>
{index === array.length - 1 ? <></> : ", "} <Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
</span> {index === array.length - 1 ? <></> : ", "}
))} </span>
))
) : (
<br />
)}
</Text> </Text>
<Text <Text

View File

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

View File

@ -9,6 +9,7 @@ import {
IPivotProps, IPivotProps,
IRectangle, IRectangle,
Label, Label,
Link,
List, List,
Overlay, Overlay,
Pivot, Pivot,
@ -28,6 +29,8 @@ import Explorer from "../../Explorer";
import { CodeOfConductComponent } from "./CodeOfConductComponent"; import { CodeOfConductComponent } from "./CodeOfConductComponent";
import { InfoComponent } from "./InfoComponent/InfoComponent"; import { InfoComponent } from "./InfoComponent/InfoComponent";
import { handleError } from "../../../Common/ErrorHandlingUtils"; import { handleError } from "../../../Common/ErrorHandlingUtils";
import { trace } from "../../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
export interface GalleryViewerComponentProps { export interface GalleryViewerComponentProps {
container?: Explorer; container?: Explorer;
@ -87,6 +90,12 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private readonly sortingOptions: IDropdownOption[]; private readonly sortingOptions: IDropdownOption[];
private viewGalleryTraced: boolean;
private viewOfficialSamplesTraced: boolean;
private viewPublicGalleryTraced: boolean;
private viewFavoritesTraced: boolean;
private viewPublishedNotebooksTraced: boolean;
private sampleNotebooks: IGalleryItem[]; private sampleNotebooks: IGalleryItem[];
private publicNotebooks: IGalleryItem[]; private publicNotebooks: IGalleryItem[];
private favoriteNotebooks: IGalleryItem[]; private favoriteNotebooks: IGalleryItem[];
@ -138,6 +147,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
} }
public render(): JSX.Element { public render(): JSX.Element {
this.traceViewGallery();
const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)]; const tabs: GalleryTabInfo[] = [this.createSamplesTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
if (this.props.container?.isGalleryPublishEnabled()) { 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 => { private isEmptyData = (data: IGalleryItem[]): boolean => {
return !data || data.length === 0; 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 ( return (
<Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}> <Stack horizontalAlign="center" tokens={{ childrenGap: 10 }}>
<FontIcon iconName={iconName} style={{ fontSize: 100, color: "lightgray", marginTop: 20 }} /> <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) content: this.isEmptyData(data)
? this.createEmptyTabContent( ? this.createEmptyTabContent(
"ContactHeart", "ContactHeart",
"You have not favorited anything", <>You don&apos;t have any favorites yet</>,
"Favorite any notebook from Official samples or Public gallery" <>
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)), : this.createSearchBarHeader(this.createCardsTabContent(data)),
}; };
@ -236,8 +298,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
content: this.isEmptyData(data) content: this.isEmptyData(data)
? this.createEmptyTabContent( ? this.createEmptyTabContent(
"Contact", "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), : this.createPublishedNotebooksTabContent(data),
}; };
@ -250,7 +315,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
{published?.length > 0 && {published?.length > 0 &&
this.createPublishedNotebooksSectionContent( this.createPublishedNotebooksSectionContent(
undefined, 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) this.createCardsTabContent(published)
)} )}
{underReview?.length > 0 && {underReview?.length > 0 &&
@ -278,8 +343,10 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
): JSX.Element => { ): JSX.Element => {
return ( return (
<Stack tokens={{ childrenGap: 10 }}> <Stack tokens={{ childrenGap: 10 }}>
{title && <Text styles={{ root: { fontWeight: FontWeights.semibold } }}>{title}</Text>} {title && (
{description && <Text>{description}</Text>} <Text styles={{ root: { fontWeight: FontWeights.semibold, marginLeft: 10, marginRight: 10 } }}>{title}</Text>
)}
{description && <Text styles={{ root: { marginLeft: 10, marginRight: 10 } }}>{description}</Text>}
{content} {content}
</Stack> </Stack>
); );
@ -344,7 +411,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
private createPolicyViolationsListContent(data: IGalleryItem[]): JSX.Element { private createPolicyViolationsListContent(data: IGalleryItem[]): JSX.Element {
return ( return (
<table> <table style={{ margin: 10 }}>
<tbody> <tbody>
<tr> <tr>
<th>Name</th> <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> </Text>
</StackItem> </StackItem>
<StackItem> <StackItem>
<Text> <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> </Text>
</StackItem> </StackItem>
<StackItem> <StackItem>
<Text> <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 <StyledLinkBase
href="https://aka.ms/cosmos-code-of-conduct" href="https://aka.ms/cosmos-code-of-conduct"
target="_blank" target="_blank"
> >
code of conduct code of conduct.
</StyledLinkBase>
and
<StyledLinkBase
href="https://aka.ms/ms-privacy-policy"
target="_blank"
>
privacy statement
</StyledLinkBase> </StyledLinkBase>
</Text> </Text>
</StackItem> </StackItem>
<StackItem> <StackItem>
<StyledCheckboxBase <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]} onChange={[Function]}
styles={ styles={
Object { Object {

View File

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

View File

@ -2253,7 +2253,7 @@ export default class Explorer {
return Promise.resolve(false); 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) { if (this.notebookManager) {
await this.notebookManager.openPublishNotebookPane( await this.notebookManager.openPublishNotebookPane(
name, name,

View File

@ -10,8 +10,10 @@ import { ImmutableNotebook } from "@nteract/commutable/src";
import { toJS } from "@nteract/commutable"; import { toJS } from "@nteract/commutable";
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent"; import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
import { HttpStatusCodes } from "../../Common/Constants"; 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 { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent";
import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
export class PublishNotebookPaneAdapter implements ReactAdapter { export class PublishNotebookPaneAdapter implements ReactAdapter {
parameters: ko.Observable<number>; parameters: ko.Observable<number>;
@ -141,11 +143,18 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
this.isExecuting = true; this.isExecuting = true;
this.triggerRender(); this.triggerRender();
let startKey: number;
try { try {
if (!this.name || !this.description || !this.author) { if (!this.name || !this.description || !this.author) {
throw new Error("Name, description, and author are required"); 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( const response = await this.junoClient.publishNotebook(
this.name, this.name,
this.description, this.description,
@ -158,7 +167,10 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
const data = response.data; const data = response.data;
if (data) { if (data) {
let isPublishPending = false;
if (data.pendingScanJobIds?.length > 0) { if (data.pendingScanJobIds?.length > 0) {
isPublishPending = true;
NotificationConsoleUtils.logConsoleInfo( 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).` `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`); NotificationConsoleUtils.logConsoleInfo(`Published ${this.name} to gallery`);
this.container.openGallery(GalleryTab.Published); this.container.openGallery(GalleryTab.Published);
} }
traceSuccess(
Action.NotebooksGalleryPublish,
{
databaseAccountName: this.container.databaseAccount()?.name,
defaultExperience: this.container.defaultExperience(),
notebookId: data.id,
isPublishPending,
},
startKey
);
} }
} catch (error) { } 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); const errorMessage = getErrorMessage(error);
this.formError = `Failed to publish ${this.name} to gallery`; this.formError = `Failed to publish ${this.name} to gallery`;
this.formErrorDetail = `${errorMessage}`; this.formErrorDetail = `${errorMessage}`;

View File

@ -14,7 +14,7 @@ export interface PublishNotebookPaneProps {
notebookAuthor: string; notebookAuthor: string;
notebookCreatedDate: string; notebookCreatedDate: string;
notebookObject: ImmutableNotebook; notebookObject: ImmutableNotebook;
notebookParentDomElement: HTMLElement; notebookParentDomElement?: HTMLElement;
onChangeName: (newValue: string) => void; onChangeName: (newValue: string) => void;
onChangeDescription: (newValue: string) => void; onChangeDescription: (newValue: string) => void;
onChangeTags: (newValue: string) => void; onChangeTags: (newValue: string) => void;
@ -110,7 +110,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
}; };
this.descriptionPara1 = 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.descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension(
this.props.notebookName, this.props.notebookName,
@ -140,16 +140,20 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
this.props.onError(formError, formErrorDetail, area); 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 = { this.thumbnailSelectorProps = {
label: "Cover image", label: "Cover image",
defaultSelectedKey: ImageTypes.Url, defaultSelectedKey: ImageTypes.Url,
ariaLabel: "Cover image", ariaLabel: "Cover image",
options: [ options: options.map((value: string) => ({ text: value, key: value })),
ImageTypes.Url,
ImageTypes.CustomImage,
ImageTypes.TakeScreenshot,
ImageTypes.UseFirstDisplayOutput,
].map((value: string) => ({ text: value, key: value })),
onChange: async (event, options) => { onChange: async (event, options) => {
this.props.clearFormError(); this.props.clearFormError();
if (options.text === ImageTypes.TakeScreenshot) { if (options.text === ImageTypes.TakeScreenshot) {
@ -301,9 +305,9 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
policyViolations: undefined, policyViolations: undefined,
pendingScanJobIds: undefined, pendingScanJobIds: undefined,
}} }}
isFavorite={false} isFavorite={undefined}
showDownload={true} showDownload={false}
showDelete={true} showDelete={false}
onClick={undefined} onClick={undefined}
onTagClick={undefined} onTagClick={undefined}
onFavoriteClick={undefined} onFavoriteClick={undefined}

View File

@ -14,7 +14,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
> >
<StackItem> <StackItem>
<Text> <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> </Text>
</StackItem> </StackItem>
<StackItem> <StackItem>
@ -65,14 +65,6 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
"key": "Custom Image", "key": "Custom Image",
"text": "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, "views": 0,
} }
} }
isFavorite={false} showDelete={false}
showDelete={true} showDownload={false}
showDownload={true}
/> />
</StackItem> </StackItem>
</Stack> </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 // "Copy to ..." isn't needed if github locations are not available
if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) { if (!this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
items = items.filter((item) => item.label !== "Copy to ..."); items = items.filter((item) => item.label !== "Copy to ...");

View File

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

View File

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