mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-22 18:32:00 +00:00
Migrate Publish Notebook Pane to React (#641)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
This commit is contained in:
@@ -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<PublishNotebookPaneProps> = ({
|
||||
notebookName,
|
||||
notebookTags,
|
||||
imageSrc,
|
||||
notebookDescription,
|
||||
notebookAuthor,
|
||||
notebookCreatedDate,
|
||||
notebookObject,
|
||||
notebookParentDomElement,
|
||||
onError,
|
||||
clearFormError,
|
||||
setNotebookName,
|
||||
setNotebookDescription,
|
||||
setNotebookTags,
|
||||
setImageSrc,
|
||||
}: PublishNotebookPaneProps) => {
|
||||
const [type, setType] = useState<string>(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 <TextField {...thumbnailUrlProps} />;
|
||||
case ImageTypes.CustomImage:
|
||||
return (
|
||||
<input
|
||||
id="selectImageFile"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(event) => {
|
||||
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<HTMLElement>(".nteract-cell-outputs");
|
||||
return cellOutputDomElements[indexOfFirstCodeCellWithDisplay];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="publishNotebookPanelContent">
|
||||
<Stack className="panelMainContent" tokens={{ childrenGap: 20 }}>
|
||||
<Stack.Item>
|
||||
<Text>{descriptionPara1}</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Text>{descriptionPara2}</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<TextField
|
||||
label="Name"
|
||||
ariaLabel="Name"
|
||||
defaultValue={FileSystemUtil.stripExtension(notebookName, "ipynb")}
|
||||
required
|
||||
onChange={(event, newValue) => {
|
||||
const notebookName = newValue + ".ipynb";
|
||||
setNotebookName(notebookName);
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<TextField
|
||||
label="Description"
|
||||
ariaLabel="Description"
|
||||
multiline
|
||||
rows={3}
|
||||
required
|
||||
onChange={(event, newValue) => {
|
||||
setNotebookDescription(newValue);
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<TextField
|
||||
label="Tags"
|
||||
ariaLabel="Tags"
|
||||
placeholder="Optional tag 1, Optional tag 2"
|
||||
onChange={(event, newValue) => {
|
||||
setNotebookTags(newValue);
|
||||
}}
|
||||
/>
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Dropdown {...thumbnailSelectorProps} />
|
||||
</Stack.Item>
|
||||
|
||||
<Stack.Item>{renderThumbnailSelectors(type)}</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Text>Preview</Text>
|
||||
</Stack.Item>
|
||||
<Stack.Item>
|
||||
<GalleryCardComponent
|
||||
data={{
|
||||
id: undefined,
|
||||
name: notebookName,
|
||||
description: notebookDescription,
|
||||
gitSha: undefined,
|
||||
tags: notebookTags.split(","),
|
||||
author: notebookAuthor,
|
||||
thumbnailUrl: imageSrc,
|
||||
created: notebookCreatedDate,
|
||||
isSample: false,
|
||||
downloads: undefined,
|
||||
favorites: undefined,
|
||||
views: undefined,
|
||||
newCellId: undefined,
|
||||
policyViolations: undefined,
|
||||
pendingScanJobIds: undefined,
|
||||
}}
|
||||
isFavorite={undefined}
|
||||
showDownload={false}
|
||||
showDelete={false}
|
||||
onClick={() => undefined}
|
||||
onTagClick={undefined}
|
||||
onFavoriteClick={undefined}
|
||||
onUnfavoriteClick={undefined}
|
||||
onDownloadClick={undefined}
|
||||
onDeleteClick={undefined}
|
||||
/>
|
||||
</Stack.Item>
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user