mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-25 15:06:55 +00:00
Added support for custom image upload during publish to Gallery (#99)
* Added support for custom image upload - Dropdown gives an option for URL or image upload - Preview shows how the card will be displayed in the gallery - base64 converted image stored in metadata document - Max limit is 1.5MiB for the image * fixed lint errors * addressed PR comments - Added test * added snapshot * fixed failing test
This commit is contained in:
parent
ffae9baca2
commit
050da28d6e
@ -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<number>;
|
||||
@ -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 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)
|
||||
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();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="panelContent">
|
||||
<Stack className="paneMainContent" tokens={{ childrenGap: 20 }}>
|
||||
<Text>{descriptionPara1}</Text>
|
||||
<Text>{descriptionPara2}</Text>
|
||||
<TextField {...descriptionProps} />
|
||||
<TextField {...tagsProps} />
|
||||
<TextField {...thumbnailProps} />
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
private clearFormError = (): void => {
|
||||
this.formError = undefined;
|
||||
this.formErrorDetail = undefined;
|
||||
this.triggerRender();
|
||||
};
|
||||
|
||||
private createContent = (): JSX.Element => {
|
||||
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 <PublishNotebookPaneComponent {...publishNotebookPaneProps} />;
|
||||
};
|
||||
|
||||
private reset = (): void => {
|
||||
|
6
src/Explorer/Panes/PublishNotebookPaneComponent.less
Normal file
6
src/Explorer/Panes/PublishNotebookPaneComponent.less
Normal file
@ -0,0 +1,6 @@
|
||||
.publishNotebookPanelContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
23
src/Explorer/Panes/PublishNotebookPaneComponent.test.tsx
Normal file
23
src/Explorer/Panes/PublishNotebookPaneComponent.test.tsx
Normal file
@ -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(<PublishNotebookPaneComponent {...props} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
205
src/Explorer/Panes/PublishNotebookPaneComponent.tsx
Normal file
205
src/Explorer/Panes/PublishNotebookPaneComponent.tsx
Normal file
@ -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<PublishNotebookPaneProps, PublishNotebookPaneState> {
|
||||
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 (
|
||||
<div className="publishNotebookPanelContent">
|
||||
<Stack className="panelMainContent" tokens={{ childrenGap: 20 }}>
|
||||
<Stack.Item>
|
||||
<Text>{this.descriptionPara1}</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Text>{this.descriptionPara2}</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<TextField {...this.descriptionProps} />
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<TextField {...this.tagsProps} />
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Dropdown {...this.thumbnailSelectorProps} />
|
||||
</Stack.Item>
|
||||
|
||||
{this.state.type === PublishNotebookPaneComponent.ImageTypes[0] ? (
|
||||
<Stack.Item>
|
||||
<TextField {...this.thumbnailUrlProps} />
|
||||
</Stack.Item>
|
||||
) : (
|
||||
<Stack.Item>
|
||||
<input
|
||||
id="selectImageFile"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={event => {
|
||||
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 });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
)}
|
||||
<Stack.Item>
|
||||
<Text>Preview</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<GalleryCardComponent
|
||||
data={{
|
||||
id: undefined,
|
||||
name: this.props.notebookName,
|
||||
description: this.state.notebookDescription,
|
||||
gitSha: undefined,
|
||||
tags: this.state.notebookTags.split(","),
|
||||
author: this.props.notebookAuthor,
|
||||
thumbnailUrl: this.state.imageSrc,
|
||||
created: this.props.notebookCreatedDate,
|
||||
isSample: false,
|
||||
downloads: 0,
|
||||
favorites: 0,
|
||||
views: 0
|
||||
}}
|
||||
isFavorite={false}
|
||||
showDownload={true}
|
||||
showDelete={true}
|
||||
onClick={undefined}
|
||||
onTagClick={undefined}
|
||||
onFavoriteClick={undefined}
|
||||
onUnfavoriteClick={undefined}
|
||||
onDownloadClick={undefined}
|
||||
onDeleteClick={undefined}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PublishNotebookPaneComponent renders 1`] = `
|
||||
<div
|
||||
className="publishNotebookPanelContent"
|
||||
>
|
||||
<Stack
|
||||
className="panelMainContent"
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StackItem>
|
||||
<Text>
|
||||
This notebook has your data. Please make sure you delete any sensitive data/output before publishing.
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text>
|
||||
Would you like to publish and share "SampleNotebook" to the gallery?
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Description"
|
||||
label="Description"
|
||||
multiline={true}
|
||||
onChange={[Function]}
|
||||
required={true}
|
||||
rows={3}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Tags"
|
||||
label="Tags"
|
||||
onChange={[Function]}
|
||||
placeholder="Optional tag 1, Optional tag 2"
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledWithResponsiveMode
|
||||
ariaLabel="Cover image"
|
||||
defaultSelectedKey="URL"
|
||||
label="Cover image"
|
||||
onChange={[Function]}
|
||||
options={
|
||||
Array [
|
||||
Object {
|
||||
"key": "URL",
|
||||
"text": "URL",
|
||||
},
|
||||
Object {
|
||||
"key": "Custom Image",
|
||||
"text": "Custom Image",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<StyledTextFieldBase
|
||||
ariaLabel="Cover image url"
|
||||
label="Cover image url"
|
||||
onChange={[Function]}
|
||||
/>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<Text>
|
||||
Preview
|
||||
</Text>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<GalleryCardComponent
|
||||
data={
|
||||
Object {
|
||||
"author": "CosmosDB",
|
||||
"created": "2020-07-17T00:00:00Z",
|
||||
"description": "",
|
||||
"downloads": 0,
|
||||
"favorites": 0,
|
||||
"gitSha": undefined,
|
||||
"id": undefined,
|
||||
"isSample": false,
|
||||
"name": "SampleNotebook.ipynb",
|
||||
"tags": Array [
|
||||
"",
|
||||
],
|
||||
"thumbnailUrl": undefined,
|
||||
"views": 0,
|
||||
}
|
||||
}
|
||||
isFavorite={false}
|
||||
showDelete={true}
|
||||
showDownload={true}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</div>
|
||||
`;
|
Loading…
Reference in New Issue
Block a user