From 8f3cb7282b6749903280d1b7070d3cc3e6ac1912 Mon Sep 17 00:00:00 2001 From: Hardikkumar Nai <80053762+hardiknai-techm@users.noreply.github.com> Date: Wed, 28 Apr 2021 06:10:03 +0530 Subject: [PATCH] Migrate Publish Notebook Pane to React (#641) Co-authored-by: Steve Faulkner --- .../Cards/GalleryCardComponent.tsx | 314 ++++++++--------- .../CodeOfConductComponent.tsx | 123 ------- .../__snapshots__/index.test.tsx.snap} | 0 .../index.test.tsx} | 8 +- .../CodeOfConductComponent/index.tsx | 110 ++++++ .../GalleryViewerComponent.tsx | 3 +- src/Explorer/Explorer.tsx | 3 - src/Explorer/Notebook/NotebookManager.tsx | 22 +- .../PublishNotebookPane.test.tsx} | 9 +- .../PublishNotebookPane.tsx | 205 +++++++++++ .../PublishNotebookPaneComponent.tsx | 297 ++++++++++++++++ .../PublishNotebookPane.test.tsx.snap} | 8 +- .../styled.less} | 0 .../Panes/PublishNotebookPaneAdapter.tsx | 244 ------------- .../Panes/PublishNotebookPaneComponent.tsx | 326 ------------------ src/Main.tsx | 3 - src/Utils/UserUtils.ts | 12 +- 17 files changed, 802 insertions(+), 885 deletions(-) delete mode 100644 src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx rename src/Explorer/Controls/NotebookGallery/{__snapshots__/CodeOfConductComponent.test.tsx.snap => CodeOfConductComponent/__snapshots__/index.test.tsx.snap} (100%) rename src/Explorer/Controls/NotebookGallery/{CodeOfConductComponent.test.tsx => CodeOfConductComponent/index.test.tsx} (84%) create mode 100644 src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.tsx rename src/Explorer/Panes/{PublishNotebookPaneComponent.test.tsx => PublishNotebookPane/PublishNotebookPane.test.tsx} (78%) create mode 100644 src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.tsx create mode 100644 src/Explorer/Panes/PublishNotebookPane/PublishNotebookPaneComponent.tsx rename src/Explorer/Panes/{__snapshots__/PublishNotebookPaneComponent.test.tsx.snap => PublishNotebookPane/__snapshots__/PublishNotebookPane.test.tsx.snap} (92%) rename src/Explorer/Panes/{PublishNotebookPaneComponent.less => PublishNotebookPane/styled.less} (100%) delete mode 100644 src/Explorer/Panes/PublishNotebookPaneAdapter.tsx delete mode 100644 src/Explorer/Panes/PublishNotebookPaneComponent.tsx diff --git a/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx b/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx index 2f81cbb2b..7930f2ed6 100644 --- a/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx +++ b/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx @@ -1,25 +1,25 @@ import { Card } from "@uifabric/react-cards"; import { + BaseButton, + Button, FontWeights, Icon, IconButton, Image, ImageFit, - Persona, - Text, Link, - BaseButton, - Button, LinkBase, + Persona, Separator, - TooltipHost, Spinner, SpinnerSize, + Text, + TooltipHost, } from "office-ui-fabric-react"; -import * as React from "react"; +import React, { FunctionComponent, useState } from "react"; +import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg"; import { IGalleryItem } from "../../../../Juno/JunoClient"; import * as FileSystemUtil from "../../../Notebook/FileSystemUtil"; -import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg"; export interface GalleryCardComponentProps { data: IGalleryItem; @@ -34,166 +34,48 @@ export interface GalleryCardComponentProps { onDeleteClick: (beforeDelete: () => void, afterDelete: () => void) => void; } -interface GalleryCardComponentState { - isDeletingPublishedNotebook: boolean; -} +export const GalleryCardComponent: FunctionComponent = ({ + data, + isFavorite, + showDownload, + showDelete, + onClick, + onTagClick, + onFavoriteClick, + onUnfavoriteClick, + onDownloadClick, + onDeleteClick, +}: GalleryCardComponentProps) => { + const CARD_WIDTH = 256; + const cardImageHeight = 144; + const cardDescriptionMaxChars = 80; + const cardItemGapBig = 10; + const cardItemGapSmall = 8; + const cardDeleteSpinnerHeight = 360; + const smallTextLineHeight = 18; -export class GalleryCardComponent extends React.Component { - public static readonly CARD_WIDTH = 256; - private static readonly cardImageHeight = 144; - public static readonly cardHeightToWidthRatio = - GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH; - private static readonly cardDescriptionMaxChars = 80; - private static readonly cardItemGapBig = 10; - private static readonly cardItemGapSmall = 8; - private static readonly cardDeleteSpinnerHeight = 360; - private static readonly smallTextLineHeight = 18; + const [isDeletingPublishedNotebook, setIsDeletingPublishedNotebook] = useState(false); - constructor(props: GalleryCardComponentProps) { - super(props); - this.state = { - isDeletingPublishedNotebook: false, - }; - } + const cardButtonsVisible = isFavorite !== undefined || showDownload || showDelete; + const options: Intl.DateTimeFormatOptions = { + year: "numeric", + month: "short", + day: "numeric", + }; + const dateString = new Date(data.created).toLocaleString("default", options); + const cardTitle = FileSystemUtil.stripExtension(data.name, "ipynb"); - public render(): JSX.Element { - const cardButtonsVisible = this.props.isFavorite !== undefined || this.props.showDownload || this.props.showDelete; - const options: Intl.DateTimeFormatOptions = { - year: "numeric", - month: "short", - day: "numeric", - }; - const dateString = new Date(this.props.data.created).toLocaleString("default", options); - const cardTitle = FileSystemUtil.stripExtension(this.props.data.name, "ipynb"); - - return ( - this.onClick(event, this.props.onClick)} - > - {this.state.isDeletingPublishedNotebook && ( - - - - )} - {!this.state.isDeletingPublishedNotebook && ( - <> - - - - - - {`${cardTitle} - - - - - {this.props.data.tags ? ( - this.props.data.tags.map((tag, index, array) => ( - - this.onClick(event, () => this.props.onTagClick(tag))}>{tag} - {index === array.length - 1 ? <> : ", "} - - )) - ) : ( -
- )} -
- - - {cardTitle} - - - - {this.renderTruncatedDescription()} - - - - {this.props.data.views !== undefined && - this.generateIconText("RedEye", this.props.data.views.toString())} - {this.props.data.downloads !== undefined && - this.generateIconText("Download", this.props.data.downloads.toString())} - {this.props.data.favorites !== undefined && - this.generateIconText("Heart", this.props.data.favorites.toString())} - -
- - {cardButtonsVisible && ( - - - - - {this.props.isFavorite !== undefined && - this.generateIconButtonWithTooltip( - this.props.isFavorite ? "HeartFill" : "Heart", - this.props.isFavorite ? "Unfavorite" : "Favorite", - "left", - this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick - )} - - {this.props.showDownload && - this.generateIconButtonWithTooltip("Download", "Download", "left", this.props.onDownloadClick)} - - {this.props.showDelete && - this.generateIconButtonWithTooltip("Delete", "Remove", "right", () => - this.props.onDeleteClick( - () => this.setState({ isDeletingPublishedNotebook: true }), - () => this.setState({ isDeletingPublishedNotebook: false }) - ) - )} - - - )} - - )} -
- ); - } - - private renderTruncatedDescription = (): string => { - let truncatedDescription = this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars); - if (this.props.data.description.length > GalleryCardComponent.cardDescriptionMaxChars) { + const renderTruncatedDescription = (): string => { + let truncatedDescription = data.description.substr(0, cardDescriptionMaxChars); + if (data.description.length > cardDescriptionMaxChars) { truncatedDescription = `${truncatedDescription} ...`; } return truncatedDescription; }; - private generateIconText = (iconName: string, text: string): JSX.Element => { + const generateIconText = (iconName: string, text: string): JSX.Element => { return ( - + {text} ); @@ -203,7 +85,7 @@ export class GalleryCardComponent extends React.Component this.onClick(event, activate)} + onClick={(event) => handlerOnClick(event, activate)} /> ); }; - private onClick = ( + const handlerOnClick = ( event: | React.MouseEvent | React.MouseEvent< @@ -239,4 +121,112 @@ export class GalleryCardComponent extends React.Component handlerOnClick(event, onClick)} + > + {isDeletingPublishedNotebook && ( + + + + )} + {!isDeletingPublishedNotebook && ( + <> + + + + + + {`${cardTitle} + + + + + {data.tags ? ( + data.tags.map((tag, index, array) => ( + + handlerOnClick(event, () => onTagClick(tag))}>{tag} + {index === array.length - 1 ? <> : ", "} + + )) + ) : ( +
+ )} +
+ + + {cardTitle} + + + + {renderTruncatedDescription()} + + + + {data.views !== undefined && generateIconText("RedEye", data.views.toString())} + {data.downloads !== undefined && generateIconText("Download", data.downloads.toString())} + {data.favorites !== undefined && generateIconText("Heart", data.favorites.toString())} + +
+ + {cardButtonsVisible && ( + + + + + {isFavorite !== undefined && + generateIconButtonWithTooltip( + isFavorite ? "HeartFill" : "Heart", + isFavorite ? "Unfavorite" : "Favorite", + "left", + isFavorite ? onUnfavoriteClick : onFavoriteClick + )} + + {showDownload && generateIconButtonWithTooltip("Download", "Download", "left", onDownloadClick)} + + {showDelete && + generateIconButtonWithTooltip("Delete", "Remove", "right", () => + onDeleteClick( + () => setIsDeletingPublishedNotebook(true), + () => setIsDeletingPublishedNotebook(false) + ) + )} + + + )} + + )} + + ); +}; diff --git a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx deleted file mode 100644 index b59daa6ed..000000000 --- a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.tsx +++ /dev/null @@ -1,123 +0,0 @@ -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 { 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; - onAcceptCodeOfConduct: (result: boolean) => void; -} - -interface CodeOfConductComponentState { - readCodeOfConduct: boolean; -} - -export class CodeOfConductComponent extends React.Component { - private viewCodeOfConductTraced: boolean; - private descriptionPara1: string; - private descriptionPara2: string; - private descriptionPara3: string; - private link1: { label: string; url: string }; - - constructor(props: CodeOfConductComponentProps) { - super(props); - - this.state = { - readCodeOfConduct: false, - }; - - 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 { - 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"); - } - } - - private onChangeCheckbox = (): void => { - this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct }); - }; - - public render(): JSX.Element { - if (!this.viewCodeOfConductTraced) { - this.viewCodeOfConductTraced = true; - trace(Action.NotebooksGalleryViewCodeOfConduct); - } - - return ( - - - {this.descriptionPara1} - - - - {this.descriptionPara2} - - - - - {this.descriptionPara3} - - {this.link1.label} - - - - - - - - - - await this.acceptCodeOfConduct()} - tabIndex={0} - className="genericPaneSubmitBtn" - text="Continue" - disabled={!this.state.readCodeOfConduct} - /> - - - ); - } -} diff --git a/src/Explorer/Controls/NotebookGallery/__snapshots__/CodeOfConductComponent.test.tsx.snap b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/__snapshots__/index.test.tsx.snap similarity index 100% rename from src/Explorer/Controls/NotebookGallery/__snapshots__/CodeOfConductComponent.test.tsx.snap rename to src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/__snapshots__/index.test.tsx.snap diff --git a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.test.tsx b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.test.tsx similarity index 84% rename from src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.test.tsx rename to src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.test.tsx index baf990a4a..fb913adaa 100644 --- a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent.test.tsx +++ b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.test.tsx @@ -1,9 +1,9 @@ -jest.mock("../../../Juno/JunoClient"); +jest.mock("../../../../Juno/JunoClient"); import { shallow } from "enzyme"; import React from "react"; -import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent"; -import { JunoClient } from "../../../Juno/JunoClient"; -import { HttpStatusCodes } from "../../../Common/Constants"; +import { CodeOfConductComponent, CodeOfConductComponentProps } from "."; +import { HttpStatusCodes } from "../../../../Common/Constants"; +import { JunoClient } from "../../../../Juno/JunoClient"; describe("CodeOfConductComponent", () => { let codeOfConductProps: CodeOfConductComponentProps; diff --git a/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.tsx b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.tsx new file mode 100644 index 000000000..71aefd3cf --- /dev/null +++ b/src/Explorer/Controls/NotebookGallery/CodeOfConductComponent/index.tsx @@ -0,0 +1,110 @@ +import { Checkbox, Link, PrimaryButton, Stack, Text } from "office-ui-fabric-react"; +import React, { FunctionComponent, useEffect, useState } from "react"; +import { CodeOfConductEndpoints, HttpStatusCodes } from "../../../../Common/Constants"; +import { getErrorMessage, getErrorStack, handleError } from "../../../../Common/ErrorHandlingUtils"; +import { JunoClient } from "../../../../Juno/JunoClient"; +import { Action } from "../../../../Shared/Telemetry/TelemetryConstants"; +import { trace, traceFailure, traceStart, traceSuccess } from "../../../../Shared/Telemetry/TelemetryProcessor"; + +export interface CodeOfConductComponentProps { + junoClient: JunoClient; + onAcceptCodeOfConduct: (result: boolean) => void; +} + +export const CodeOfConductComponent: FunctionComponent = ({ + junoClient, + onAcceptCodeOfConduct, +}: CodeOfConductComponentProps) => { + const descriptionPara1 = "Azure Cosmos DB Notebook Gallery - Code of Conduct"; + const descriptionPara2 = "The notebook public gallery contains notebook samples shared by users of Azure Cosmos DB."; + const descriptionPara3 = "In order to view and publish your samples to the gallery, you must accept the "; + const link1: { label: string; url: string } = { + label: "code of conduct.", + url: CodeOfConductEndpoints.codeOfConduct, + }; + + const [readCodeOfConduct, setReadCodeOfConduct] = useState(false); + + const acceptCodeOfConduct = async (): Promise => { + const startKey = traceStart(Action.NotebooksGalleryAcceptCodeOfConduct); + + try { + const response = await 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); + + 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"); + } + }; + + const onChangeCheckbox = (): void => { + setReadCodeOfConduct(!readCodeOfConduct); + }; + + useEffect(() => { + trace(Action.NotebooksGalleryViewCodeOfConduct); + }, []); + + return ( + + + {descriptionPara1} + + + + {descriptionPara2} + + + + + {descriptionPara3} + + {link1.label} + + + + + + + + + + await acceptCodeOfConduct()} + tabIndex={0} + className="genericPaneSubmitBtn" + text="Continue" + disabled={!readCodeOfConduct} + /> + + + ); +}; diff --git a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx index 2831ec866..db332a55b 100644 --- a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx +++ b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx @@ -34,6 +34,7 @@ import { CodeOfConductComponent } from "./CodeOfConductComponent"; import "./GalleryViewerComponent.less"; import { InfoComponent } from "./InfoComponent/InfoComponent"; +const CARD_WIDTH = 256; export interface GalleryViewerComponentProps { container?: Explorer; junoClient: JunoClient; @@ -643,7 +644,7 @@ export class GalleryViewerComponent extends React.Component { if (itemIndex === 0) { - this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH) || this.columnCount; + this.columnCount = Math.floor(visibleRect.width / CARD_WIDTH) || this.columnCount; this.rowCount = GalleryViewerComponent.rowsPerPage; } diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index 5b3fcebc3..644f5980b 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -6,7 +6,6 @@ import React from "react"; import _ from "underscore"; import { AuthType } from "../AuthType"; import { BindingHandlersRegisterer } from "../Bindings/BindingHandlersRegisterer"; -import { ReactAdapter } from "../Bindings/ReactBindingHandler"; import * as Constants from "../Common/Constants"; import { ExplorerMetrics } from "../Common/Constants"; import { readCollection } from "../Common/dataAccess/readCollection"; @@ -174,7 +173,6 @@ export default class Explorer { public graphStylingPane: GraphStylingPane; public cassandraAddCollectionPane: CassandraAddCollectionPane; public gitHubReposPane: ContextualPaneBase; - public publishNotebookPaneAdapter: ReactAdapter; // features public isGitHubPaneEnabled: ko.Observable; @@ -1410,7 +1408,6 @@ export default class Explorer { ): Promise { if (this.notebookManager) { await this.notebookManager.openPublishNotebookPane(name, content, parentDomElement); - this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; this.isPublishNotebookPaneEnabled(true); } } diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 7d8c9a1d5..cba1b0ccd 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -2,7 +2,7 @@ * Contains all notebook related stuff meant to be dynamically loaded by explorer */ -import type { ImmutableNotebook } from "@nteract/commutable"; +import { ImmutableNotebook } from "@nteract/commutable"; import type { IContentProvider } from "@nteract/core"; import ko from "knockout"; import React from "react"; @@ -22,7 +22,7 @@ import Explorer from "../Explorer"; import { ContextualPaneBase } from "../Panes/ContextualPaneBase"; import { CopyNotebookPane } from "../Panes/CopyNotebookPane/CopyNotebookPane"; import { GitHubReposPane } from "../Panes/GitHubReposPane"; -import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter"; +import { PublishNotebookPane } from "../Panes/PublishNotebookPane/PublishNotebookPane"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { NotebookContentProvider } from "./NotebookComponent/NotebookContentProvider"; import { NotebookContainerClient } from "./NotebookContainerClient"; @@ -53,7 +53,6 @@ export default class NotebookManager { private gitHubClient: GitHubClient; public gitHubReposPane: ContextualPaneBase; - public publishNotebookPaneAdapter: PublishNotebookPaneAdapter; public initialize(params: NotebookManagerOptions): void { this.params = params; @@ -91,8 +90,6 @@ export default class NotebookManager { this.notebookContentProvider ); - this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient); - this.gitHubOAuthService.getTokenObservable().subscribe((token) => { this.gitHubClient.setToken(token?.access_token); @@ -123,7 +120,20 @@ export default class NotebookManager { content: NotebookPaneContent, parentDomElement: HTMLElement ): Promise { - await this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement); + const explorer = this.params.container; + explorer.openSidePanel( + "New Collection", + + ); } public openCopyNotebookPane(name: string, content: string): void { diff --git a/src/Explorer/Panes/PublishNotebookPaneComponent.test.tsx b/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.test.tsx similarity index 78% rename from src/Explorer/Panes/PublishNotebookPaneComponent.test.tsx rename to src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.test.tsx index 7870793bc..c80c2f2c3 100644 --- a/src/Explorer/Panes/PublishNotebookPaneComponent.test.tsx +++ b/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.test.tsx @@ -8,14 +8,15 @@ describe("PublishNotebookPaneComponent", () => { notebookName: "SampleNotebook.ipynb", notebookDescription: "sample description", notebookTags: "tag1, tag2", + imageSrc: "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg", notebookAuthor: "CosmosDB", notebookCreatedDate: "2020-07-17T00:00:00Z", notebookObject: undefined, notebookParentDomElement: undefined, - onChangeName: undefined, - onChangeDescription: undefined, - onChangeTags: undefined, - onChangeImageSrc: undefined, + setNotebookName: undefined, + setNotebookDescription: undefined, + setNotebookTags: undefined, + setImageSrc: undefined, onError: undefined, clearFormError: undefined, }; diff --git a/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.tsx b/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.tsx new file mode 100644 index 000000000..7bbd3eaec --- /dev/null +++ b/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPane.tsx @@ -0,0 +1,205 @@ +import { ImmutableNotebook, toJS } from "@nteract/commutable"; +import React, { FunctionComponent, useEffect, useState } from "react"; +import { HttpStatusCodes } from "../../../Common/Constants"; +import { getErrorMessage, getErrorStack, handleError } from "../../../Common/ErrorHandlingUtils"; +import { JunoClient } from "../../../Juno/JunoClient"; +import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; +import { traceFailure, traceStart, traceSuccess } from "../../../Shared/Telemetry/TelemetryProcessor"; +import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils"; +import { CodeOfConductComponent } from "../../Controls/NotebookGallery/CodeOfConductComponent"; +import { GalleryTab } from "../../Controls/NotebookGallery/GalleryViewerComponent"; +import Explorer from "../../Explorer"; +import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; +import { + GenericRightPaneComponent, + GenericRightPaneProps, +} from "../GenericRightPaneComponent/GenericRightPaneComponent"; +import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent"; + +export interface PublishNotebookPaneAProps { + explorer: Explorer; + closePanel: () => void; + openNotificationConsole: () => void; + junoClient: JunoClient; + name: string; + author: string; + notebookContent: string | ImmutableNotebook; + parentDomElement: HTMLElement; +} +export const PublishNotebookPane: FunctionComponent = ({ + explorer: container, + junoClient, + closePanel, + name, + author, + notebookContent, + parentDomElement, +}: PublishNotebookPaneAProps): JSX.Element => { + const [isCodeOfConductAccepted, setIsCodeOfConductAccepted] = useState(false); + const [content, setContent] = useState(""); + const [formError, setFormError] = useState(""); + const [formErrorDetail, setFormErrorDetail] = useState(""); + const [isExecuting, setIsExecuting] = useState(); + + const [notebookName, setNotebookName] = useState(name); + const [notebookDescription, setNotebookDescription] = useState(""); + const [notebookTags, setNotebookTags] = useState(""); + const [imageSrc, setImageSrc] = useState(); + + const CodeOfConductAccepted = async () => { + try { + const response = await junoClient.isCodeOfConductAccepted(); + if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { + throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); + } + setIsCodeOfConductAccepted(response.data); + } catch (error) { + handleError( + error, + "PublishNotebookPaneAdapter/isCodeOfConductAccepted", + "Failed to check if code of conduct was accepted" + ); + } + }; + const [notebookObject, setNotebookObject] = useState(); + useEffect(() => { + CodeOfConductAccepted(); + let newContent; + if (typeof notebookContent === "string") { + newContent = notebookContent as string; + } else { + newContent = JSON.stringify(toJS(notebookContent)); + setNotebookObject(notebookContent); + } + setContent(newContent); + }, []); + + const submit = async (): Promise => { + const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${name} to gallery`); + setIsExecuting(true); + + let startKey: number; + + if (!notebookName || !notebookDescription || !author || !imageSrc) { + setFormError(`Failed to publish ${notebookName} to gallery`); + setFormErrorDetail("Name, description, author and cover image are required"); + createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit"); + setIsExecuting(false); + return; + } + + try { + startKey = traceStart(Action.NotebooksGalleryPublish, {}); + + const response = await junoClient.publishNotebook( + notebookName, + notebookDescription, + notebookTags?.split(","), + author, + imageSrc, + content + ); + + const data = response.data; + if (data) { + let isPublishPending = false; + + if (data.pendingScanJobIds?.length > 0) { + isPublishPending = true; + NotificationConsoleUtils.logConsoleInfo( + `Content of ${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).` + ); + } else { + NotificationConsoleUtils.logConsoleInfo(`Published ${notebookName} to gallery`); + container.openGallery(GalleryTab.Published); + } + + traceSuccess( + Action.NotebooksGalleryPublish, + { + notebookId: data.id, + isPublishPending, + }, + startKey + ); + } + } catch (error) { + traceFailure( + Action.NotebooksGalleryPublish, + { + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey + ); + + const errorMessage = getErrorMessage(error); + setFormError(`Failed to publish ${FileSystemUtil.stripExtension(notebookName, "ipynb")} to gallery`); + setFormErrorDetail(`${errorMessage}`); + handleError(errorMessage, "PublishNotebookPaneAdapter/submit", formError); + return; + } finally { + clearPublishingMessage(); + setIsExecuting(false); + } + + closePanel(); + }; + + const createFormError = (formError: string, formErrorDetail: string, area: string): void => { + setFormError(formError); + setFormErrorDetail(formErrorDetail); + handleError(formErrorDetail, area, formError); + }; + + const clearFormError = (): void => { + setFormError(""); + setFormErrorDetail(""); + }; + + const props: GenericRightPaneProps = { + container: container, + formError: formError, + formErrorDetail: formErrorDetail, + id: "publishnotebookpane", + isExecuting: isExecuting, + title: "Publish to gallery", + submitButtonText: "Publish", + onSubmit: () => submit(), + onClose: closePanel, + isSubmitButtonHidden: !isCodeOfConductAccepted, + }; + + const publishNotebookPaneProps: PublishNotebookPaneProps = { + notebookDescription, + notebookTags, + imageSrc, + notebookName, + notebookAuthor: author, + notebookCreatedDate: new Date().toISOString(), + notebookObject: notebookObject, + notebookParentDomElement: parentDomElement, + onError: createFormError, + clearFormError: clearFormError, + setNotebookName, + setNotebookDescription, + setNotebookTags, + setImageSrc, + }; + return ( + + {!isCodeOfConductAccepted ? ( +
+ { + setIsCodeOfConductAccepted(isAccepted); + }} + /> +
+ ) : ( + + )} +
+ ); +}; diff --git a/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPaneComponent.tsx b/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPaneComponent.tsx new file mode 100644 index 000000000..f55e17fe4 --- /dev/null +++ b/src/Explorer/Panes/PublishNotebookPane/PublishNotebookPaneComponent.tsx @@ -0,0 +1,297 @@ +import { ImmutableNotebook } from "@nteract/commutable"; +import Html2Canvas from "html2canvas"; +import { Dropdown, IDropdownProps, ITextFieldProps, Stack, Text, TextField } from "office-ui-fabric-react"; +import React, { FunctionComponent, useState } from "react"; +import { GalleryCardComponent } from "../../Controls/NotebookGallery/Cards/GalleryCardComponent"; +import * as FileSystemUtil from "../../Notebook/FileSystemUtil"; +import { NotebookUtil } from "../../Notebook/NotebookUtil"; +import "./styled.less"; + +export interface PublishNotebookPaneProps { + notebookName: string; + notebookAuthor: string; + notebookTags: string; + imageSrc: string; + notebookDescription: string; + notebookCreatedDate: string; + notebookObject: ImmutableNotebook; + notebookParentDomElement?: HTMLElement; + onError: (formError: string, formErrorDetail: string, area: string) => void; + clearFormError: () => void; + setNotebookName: (newValue: string) => void; + setNotebookDescription: (newValue: string) => void; + setNotebookTags: (newValue: string) => void; + setImageSrc: (newValue: string) => void; +} + +enum ImageTypes { + Url = "URL", + CustomImage = "Custom Image", + TakeScreenshot = "Take Screenshot", + UseFirstDisplayOutput = "Use First Display Output", +} + +export const PublishNotebookPaneComponent: FunctionComponent = ({ + notebookName, + notebookTags, + imageSrc, + notebookDescription, + notebookAuthor, + notebookCreatedDate, + notebookObject, + notebookParentDomElement, + onError, + clearFormError, + setNotebookName, + setNotebookDescription, + setNotebookTags, + setImageSrc, +}: PublishNotebookPaneProps) => { + const [type, setType] = useState(ImageTypes.CustomImage); + const CARD_WIDTH = 256; + const cardImageHeight = 144; + const cardHeightToWidthRatio = cardImageHeight / CARD_WIDTH; + + const maxImageSizeInMib = 1.5; + + const descriptionPara1 = + "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."; + + const descriptionPara2 = `Would you like to publish and share "${FileSystemUtil.stripExtension( + notebookName, + "ipynb" + )}" to the gallery?`; + + const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url]; + const thumbnailSelectorProps: IDropdownProps = { + label: "Cover image", + defaultSelectedKey: ImageTypes.CustomImage, + ariaLabel: "Cover image", + options: options.map((value: string) => ({ text: value, key: value })), + onChange: async (event, options) => { + setImageSrc(""); + clearFormError(); + if (options.text === ImageTypes.TakeScreenshot) { + try { + await takeScreenshot(notebookParentDomElement, screenshotErrorHandler); + } catch (error) { + screenshotErrorHandler(error); + } + } else if (options.text === ImageTypes.UseFirstDisplayOutput) { + try { + await takeScreenshot(findFirstOutput(), firstOutputErrorHandler); + } catch (error) { + firstOutputErrorHandler(error); + } + } + setType(options.text); + }, + }; + + const thumbnailUrlProps: ITextFieldProps = { + label: "Cover image url", + ariaLabel: "Cover image url", + required: true, + onChange: (event, newValue) => { + setImageSrc(newValue); + }, + }; + + const screenshotErrorHandler = (error: Error) => { + const formError = "Failed to take screen shot"; + const formErrorDetail = `${error}`; + const area = "PublishNotebookPaneComponent/takeScreenshot"; + onError(formError, formErrorDetail, area); + }; + + const firstOutputErrorHandler = (error: Error) => { + const formError = "Failed to capture first output"; + const formErrorDetail = `${error}`; + const area = "PublishNotebookPaneComponent/UseFirstOutput"; + onError(formError, formErrorDetail, area); + }; + + if (notebookParentDomElement) { + options.push(ImageTypes.TakeScreenshot); + if (notebookObject) { + options.push(ImageTypes.UseFirstDisplayOutput); + } + } + + const imageToBase64 = (file: File, updateImageSrc: (result: string) => void) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = () => { + updateImageSrc(reader.result.toString()); + }; + + reader.onerror = (error) => { + const formError = `Failed to convert ${file.name} to base64 format`; + const formErrorDetail = `${error}`; + const area = "PublishNotebookPaneComponent/selectImageFile"; + onError(formError, formErrorDetail, area); + }; + }; + + const takeScreenshot = (target: HTMLElement, onError: (error: Error) => void): void => { + const updateImageSrcWithScreenshot = (canvasUrl: string): void => { + setImageSrc(canvasUrl); + }; + + target.scrollIntoView(); + Html2Canvas(target, { + useCORS: true, + allowTaint: true, + scale: 1, + logging: true, + }) + .then((canvas) => { + //redraw canvas to fit Card Cover Image dimensions + const originalImageData = canvas.toDataURL(); + const requiredHeight = parseInt(canvas.style.width.split("px")[0]) * cardHeightToWidthRatio; + canvas.height = requiredHeight; + const context = canvas.getContext("2d"); + const image = new Image(); + image.src = originalImageData; + image.onload = () => { + context.drawImage(image, 0, 0); + updateImageSrcWithScreenshot(canvas.toDataURL()); + }; + }) + .catch((error) => { + onError(error); + }); + }; + + const renderThumbnailSelectors = (type: string) => { + switch (type) { + case ImageTypes.Url: + return ; + case ImageTypes.CustomImage: + return ( + { + const file = event.target.files[0]; + if (file.size / 1024 ** 2 > maxImageSizeInMib) { + event.target.value = ""; + const formError = `Failed to upload ${file.name}`; + const formErrorDetail = `Image is larger than ${maxImageSizeInMib} MiB. Please Choose a different image.`; + const area = "PublishNotebookPaneComponent/selectImageFile"; + + onError(formError, formErrorDetail, area); + setImageSrc(""); + return; + } else { + clearFormError(); + } + imageToBase64(file, (result: string) => { + setImageSrc(result); + }); + }} + /> + ); + default: + return <>; + } + }; + + const findFirstOutput = (): HTMLElement => { + const indexOfFirstCodeCellWithDisplay = NotebookUtil.findFirstCodeCellWithDisplay(notebookObject); + const cellOutputDomElements = notebookParentDomElement.querySelectorAll(".nteract-cell-outputs"); + return cellOutputDomElements[indexOfFirstCodeCellWithDisplay]; + }; + + return ( +
+ + + {descriptionPara1} + + + + {descriptionPara2} + + + + { + const notebookName = newValue + ".ipynb"; + setNotebookName(notebookName); + }} + /> + + + + { + setNotebookDescription(newValue); + }} + /> + + + + { + setNotebookTags(newValue); + }} + /> + + + + + + + {renderThumbnailSelectors(type)} + + + Preview + + + undefined} + onTagClick={undefined} + onFavoriteClick={undefined} + onUnfavoriteClick={undefined} + onDownloadClick={undefined} + onDeleteClick={undefined} + /> + + +
+ ); +}; diff --git a/src/Explorer/Panes/__snapshots__/PublishNotebookPaneComponent.test.tsx.snap b/src/Explorer/Panes/PublishNotebookPane/__snapshots__/PublishNotebookPane.test.tsx.snap similarity index 92% rename from src/Explorer/Panes/__snapshots__/PublishNotebookPaneComponent.test.tsx.snap rename to src/Explorer/Panes/PublishNotebookPane/__snapshots__/PublishNotebookPane.test.tsx.snap index 4986c1ef9..646cc18c6 100644 --- a/src/Explorer/Panes/__snapshots__/PublishNotebookPaneComponent.test.tsx.snap +++ b/src/Explorer/Panes/PublishNotebookPane/__snapshots__/PublishNotebookPane.test.tsx.snap @@ -88,7 +88,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = ` Object { "author": "CosmosDB", "created": "2020-07-17T00:00:00Z", - "description": "", + "description": "sample description", "downloads": undefined, "favorites": undefined, "gitSha": undefined, @@ -99,12 +99,14 @@ exports[`PublishNotebookPaneComponent renders 1`] = ` "pendingScanJobIds": undefined, "policyViolations": undefined, "tags": Array [ - "", + "tag1", + " tag2", ], - "thumbnailUrl": undefined, + "thumbnailUrl": "https://i.ytimg.com/vi/E_lByLdKeKY/maxresdefault.jpg", "views": undefined, } } + onClick={[Function]} showDelete={false} showDownload={false} /> diff --git a/src/Explorer/Panes/PublishNotebookPaneComponent.less b/src/Explorer/Panes/PublishNotebookPane/styled.less similarity index 100% rename from src/Explorer/Panes/PublishNotebookPaneComponent.less rename to src/Explorer/Panes/PublishNotebookPane/styled.less diff --git a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx b/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx deleted file mode 100644 index ee7927d69..000000000 --- a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import { toJS } from "@nteract/commutable"; -import { ImmutableNotebook } from "@nteract/commutable/src"; -import ko from "knockout"; -import * as React from "react"; -import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import { HttpStatusCodes } from "../../Common/Constants"; -import { getErrorMessage, getErrorStack, handleError } from "../../Common/ErrorHandlingUtils"; -import { JunoClient } from "../../Juno/JunoClient"; -import { Action } from "../../Shared/Telemetry/TelemetryConstants"; -import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor"; -import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; -import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent"; -import { GalleryTab } from "../Controls/NotebookGallery/GalleryViewerComponent"; -import Explorer from "../Explorer"; -import * as FileSystemUtil from "../Notebook/FileSystemUtil"; -import { - GenericRightPaneComponent, - GenericRightPaneProps, -} from "./GenericRightPaneComponent/GenericRightPaneComponent"; -import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent"; - -export class PublishNotebookPaneAdapter implements ReactAdapter { - parameters: ko.Observable; - private isOpened: boolean; - private isExecuting: boolean; - private formError: string; - private formErrorDetail: string; - - private name: string; - private author: string; - private content: string; - private description: string; - private tags: string; - private imageSrc: string; - private notebookObject: ImmutableNotebook; - private parentDomElement: HTMLElement; - private isCodeOfConductAccepted: boolean; - - constructor(private container: Explorer, private junoClient: JunoClient) { - this.parameters = ko.observable(Date.now()); - this.reset(); - this.triggerRender(); - } - - public renderComponent(): JSX.Element { - if (!this.isOpened) { - return undefined; - } - - const props: GenericRightPaneProps = { - container: this.container, - formError: this.formError, - formErrorDetail: this.formErrorDetail, - id: "publishnotebookpane", - isExecuting: this.isExecuting, - title: "Publish to gallery", - submitButtonText: "Publish", - onClose: () => this.close(), - onSubmit: () => this.submit(), - isSubmitButtonHidden: !this.isCodeOfConductAccepted, - }; - - const publishNotebookPaneProps: PublishNotebookPaneProps = { - notebookName: this.name, - notebookDescription: "", - notebookTags: "", - notebookAuthor: this.author, - notebookCreatedDate: new Date().toISOString(), - notebookObject: this.notebookObject, - notebookParentDomElement: this.parentDomElement, - onChangeName: (newValue: string) => (this.name = newValue), - onChangeDescription: (newValue: string) => (this.description = newValue), - onChangeTags: (newValue: string) => (this.tags = newValue), - onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue), - onError: this.createFormError, - clearFormError: this.clearFormError, - }; - - return ( - - {!this.isCodeOfConductAccepted ? ( -
- { - this.isCodeOfConductAccepted = true; - this.triggerRender(); - }} - /> -
- ) : ( - - )} -
- ); - } - - public triggerRender(): void { - window.requestAnimationFrame(() => this.parameters(Date.now())); - } - - public async open( - name: string, - author: string, - notebookContent: string | ImmutableNotebook, - parentDomElement: HTMLElement - ): Promise { - try { - const response = await this.junoClient.isCodeOfConductAccepted(); - if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) { - throw new Error(`Received HTTP ${response.status} when accepting code of conduct`); - } - - this.isCodeOfConductAccepted = response.data; - } catch (error) { - handleError( - error, - "PublishNotebookPaneAdapter/isCodeOfConductAccepted", - "Failed to check if code of conduct was accepted" - ); - } - - this.name = name; - this.author = author; - if (typeof notebookContent === "string") { - this.content = notebookContent as string; - } else { - this.content = JSON.stringify(toJS(notebookContent)); - this.notebookObject = notebookContent; - } - this.parentDomElement = parentDomElement; - - this.isOpened = true; - this.triggerRender(); - } - - public close(): void { - this.reset(); - this.triggerRender(); - } - - public async submit(): Promise { - const clearPublishingMessage = NotificationConsoleUtils.logConsoleProgress(`Publishing ${this.name} to gallery`); - this.isExecuting = true; - this.triggerRender(); - - let startKey: number; - - if (!this.name || !this.description || !this.author || !this.imageSrc) { - const formError = `Failed to publish ${this.name} to gallery`; - const formErrorDetail = "Name, description, author and cover image are required"; - this.createFormError(formError, formErrorDetail, "PublishNotebookPaneAdapter/submit"); - this.isExecuting = false; - return; - } - - try { - startKey = traceStart(Action.NotebooksGalleryPublish, {}); - - const response = await this.junoClient.publishNotebook( - this.name, - this.description, - this.tags?.split(","), - this.author, - this.imageSrc, - this.content - ); - - 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).` - ); - } else { - NotificationConsoleUtils.logConsoleInfo(`Published ${this.name} to gallery`); - this.container.openGallery(GalleryTab.Published); - } - - traceSuccess( - Action.NotebooksGalleryPublish, - { - notebookId: data.id, - isPublishPending, - }, - startKey - ); - } - } catch (error) { - traceFailure( - Action.NotebooksGalleryPublish, - { - error: getErrorMessage(error), - errorStack: getErrorStack(error), - }, - startKey - ); - - const errorMessage = getErrorMessage(error); - this.formError = `Failed to publish ${FileSystemUtil.stripExtension(this.name, "ipynb")} to gallery`; - this.formErrorDetail = `${errorMessage}`; - handleError(errorMessage, "PublishNotebookPaneAdapter/submit", this.formError); - return; - } finally { - clearPublishingMessage(); - this.isExecuting = false; - this.triggerRender(); - } - - this.close(); - } - - private createFormError = (formError: string, formErrorDetail: string, area: string): void => { - this.formError = formError; - this.formErrorDetail = formErrorDetail; - handleError(formErrorDetail, area, formError); - this.triggerRender(); - }; - - private clearFormError = (): void => { - this.formError = undefined; - this.formErrorDetail = undefined; - this.triggerRender(); - }; - - private reset = (): void => { - this.isOpened = false; - this.isExecuting = false; - this.formError = undefined; - this.formErrorDetail = undefined; - this.name = undefined; - this.author = undefined; - this.content = undefined; - this.description = undefined; - this.tags = undefined; - this.imageSrc = undefined; - this.notebookObject = undefined; - this.parentDomElement = undefined; - this.isCodeOfConductAccepted = undefined; - }; -} diff --git a/src/Explorer/Panes/PublishNotebookPaneComponent.tsx b/src/Explorer/Panes/PublishNotebookPaneComponent.tsx deleted file mode 100644 index 1075ae5c2..000000000 --- a/src/Explorer/Panes/PublishNotebookPaneComponent.tsx +++ /dev/null @@ -1,326 +0,0 @@ -import { ITextFieldProps, Stack, Text, TextField, Dropdown, IDropdownProps } from "office-ui-fabric-react"; -import * as React from "react"; -import { GalleryCardComponent } from "../Controls/NotebookGallery/Cards/GalleryCardComponent"; -import * as FileSystemUtil from "../Notebook/FileSystemUtil"; -import "./PublishNotebookPaneComponent.less"; -import Html2Canvas from "html2canvas"; -import { ImmutableNotebook } from "@nteract/commutable/src"; -import { NotebookUtil } from "../Notebook/NotebookUtil"; - -export interface PublishNotebookPaneProps { - notebookName: string; - notebookDescription: string; - notebookTags: string; - notebookAuthor: string; - notebookCreatedDate: string; - notebookObject: ImmutableNotebook; - notebookParentDomElement?: HTMLElement; - onChangeName: (newValue: string) => void; - onChangeDescription: (newValue: string) => void; - onChangeTags: (newValue: string) => void; - onChangeImageSrc: (newValue: string) => void; - onError: (formError: string, formErrorDetail: string, area: string) => void; - clearFormError: () => void; -} - -interface PublishNotebookPaneState { - type: string; - notebookName: string; - notebookDescription: string; - notebookTags: string; - imageSrc: string; -} - -enum ImageTypes { - Url = "URL", - CustomImage = "Custom Image", - TakeScreenshot = "Take Screenshot", - UseFirstDisplayOutput = "Use First Display Output", -} - -export class PublishNotebookPaneComponent extends React.Component { - private static readonly maxImageSizeInMib = 1.5; - private descriptionPara1: string; - private descriptionPara2: string; - private nameProps: ITextFieldProps; - private descriptionProps: ITextFieldProps; - private tagsProps: ITextFieldProps; - private thumbnailUrlProps: ITextFieldProps; - private thumbnailSelectorProps: IDropdownProps; - private imageToBase64: (file: File, updateImageSrc: (result: string) => void) => void; - private takeScreenshot: (target: HTMLElement, onError: (error: Error) => void) => void; - - constructor(props: PublishNotebookPaneProps) { - super(props); - - this.state = { - type: ImageTypes.CustomImage, - notebookName: props.notebookName, - notebookDescription: "", - notebookTags: "", - imageSrc: undefined, - }; - - this.imageToBase64 = (file: File, updateImageSrc: (result: string) => void) => { - const reader = new FileReader(); - reader.readAsDataURL(file); - reader.onload = () => { - updateImageSrc(reader.result.toString()); - }; - - const onError = this.props.onError; - reader.onerror = (error) => { - const formError = `Failed to convert ${file.name} to base64 format`; - const formErrorDetail = `${error}`; - const area = "PublishNotebookPaneComponent/selectImageFile"; - onError(formError, formErrorDetail, area); - }; - }; - - this.takeScreenshot = (target: HTMLElement, onError: (error: Error) => void): void => { - const updateImageSrcWithScreenshot = (canvasUrl: string): void => { - this.props.onChangeImageSrc(canvasUrl); - this.setState({ imageSrc: canvasUrl }); - }; - - target.scrollIntoView(); - Html2Canvas(target, { - useCORS: true, - allowTaint: true, - scale: 1, - logging: true, - }) - .then((canvas) => { - //redraw canvas to fit Card Cover Image dimensions - const originalImageData = canvas.toDataURL(); - const requiredHeight = - parseInt(canvas.style.width.split("px")[0]) * GalleryCardComponent.cardHeightToWidthRatio; - canvas.height = requiredHeight; - const context = canvas.getContext("2d"); - const image = new Image(); - image.src = originalImageData; - image.onload = () => { - context.drawImage(image, 0, 0); - updateImageSrcWithScreenshot(canvas.toDataURL()); - }; - }) - .catch((error) => { - onError(error); - }); - }; - - this.descriptionPara1 = - "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, - "ipynb" - )}" to the gallery?`; - - this.thumbnailUrlProps = { - label: "Cover image url", - ariaLabel: "Cover image url", - required: true, - onChange: (event, newValue) => { - this.props.onChangeImageSrc(newValue); - this.setState({ imageSrc: newValue }); - }, - }; - - const screenshotErrorHandler = (error: Error) => { - const formError = "Failed to take screen shot"; - const formErrorDetail = `${error}`; - const area = "PublishNotebookPaneComponent/takeScreenshot"; - this.props.onError(formError, formErrorDetail, area); - }; - - const firstOutputErrorHandler = (error: Error) => { - const formError = "Failed to capture first output"; - const formErrorDetail = `${error}`; - const area = "PublishNotebookPaneComponent/UseFirstOutput"; - this.props.onError(formError, formErrorDetail, area); - }; - - const options: ImageTypes[] = [ImageTypes.CustomImage, ImageTypes.Url]; - - if (this.props.notebookParentDomElement) { - options.push(ImageTypes.TakeScreenshot); - if (this.props.notebookObject) { - options.push(ImageTypes.UseFirstDisplayOutput); - } - } - - this.thumbnailSelectorProps = { - label: "Cover image", - defaultSelectedKey: ImageTypes.CustomImage, - ariaLabel: "Cover image", - options: options.map((value: string) => ({ text: value, key: value })), - onChange: async (event, options) => { - this.setState({ imageSrc: undefined }); - this.props.onChangeImageSrc(undefined); - this.props.clearFormError(); - if (options.text === ImageTypes.TakeScreenshot) { - try { - await this.takeScreenshot(this.props.notebookParentDomElement, screenshotErrorHandler); - } catch (error) { - screenshotErrorHandler(error); - } - } else if (options.text === ImageTypes.UseFirstDisplayOutput) { - try { - await this.takeScreenshot(this.findFirstOutput(), firstOutputErrorHandler); - } catch (error) { - firstOutputErrorHandler(error); - } - } - this.setState({ type: options.text }); - }, - }; - - this.nameProps = { - label: "Name", - ariaLabel: "Name", - defaultValue: FileSystemUtil.stripExtension(this.props.notebookName, "ipynb"), - required: true, - onChange: (event, newValue) => { - const notebookName = newValue + ".ipynb"; - this.props.onChangeName(notebookName); - this.setState({ notebookName }); - }, - }; - - this.descriptionProps = { - label: "Description", - ariaLabel: "Description", - multiline: true, - rows: 3, - required: true, - onChange: (event, newValue) => { - this.props.onChangeDescription(newValue); - this.setState({ notebookDescription: newValue }); - }, - }; - - this.tagsProps = { - label: "Tags", - ariaLabel: "Tags", - placeholder: "Optional tag 1, Optional tag 2", - onChange: (event, newValue) => { - this.props.onChangeTags(newValue); - this.setState({ notebookTags: newValue }); - }, - }; - } - - private renderThumbnailSelectors(type: string) { - switch (type) { - case ImageTypes.Url: - return ; - case ImageTypes.CustomImage: - return ( - { - const file = event.target.files[0]; - if (file.size / 1024 ** 2 > PublishNotebookPaneComponent.maxImageSizeInMib) { - event.target.value = ""; - const formError = `Failed to upload ${file.name}`; - const formErrorDetail = `Image is larger than ${PublishNotebookPaneComponent.maxImageSizeInMib} MiB. Please Choose a different image.`; - const area = "PublishNotebookPaneComponent/selectImageFile"; - - this.props.onError(formError, formErrorDetail, area); - this.props.onChangeImageSrc(undefined); - this.setState({ imageSrc: undefined }); - return; - } else { - this.props.clearFormError(); - } - this.imageToBase64(file, (result: string) => { - this.props.onChangeImageSrc(result); - this.setState({ imageSrc: result }); - }); - }} - /> - ); - default: - return <>; - } - } - - private findFirstOutput(): HTMLElement { - const indexOfFirstCodeCellWithDisplay = NotebookUtil.findFirstCodeCellWithDisplay(this.props.notebookObject); - const cellOutputDomElements = this.props.notebookParentDomElement.querySelectorAll( - ".nteract-cell-outputs" - ); - return cellOutputDomElements[indexOfFirstCodeCellWithDisplay]; - } - - public render(): JSX.Element { - return ( -
- - - {this.descriptionPara1} - - - - {this.descriptionPara2} - - - - - - - - - - - - - - - - - - - {this.renderThumbnailSelectors(this.state.type)} - - - Preview - - - - - -
- ); - } -} diff --git a/src/Main.tsx b/src/Main.tsx index 8d06015af..8970ffbc8 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -235,9 +235,6 @@ const App: React.FunctionComponent = () => {
- -
- {showDialog && }
); diff --git a/src/Utils/UserUtils.ts b/src/Utils/UserUtils.ts index 78270c37b..991211dff 100644 --- a/src/Utils/UserUtils.ts +++ b/src/Utils/UserUtils.ts @@ -1,8 +1,8 @@ -import { decryptJWTToken } from "./AuthorizationUtils"; import { userContext } from "../UserContext"; +import { decryptJWTToken } from "./AuthorizationUtils"; -export function getFullName(): string { - const authToken = userContext.authorizationToken; - const props = decryptJWTToken(authToken); - return props.name; -} +export const getFullName = (): string => { + const { authorizationToken } = userContext; + const { name } = decryptJWTToken(authorizationToken); + return name; +};