Added support for taking screenshot during Notebook publish to Gallery (#108)

* Added support for taking screenshot

- Screenshot is taken using html2canvas package
- Converted to base 64 and uploaded to metadata
- For Using first display output
  - Notebok object is passed instead of string, to publish pane
  - The first cell with output present is parsed out
  - The dom is also parsed to get corresponding div element to take screenshot of the first output

* fixed bug

* Addressed PR comments

- FIxed bug that didn't capture screenshot when mutiple notebook tabs are opened

* removed unnecessary dependencies

* fixed compile issues

* more edits
This commit is contained in:
Srinath Narayanan 2020-07-23 00:43:53 -07:00 committed by GitHub
parent acc65c9588
commit dc67c5f40b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 525 additions and 208 deletions

21
package-lock.json generated
View File

@ -9519,6 +9519,11 @@
"resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
"integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA="
},
"base64-arraybuffer": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz",
"integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ=="
},
"base64-js": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
@ -10874,6 +10879,14 @@
"resolved": "https://registry.npmjs.org/css-element-queries/-/css-element-queries-1.1.1.tgz",
"integrity": "sha512-/PX6Bkk77ShgbOx/mpawHdEvS3PGgy1mmMktcztDPndWdMJxcorcQiivrs+nEljqtBpvNEhAmQky9tQR6FSm8Q=="
},
"css-line-break": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz",
"integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==",
"requires": {
"base64-arraybuffer": "^0.2.0"
}
},
"css-loader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-1.0.0.tgz",
@ -15557,6 +15570,14 @@
}
}
},
"html2canvas": {
"version": "1.0.0-rc.5",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.5.tgz",
"integrity": "sha512-DtNqPxJNXPoTajs+lVQzGS1SULRI4GQaROeU5R41xH8acffHukxRh/NBVcTBsfCkJSkLq91rih5TpbEwUP9yWA==",
"requires": {
"css-line-break": "1.1.1"
}
},
"htmlparser2": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",

View File

@ -54,6 +54,7 @@
"es6-symbol": "3.1.3",
"eslint-plugin-jest": "23.13.2",
"hasher": "1.2.0",
"html2canvas": "1.0.0-rc.5",
"immutable": "4.0.0-rc.12",
"is-ci": "2.0.0",
"jquery": "3.5.1",

View File

@ -36,6 +36,8 @@ export interface GalleryCardComponentProps {
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
public static readonly CARD_WIDTH = 256;
private static readonly cardImageHeight = 144;
public static readonly cardHeightToWidthRatio =
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
private static readonly cardDescriptionMaxChars = 88;
private static readonly cardItemGapBig = 10;
private static readonly cardItemGapSmall = 8;

View File

@ -2347,9 +2347,9 @@ export default class Explorer {
return Promise.resolve(false);
}
public publishNotebook(name: string, content: string): void {
public publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): void {
if (this.notebookManager) {
this.notebookManager.openPublishNotebookPane(name, content);
this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
this.isPublishNotebookPaneEnabled(true);
}

View File

@ -8,6 +8,7 @@ import { actions, createContentRef, createKernelRef, selectors } from "@nteract/
import VirtualCommandBarComponent from "./VirtualCommandBarComponent";
import { NotebookContentItem } from "../NotebookContentItem";
import { NotebookComponentBootstrapper } from "./NotebookComponentBootstrapper";
import { CdbAppState } from "./types";
export interface NotebookComponentAdapterOptions {
contentItem: NotebookContentItem;
@ -18,6 +19,7 @@ export interface NotebookComponentAdapterOptions {
export class NotebookComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
private onUpdateKernelInfo: () => void;
public getNotebookParentElement: () => HTMLElement;
public parameters: any;
constructor(options: NotebookComponentAdapterOptions) {
@ -44,6 +46,11 @@ export class NotebookComponentAdapter extends NotebookComponentBootstrapper impl
})
);
}
this.getNotebookParentElement = () => {
const cdbAppState = this.getStore().getState() as CdbAppState;
return cdbAppState.cdb.currentNotebookParentElements.get(this.contentRef);
};
}
protected renderExtraComponent = (): JSX.Element => {

View File

@ -17,7 +17,7 @@ import {
} from "@nteract/core";
import * as Immutable from "immutable";
import { Provider } from "react-redux";
import { CellType, CellId, toJS } from "@nteract/commutable";
import { CellType, CellId, ImmutableNotebook } from "@nteract/commutable";
import { Store, AnyAction } from "redux";
import "./NotebookComponent.less";
@ -71,14 +71,14 @@ export class NotebookComponentBootstrapper {
);
}
public getContent(): { name: string; content: string } {
public getContent(): { name: string; content: string | ImmutableNotebook } {
const record = this.getStore()
.getState()
.core.entities.contents.byRef.get(this.contentRef);
let content: string;
let content: string | ImmutableNotebook;
switch (record.model.type) {
case "notebook":
content = JSON.stringify(toJS(record.model.notebook));
content = record.model.notebook;
break;
case "file":
content = record.model.text;

View File

@ -84,3 +84,22 @@ export const traceNotebookTelemetry = (payload: {
payload
};
};
export const UPDATE_NOTEBOOK_PARENT_DOM_ELTS = "UPDATE_NOTEBOOK_PARENT_DOM_ELTS";
export interface UpdateNotebookParentDomEltAction {
type: "UPDATE_NOTEBOOK_PARENT_DOM_ELTS";
payload: {
contentRef: ContentRef;
parentElt: HTMLElement;
};
}
export const UpdateNotebookParentDomElt = (payload: {
contentRef: ContentRef;
parentElt: HTMLElement;
}): UpdateNotebookParentDomEltAction => {
return {
type: UPDATE_NOTEBOOK_PARENT_DOM_ELTS,
payload
};
};

View File

@ -82,6 +82,19 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
});
return state;
}
case cdbActions.UPDATE_NOTEBOOK_PARENT_DOM_ELTS: {
const typedAction = action as cdbActions.UpdateNotebookParentDomEltAction;
var parentEltsMap = state.get("currentNotebookParentElements");
const contentRef = typedAction.payload.contentRef;
const parentElt = typedAction.payload.parentElt;
if (parentElt) {
parentEltsMap.set(contentRef, parentElt);
} else {
parentEltsMap.delete(contentRef);
}
return state.set("currentNotebookParentElements", parentEltsMap);
}
}
return state;
};

View File

@ -1,5 +1,5 @@
import * as Immutable from "immutable";
import { AppState } from "@nteract/core";
import { AppState, ContentRef } from "@nteract/core";
import { Notebook } from "../../../Common/Constants";
import { CellId } from "@nteract/commutable";
@ -9,6 +9,7 @@ export interface CdbRecordProps {
defaultExperience: string | undefined;
kernelRestartDelayMs: number;
hoveredCellId: CellId | undefined;
currentNotebookParentElements: Map<ContentRef, HTMLElement>;
}
export type CdbRecord = Immutable.RecordOf<CdbRecordProps>;
@ -21,5 +22,6 @@ export const makeCdbRecord = Immutable.Record<CdbRecordProps>({
databaseAccountName: undefined,
defaultExperience: undefined,
kernelRestartDelayMs: Notebook.kernelRestartInitialDelayMs,
hoveredCellId: undefined
hoveredCellId: undefined,
currentNotebookParentElements: new Map<ContentRef, HTMLElement>()
});

View File

@ -23,6 +23,7 @@ import { DialogProps } from "../Controls/DialogReactComponent/DialogComponent";
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
import { getFullName } from "../../Utils/UserUtils";
import { ImmutableNotebook } from "@nteract/commutable";
import Explorer from "../Explorer";
export interface NotebookManagerOptions {
@ -108,8 +109,12 @@ export default class NotebookManager {
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
}
public openPublishNotebookPane(name: string, content: string): void {
this.publishNotebookPaneAdapter.open(name, getFullName(), content);
public openPublishNotebookPane(
name: string,
content: string | ImmutableNotebook,
parentDomElement: HTMLElement
): void {
this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement);
}
// Octokit's error handler uses any

View File

@ -30,11 +30,18 @@ import { CellType } from "@nteract/commutable/src";
import "./NotebookRenderer.less";
import HoverableCell from "./decorators/HoverableCell";
import CellLabeler from "./decorators/CellLabeler";
import * as cdbActions from "../NotebookComponent/actions";
export interface NotebookRendererProps {
export interface NotebookRendererBaseProps {
contentRef: any;
}
interface NotebookRendererDispatchProps {
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => void;
}
type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatchProps;
interface PassedEditorProps {
id: string;
contentRef: ContentRef;
@ -68,6 +75,8 @@ const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, child
};
class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
private notebookRendererRef = React.createRef<HTMLDivElement>();
constructor(props: NotebookRendererProps) {
super(props);
@ -78,13 +87,22 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
componentDidMount() {
loadTransform(this.props as any);
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
}
componentDidUpdate() {
this.props.updateNotebookParentDomElt(this.props.contentRef, this.notebookRendererRef.current);
}
componentWillUnmount() {
this.props.updateNotebookParentDomElt(this.props.contentRef, undefined);
}
render(): JSX.Element {
return (
<>
<div className="NotebookRendererContainer">
<div className="NotebookRenderer">
<div className="NotebookRenderer" ref={this.notebookRendererRef}>
<DndProvider backend={HTML5Backend}>
<KeyboardShortcuts contentRef={this.props.contentRef}>
<Cells contentRef={this.props.contentRef}>
@ -146,7 +164,7 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
}
}
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererProps) => {
const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: NotebookRendererBaseProps) => {
const mapDispatchToProps = (dispatch: Dispatch) => {
return {
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
@ -156,6 +174,14 @@ const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: Noteboo
component: transform
})
);
},
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => {
return dispatch(
cdbActions.UpdateNotebookParentDomElt({
contentRef,
parentElt
})
);
}
};
};

View File

@ -1,5 +1,15 @@
import { NotebookUtil } from "./NotebookUtil";
import * as GitHubUtils from "../../Utils/GitHubUtils";
import {
ImmutableNotebook,
MediaBundle,
CodeCellParams,
MarkdownCellParams,
makeCodeCell,
makeMarkdownCell,
makeNotebookRecord
} from "@nteract/commutable";
import { List, Map } from "immutable";
const fileName = "file";
const notebookName = "file.ipynb";
@ -7,6 +17,57 @@ const filePath = `folder/${fileName}`;
const notebookPath = `folder/${notebookName}`;
const gitHubFileUri = GitHubUtils.toContentUri("owner", "repo", "branch", filePath);
const gitHubNotebookUri = GitHubUtils.toContentUri("owner", "repo", "branch", notebookPath);
const notebookRecord = makeNotebookRecord({
cellOrder: List.of("0", "1", "2", "3"),
cellMap: Map({
"0": makeMarkdownCell({
cell_type: "markdown",
source: "abc",
metadata: undefined
} as MarkdownCellParams),
"1": makeCodeCell({
cell_type: "code",
execution_count: undefined,
metadata: undefined,
source: "print(5)",
outputs: List.of({
name: "stdout",
output_type: "stream",
text: "5"
})
} as CodeCellParams),
"2": makeCodeCell({
cell_type: "code",
execution_count: undefined,
metadata: undefined,
source: 'display(HTML("<h1>Sample html</h1>"))',
outputs: List.of({
data: Object.freeze({
data: {
"text/html": "<h1>Sample output</h1>",
"text/plain": "<IPython.core.display.HTML object>"
}
} as MediaBundle),
output_type: "display_data",
metadata: undefined
})
} as CodeCellParams),
"3": makeCodeCell({
cell_type: "code",
execution_count: undefined,
metadata: undefined,
source: 'print("hello world")',
outputs: List.of({
name: "stdout",
output_type: "stream",
text: "hello world"
})
} as CodeCellParams)
}),
nbformat_minor: 2,
nbformat: 2,
metadata: undefined
});
describe("NotebookUtil", () => {
describe("isNotebookFile", () => {
@ -46,4 +107,11 @@ describe("NotebookUtil", () => {
);
});
});
describe("findFirstCodeCellWithDisplay", () => {
it("works for Notebook file", () => {
const notebookObject = notebookRecord as ImmutableNotebook;
expect(NotebookUtil.findFirstCodeCellWithDisplay(notebookObject)).toEqual(1);
});
});
});

View File

@ -1,5 +1,5 @@
import path from "path";
import { ImmutableNotebook } from "@nteract/commutable";
import { ImmutableNotebook, ImmutableCodeCell, ImmutableOutput } from "@nteract/commutable";
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
import { StringUtils } from "../../Utils/StringUtils";
import * as GitHubUtils from "../../Utils/GitHubUtils";
@ -100,4 +100,30 @@ export class NotebookUtil {
const basePath = path.split(contentName).shift();
return `${basePath}${newName}`;
}
public static findFirstCodeCellWithDisplay(notebookObject: ImmutableNotebook): number {
let codeCellCount = -1;
for (let i = 0; i < notebookObject.cellOrder.size; i++) {
const cellId = notebookObject.cellOrder.get(i);
if (cellId) {
const cell = notebookObject.cellMap.get(cellId);
if (cell && cell.cell_type === "code") {
codeCellCount++;
const codeCell = cell as ImmutableCodeCell;
if (codeCell.outputs) {
const displayOutput = codeCell.outputs.find((output: ImmutableOutput) => {
if (output.output_type === "display_data" || output.output_type === "execute_result") {
return true;
}
return false;
});
if (displayOutput) {
return codeCellCount;
}
}
}
}
}
throw new Error("Output does not exist for any of the cells.");
}
}

View File

@ -2,12 +2,14 @@ import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import * as Logger from "../../Common/Logger";
import Explorer from "../Explorer";
import { JunoClient } from "../../Juno/JunoClient";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
import Explorer from "../Explorer";
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
import { ImmutableNotebook } from "@nteract/commutable/src";
import { toJS } from "@nteract/commutable";
export class PublishNotebookPaneAdapter implements ReactAdapter {
parameters: ko.Observable<number>;
@ -22,6 +24,8 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
private description: string;
private tags: string;
private imageSrc: string;
private notebookObject: ImmutableNotebook;
private parentDomElement: HTMLElement;
constructor(private container: Explorer, private junoClient: JunoClient) {
this.parameters = ko.observable(Date.now());
@ -54,10 +58,21 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
public open(name: string, author: string, content: string): void {
public open(
name: string,
author: string,
notebookContent: string | ImmutableNotebook,
parentDomElement: HTMLElement
): void {
this.name = name;
this.author = author;
this.content = content;
if (typeof notebookContent === "string") {
this.content = notebookContent as string;
} else {
this.content = JSON.stringify(toJS(notebookContent as ImmutableNotebook));
this.notebookObject = notebookContent;
}
this.parentDomElement = parentDomElement;
this.isOpened = true;
this.triggerRender();
@ -134,6 +149,8 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
notebookTags: "",
notebookAuthor: this.author,
notebookCreatedDate: new Date().toISOString(),
notebookObject: this.notebookObject,
notebookParentDomElement: this.parentDomElement,
onChangeDescription: (newValue: string) => (this.description = newValue),
onChangeTags: (newValue: string) => (this.tags = newValue),
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
@ -152,5 +169,10 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
this.name = undefined;
this.author = undefined;
this.content = undefined;
this.description = undefined;
this.tags = undefined;
this.imageSrc = undefined;
this.notebookObject = undefined;
this.parentDomElement = undefined;
};
}

View File

@ -10,6 +10,8 @@ describe("PublishNotebookPaneComponent", () => {
notebookTags: "tag1, tag2",
notebookAuthor: "CosmosDB",
notebookCreatedDate: "2020-07-17T00:00:00Z",
notebookObject: undefined,
notebookParentDomElement: undefined,
onChangeDescription: undefined,
onChangeTags: undefined,
onChangeImageSrc: undefined,

View File

@ -3,6 +3,9 @@ import * as React from "react";
import { GalleryCardComponent } from "../Controls/NotebookGallery/Cards/GalleryCardComponent";
import { 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;
@ -10,6 +13,8 @@ export interface PublishNotebookPaneProps {
notebookTags: string;
notebookAuthor: string;
notebookCreatedDate: string;
notebookObject: ImmutableNotebook;
notebookParentDomElement: HTMLElement;
onChangeDescription: (newValue: string) => void;
onChangeTags: (newValue: string) => void;
onChangeImageSrc: (newValue: string) => void;
@ -24,9 +29,15 @@ interface PublishNotebookPaneState {
imageSrc: string;
}
enum ImageTypes {
Url = "URL",
CustomImage = "Custom Image",
TakeScreenshot = "Take Screenshot",
UseFirstDisplayOutput = "Use First Display Output"
}
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;
@ -34,12 +45,13 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
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: PublishNotebookPaneComponent.ImageTypes[0],
type: ImageTypes.Url,
notebookDescription: "",
notebookTags: "",
imageSrc: undefined
@ -61,6 +73,38 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
};
};
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 = function() {
context.drawImage(image, 0, 0);
updateImageSrcWithScreenshot(canvas.toDataURL());
};
})
.catch(error => {
onError(error);
});
};
this.descriptionPara1 =
"This notebook has your data. Please make sure you delete any sensitive data/output before publishing.";
@ -78,12 +122,45 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
}
};
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);
};
this.thumbnailSelectorProps = {
label: "Cover image",
defaultSelectedKey: PublishNotebookPaneComponent.ImageTypes[0],
defaultSelectedKey: ImageTypes.Url,
ariaLabel: "Cover image",
options: PublishNotebookPaneComponent.ImageTypes.map((value: string) => ({ text: value, key: value })),
onChange: (event, options) => {
options: [
ImageTypes.Url,
ImageTypes.CustomImage,
ImageTypes.TakeScreenshot,
ImageTypes.UseFirstDisplayOutput
].map((value: string) => ({ text: value, key: value })),
onChange: async (event, options) => {
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 });
}
};
@ -111,6 +188,51 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
};
}
private renderThumbnailSelectors(type: string) {
switch (type) {
case ImageTypes.Url:
return <TextField {...this.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 > 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<HTMLElement>(
".nteract-cell-outputs"
);
return cellOutputDomElements[indexOfFirstCodeCellWithDisplay];
}
public render(): JSX.Element {
return (
<div className="publishNotebookPanelContent">
@ -135,39 +257,8 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
<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";
<Stack.Item>{this.renderThumbnailSelectors(this.state.type)}</Stack.Item>
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>

View File

@ -56,6 +56,14 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
"key": "Custom Image",
"text": "Custom Image",
},
Object {
"key": "Take Screenshot",
"text": "Take Screenshot",
},
Object {
"key": "Use First Display Output",
"text": "Use First Display Output",
},
]
}
/>

View File

@ -449,7 +449,11 @@ export default class NotebookTabV2 extends TabsBase implements ViewModels.Tab {
private publishToGallery = () => {
const notebookContent = this.notebookComponentAdapter.getContent();
this.container.publishNotebook(notebookContent.name, notebookContent.content);
this.container.publishNotebook(
notebookContent.name,
notebookContent.content,
this.notebookComponentAdapter.getNotebookParentElement()
);
};
private traceTelemetry(actionType: number) {