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:
parent
e0063c76d9
commit
5038a01079
|
@ -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) => (
|
||||
<span key={tag}>
|
||||
<Link onClick={(event) => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
|
||||
{index === array.length - 1 ? <></> : ", "}
|
||||
</span>
|
||||
))}
|
||||
{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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 ...");
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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`);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue