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:
parent
acc65c9588
commit
dc67c5f40b
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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>()
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,36 +188,12 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
private renderThumbnailSelectors(type: string) {
|
||||
switch (type) {
|
||||
case ImageTypes.Url:
|
||||
return <TextField {...this.thumbnailUrlProps} />;
|
||||
case ImageTypes.CustomImage:
|
||||
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"
|
||||
|
@ -166,8 +219,46 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
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">
|
||||
<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>
|
||||
|
||||
<Stack.Item>{this.renderThumbnailSelectors(this.state.type)}</Stack.Item>
|
||||
|
||||
<Stack.Item>
|
||||
<Text>Preview</Text>
|
||||
</Stack.Item>
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue