diff --git a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx b/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx index e0af34ae8..4fa84141e 100644 --- a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx +++ b/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx @@ -1,5 +1,4 @@ import ko from "knockout"; -import { ITextFieldProps, Stack, Text, TextField } from "office-ui-fabric-react"; import * as React from "react"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import * as Logger from "../../Common/Logger"; @@ -7,8 +6,8 @@ import * as ViewModels from "../../Contracts/ViewModels"; import { JunoClient } from "../../Juno/JunoClient"; import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; -import { FileSystemUtil } from "../Notebook/FileSystemUtil"; import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent"; +import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent"; export class PublishNotebookPaneAdapter implements ReactAdapter { parameters: ko.Observable; @@ -22,7 +21,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter { private content: string; private description: string; private tags: string; - private thumbnailUrl: string; + private imageSrc: string; constructor(private container: ViewModels.Explorer, private junoClient: JunoClient) { this.parameters = ko.observable(Date.now()); @@ -87,7 +86,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter { this.description, this.tags?.split(","), this.author, - this.thumbnailUrl, + this.imageSrc, this.content ); if (!response.data) { @@ -112,44 +111,37 @@ export class PublishNotebookPaneAdapter implements ReactAdapter { this.close(); } + private createFormErrorForLargeImageSelection = (formError: string, formErrorDetail: string, area: string): void => { + this.formError = formError; + this.formErrorDetail = formErrorDetail; + + const message = `${this.formError}: ${this.formErrorDetail}`; + Logger.logError(message, area); + NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message); + this.triggerRender(); + }; + + private clearFormError = (): void => { + this.formError = undefined; + this.formErrorDetail = undefined; + this.triggerRender(); + }; + private createContent = (): JSX.Element => { - const descriptionPara1 = - "This notebook has your data. Please make sure you delete any sensitive data/output before publishing."; - const descriptionPara2 = `Would you like to publish and share ${FileSystemUtil.stripExtension( - this.name, - "ipynb" - )} to the gallery?`; - const descriptionProps: ITextFieldProps = { - label: "Description", - ariaLabel: "Description", - multiline: true, - rows: 3, - required: true, - onChange: (event, newValue) => (this.description = newValue) - }; - const tagsProps: ITextFieldProps = { - label: "Tags", - ariaLabel: "Tags", - placeholder: "Optional tag 1, Optional tag 2", - onChange: (event, newValue) => (this.tags = newValue) - }; - const thumbnailProps: ITextFieldProps = { - label: "Cover image url", - ariaLabel: "Cover image url", - onChange: (event, newValue) => (this.thumbnailUrl = newValue) + const publishNotebookPaneProps: PublishNotebookPaneProps = { + notebookName: this.name, + notebookDescription: "", + notebookTags: "", + notebookAuthor: this.author, + notebookCreatedDate: new Date().toISOString(), + onChangeDescription: (newValue: string) => (this.description = newValue), + onChangeTags: (newValue: string) => (this.tags = newValue), + onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue), + onError: this.createFormErrorForLargeImageSelection, + clearFormError: this.clearFormError }; - return ( -
- - {descriptionPara1} - {descriptionPara2} - - - - -
- ); + return ; }; private reset = (): void => { diff --git a/src/Explorer/Panes/PublishNotebookPaneComponent.less b/src/Explorer/Panes/PublishNotebookPaneComponent.less new file mode 100644 index 000000000..e1170b670 --- /dev/null +++ b/src/Explorer/Panes/PublishNotebookPaneComponent.less @@ -0,0 +1,6 @@ +.publishNotebookPanelContent { + display: flex; + flex-direction: column; + flex: 1; + overflow-y: auto; +} \ No newline at end of file diff --git a/src/Explorer/Panes/PublishNotebookPaneComponent.test.tsx b/src/Explorer/Panes/PublishNotebookPaneComponent.test.tsx new file mode 100644 index 000000000..6be43800c --- /dev/null +++ b/src/Explorer/Panes/PublishNotebookPaneComponent.test.tsx @@ -0,0 +1,23 @@ +import { shallow } from "enzyme"; +import React from "react"; +import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent"; + +describe("PublishNotebookPaneComponent", () => { + it("renders", () => { + const props: PublishNotebookPaneProps = { + notebookName: "SampleNotebook.ipynb", + notebookDescription: "sample description", + notebookTags: "tag1, tag2", + notebookAuthor: "CosmosDB", + notebookCreatedDate: "2020-07-17T00:00:00Z", + onChangeDescription: undefined, + onChangeTags: undefined, + onChangeImageSrc: undefined, + onError: undefined, + clearFormError: undefined + }; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/Explorer/Panes/PublishNotebookPaneComponent.tsx b/src/Explorer/Panes/PublishNotebookPaneComponent.tsx new file mode 100644 index 000000000..b87c822d0 --- /dev/null +++ b/src/Explorer/Panes/PublishNotebookPaneComponent.tsx @@ -0,0 +1,205 @@ +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 { FileSystemUtil } from "../Notebook/FileSystemUtil"; +import "./PublishNotebookPaneComponent.less"; + +export interface PublishNotebookPaneProps { + notebookName: string; + notebookDescription: string; + notebookTags: string; + notebookAuthor: string; + notebookCreatedDate: string; + 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; + notebookDescription: string; + notebookTags: string; + imageSrc: string; +} + +export class PublishNotebookPaneComponent extends React.Component { + private static readonly maxImageSizeInMib = 1.5; + private static readonly ImageTypes = ["URL", "Custom Image"]; + private descriptionPara1: string; + private descriptionPara2: string; + private descriptionProps: ITextFieldProps; + private tagsProps: ITextFieldProps; + private thumbnailUrlProps: ITextFieldProps; + private thumbnailSelectorProps: IDropdownProps; + private imageToBase64: (file: File, updateImageSrc: (result: string) => void) => void; + + constructor(props: PublishNotebookPaneProps) { + super(props); + + this.state = { + type: PublishNotebookPaneComponent.ImageTypes[0], + notebookDescription: "", + notebookTags: "", + imageSrc: undefined + }; + + this.imageToBase64 = (file: File, updateImageSrc: (result: string) => void) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onload = function() { + updateImageSrc(reader.result.toString()); + }; + + const onError = this.props.onError; + reader.onerror = function(error) { + const formError = `Failed to convert ${file.name} to base64 format`; + const formErrorDetail = `${error}`; + const area = "PublishNotebookPaneComponent/selectImageFile"; + onError(formError, formErrorDetail, area); + }; + }; + + this.descriptionPara1 = + "This notebook has your data. Please make sure you delete any sensitive data/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", + onChange: (event, newValue) => { + this.props.onChangeImageSrc(newValue); + this.setState({ imageSrc: newValue }); + } + }; + + this.thumbnailSelectorProps = { + label: "Cover image", + defaultSelectedKey: PublishNotebookPaneComponent.ImageTypes[0], + ariaLabel: "Cover image", + options: PublishNotebookPaneComponent.ImageTypes.map((value: string) => ({ text: value, key: value })), + onChange: (event, options) => { + this.setState({ type: options.text }); + } + }; + + 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 }); + } + }; + } + + public render(): JSX.Element { + return ( +
+ + + {this.descriptionPara1} + + + + {this.descriptionPara2} + + + + + + + + + + + + + + + {this.state.type === PublishNotebookPaneComponent.ImageTypes[0] ? ( + + + + ) : ( + + { + 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 }); + }); + }} + /> + + )} + + Preview + + + + + +
+ ); + } +} diff --git a/src/Explorer/Panes/__snapshots__/PublishNotebookPaneComponent.test.tsx.snap b/src/Explorer/Panes/__snapshots__/PublishNotebookPaneComponent.test.tsx.snap new file mode 100644 index 000000000..bba1e3351 --- /dev/null +++ b/src/Explorer/Panes/__snapshots__/PublishNotebookPaneComponent.test.tsx.snap @@ -0,0 +1,102 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PublishNotebookPaneComponent renders 1`] = ` +
+ + + + This notebook has your data. Please make sure you delete any sensitive data/output before publishing. + + + + + Would you like to publish and share "SampleNotebook" to the gallery? + + + + + + + + + + + + + + + + + Preview + + + + + + +
+`;