mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-02-16 17:25:58 +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 ko from "knockout";
|
||||||
import { ITextFieldProps, Stack, Text, TextField } from "office-ui-fabric-react";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
import * as Logger from "../../Common/Logger";
|
import * as Logger from "../../Common/Logger";
|
||||||
@ -7,8 +6,8 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
|||||||
import { JunoClient } from "../../Juno/JunoClient";
|
import { JunoClient } from "../../Juno/JunoClient";
|
||||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import { FileSystemUtil } from "../Notebook/FileSystemUtil";
|
|
||||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
||||||
|
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
||||||
|
|
||||||
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
parameters: ko.Observable<number>;
|
parameters: ko.Observable<number>;
|
||||||
@ -22,7 +21,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
private content: string;
|
private content: string;
|
||||||
private description: string;
|
private description: string;
|
||||||
private tags: string;
|
private tags: string;
|
||||||
private thumbnailUrl: string;
|
private imageSrc: string;
|
||||||
|
|
||||||
constructor(private container: ViewModels.Explorer, private junoClient: JunoClient) {
|
constructor(private container: ViewModels.Explorer, private junoClient: JunoClient) {
|
||||||
this.parameters = ko.observable(Date.now());
|
this.parameters = ko.observable(Date.now());
|
||||||
@ -87,7 +86,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
this.description,
|
this.description,
|
||||||
this.tags?.split(","),
|
this.tags?.split(","),
|
||||||
this.author,
|
this.author,
|
||||||
this.thumbnailUrl,
|
this.imageSrc,
|
||||||
this.content
|
this.content
|
||||||
);
|
);
|
||||||
if (!response.data) {
|
if (!response.data) {
|
||||||
@ -112,44 +111,37 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
|||||||
this.close();
|
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 => {
|
private createContent = (): JSX.Element => {
|
||||||
const descriptionPara1 =
|
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
||||||
"This notebook has your data. Please make sure you delete any sensitive data/output before publishing.";
|
notebookName: this.name,
|
||||||
const descriptionPara2 = `Would you like to publish and share ${FileSystemUtil.stripExtension(
|
notebookDescription: "",
|
||||||
this.name,
|
notebookTags: "",
|
||||||
"ipynb"
|
notebookAuthor: this.author,
|
||||||
)} to the gallery?`;
|
notebookCreatedDate: new Date().toISOString(),
|
||||||
const descriptionProps: ITextFieldProps = {
|
onChangeDescription: (newValue: string) => (this.description = newValue),
|
||||||
label: "Description",
|
onChangeTags: (newValue: string) => (this.tags = newValue),
|
||||||
ariaLabel: "Description",
|
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
||||||
multiline: true,
|
onError: this.createFormErrorForLargeImageSelection,
|
||||||
rows: 3,
|
clearFormError: this.clearFormError
|
||||||
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)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return <PublishNotebookPaneComponent {...publishNotebookPaneProps} />;
|
||||||
<div className="panelContent">
|
|
||||||
<Stack className="paneMainContent" tokens={{ childrenGap: 20 }}>
|
|
||||||
<Text>{descriptionPara1}</Text>
|
|
||||||
<Text>{descriptionPara2}</Text>
|
|
||||||
<TextField {...descriptionProps} />
|
|
||||||
<TextField {...tagsProps} />
|
|
||||||
<TextField {...thumbnailProps} />
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private reset = (): void => {
|
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…
x
Reference in New Issue
Block a user