mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-25 15:06:55 +00:00
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
21
package-lock.json
generated
21
package-lock.json
generated
@ -9519,6 +9519,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz",
|
||||||
"integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA="
|
"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": {
|
"base64-js": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/css-element-queries/-/css-element-queries-1.1.1.tgz",
|
||||||
"integrity": "sha512-/PX6Bkk77ShgbOx/mpawHdEvS3PGgy1mmMktcztDPndWdMJxcorcQiivrs+nEljqtBpvNEhAmQky9tQR6FSm8Q=="
|
"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": {
|
"css-loader": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-loader/-/css-loader-1.0.0.tgz",
|
"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": {
|
"htmlparser2": {
|
||||||
"version": "3.10.1",
|
"version": "3.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
|
||||||
|
@ -54,6 +54,7 @@
|
|||||||
"es6-symbol": "3.1.3",
|
"es6-symbol": "3.1.3",
|
||||||
"eslint-plugin-jest": "23.13.2",
|
"eslint-plugin-jest": "23.13.2",
|
||||||
"hasher": "1.2.0",
|
"hasher": "1.2.0",
|
||||||
|
"html2canvas": "1.0.0-rc.5",
|
||||||
"immutable": "4.0.0-rc.12",
|
"immutable": "4.0.0-rc.12",
|
||||||
"is-ci": "2.0.0",
|
"is-ci": "2.0.0",
|
||||||
"jquery": "3.5.1",
|
"jquery": "3.5.1",
|
||||||
|
@ -36,6 +36,8 @@ export interface GalleryCardComponentProps {
|
|||||||
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
|
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
|
||||||
public static readonly CARD_WIDTH = 256;
|
public static readonly CARD_WIDTH = 256;
|
||||||
private static readonly cardImageHeight = 144;
|
private static readonly cardImageHeight = 144;
|
||||||
|
public static readonly cardHeightToWidthRatio =
|
||||||
|
GalleryCardComponent.cardImageHeight / GalleryCardComponent.CARD_WIDTH;
|
||||||
private static readonly cardDescriptionMaxChars = 88;
|
private static readonly cardDescriptionMaxChars = 88;
|
||||||
private static readonly cardItemGapBig = 10;
|
private static readonly cardItemGapBig = 10;
|
||||||
private static readonly cardItemGapSmall = 8;
|
private static readonly cardItemGapSmall = 8;
|
||||||
|
@ -2347,9 +2347,9 @@ export default class Explorer {
|
|||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public publishNotebook(name: string, content: string): void {
|
public publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): void {
|
||||||
if (this.notebookManager) {
|
if (this.notebookManager) {
|
||||||
this.notebookManager.openPublishNotebookPane(name, content);
|
this.notebookManager.openPublishNotebookPane(name, content, parentDomElement);
|
||||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||||
this.isPublishNotebookPaneEnabled(true);
|
this.isPublishNotebookPaneEnabled(true);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { actions, createContentRef, createKernelRef, selectors } from "@nteract/
|
|||||||
import VirtualCommandBarComponent from "./VirtualCommandBarComponent";
|
import VirtualCommandBarComponent from "./VirtualCommandBarComponent";
|
||||||
import { NotebookContentItem } from "../NotebookContentItem";
|
import { NotebookContentItem } from "../NotebookContentItem";
|
||||||
import { NotebookComponentBootstrapper } from "./NotebookComponentBootstrapper";
|
import { NotebookComponentBootstrapper } from "./NotebookComponentBootstrapper";
|
||||||
|
import { CdbAppState } from "./types";
|
||||||
|
|
||||||
export interface NotebookComponentAdapterOptions {
|
export interface NotebookComponentAdapterOptions {
|
||||||
contentItem: NotebookContentItem;
|
contentItem: NotebookContentItem;
|
||||||
@ -18,6 +19,7 @@ export interface NotebookComponentAdapterOptions {
|
|||||||
|
|
||||||
export class NotebookComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
|
export class NotebookComponentAdapter extends NotebookComponentBootstrapper implements ReactAdapter {
|
||||||
private onUpdateKernelInfo: () => void;
|
private onUpdateKernelInfo: () => void;
|
||||||
|
public getNotebookParentElement: () => HTMLElement;
|
||||||
public parameters: any;
|
public parameters: any;
|
||||||
|
|
||||||
constructor(options: NotebookComponentAdapterOptions) {
|
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 => {
|
protected renderExtraComponent = (): JSX.Element => {
|
||||||
|
@ -17,7 +17,7 @@ import {
|
|||||||
} from "@nteract/core";
|
} from "@nteract/core";
|
||||||
import * as Immutable from "immutable";
|
import * as Immutable from "immutable";
|
||||||
import { Provider } from "react-redux";
|
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 { Store, AnyAction } from "redux";
|
||||||
|
|
||||||
import "./NotebookComponent.less";
|
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()
|
const record = this.getStore()
|
||||||
.getState()
|
.getState()
|
||||||
.core.entities.contents.byRef.get(this.contentRef);
|
.core.entities.contents.byRef.get(this.contentRef);
|
||||||
let content: string;
|
let content: string | ImmutableNotebook;
|
||||||
switch (record.model.type) {
|
switch (record.model.type) {
|
||||||
case "notebook":
|
case "notebook":
|
||||||
content = JSON.stringify(toJS(record.model.notebook));
|
content = record.model.notebook;
|
||||||
break;
|
break;
|
||||||
case "file":
|
case "file":
|
||||||
content = record.model.text;
|
content = record.model.text;
|
||||||
|
@ -84,3 +84,22 @@ export const traceNotebookTelemetry = (payload: {
|
|||||||
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;
|
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;
|
return state;
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import * as Immutable from "immutable";
|
import * as Immutable from "immutable";
|
||||||
import { AppState } from "@nteract/core";
|
import { AppState, ContentRef } from "@nteract/core";
|
||||||
|
|
||||||
import { Notebook } from "../../../Common/Constants";
|
import { Notebook } from "../../../Common/Constants";
|
||||||
import { CellId } from "@nteract/commutable";
|
import { CellId } from "@nteract/commutable";
|
||||||
@ -9,6 +9,7 @@ export interface CdbRecordProps {
|
|||||||
defaultExperience: string | undefined;
|
defaultExperience: string | undefined;
|
||||||
kernelRestartDelayMs: number;
|
kernelRestartDelayMs: number;
|
||||||
hoveredCellId: CellId | undefined;
|
hoveredCellId: CellId | undefined;
|
||||||
|
currentNotebookParentElements: Map<ContentRef, HTMLElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CdbRecord = Immutable.RecordOf<CdbRecordProps>;
|
export type CdbRecord = Immutable.RecordOf<CdbRecordProps>;
|
||||||
@ -21,5 +22,6 @@ export const makeCdbRecord = Immutable.Record<CdbRecordProps>({
|
|||||||
databaseAccountName: undefined,
|
databaseAccountName: undefined,
|
||||||
defaultExperience: undefined,
|
defaultExperience: undefined,
|
||||||
kernelRestartDelayMs: Notebook.kernelRestartInitialDelayMs,
|
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 { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||||
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
|
import { PublishNotebookPaneAdapter } from "../Panes/PublishNotebookPaneAdapter";
|
||||||
import { getFullName } from "../../Utils/UserUtils";
|
import { getFullName } from "../../Utils/UserUtils";
|
||||||
|
import { ImmutableNotebook } from "@nteract/commutable";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
|
|
||||||
export interface NotebookManagerOptions {
|
export interface NotebookManagerOptions {
|
||||||
@ -108,8 +109,12 @@ export default class NotebookManager {
|
|||||||
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openPublishNotebookPane(name: string, content: string): void {
|
public openPublishNotebookPane(
|
||||||
this.publishNotebookPaneAdapter.open(name, getFullName(), content);
|
name: string,
|
||||||
|
content: string | ImmutableNotebook,
|
||||||
|
parentDomElement: HTMLElement
|
||||||
|
): void {
|
||||||
|
this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Octokit's error handler uses any
|
// Octokit's error handler uses any
|
||||||
|
@ -30,11 +30,18 @@ import { CellType } from "@nteract/commutable/src";
|
|||||||
import "./NotebookRenderer.less";
|
import "./NotebookRenderer.less";
|
||||||
import HoverableCell from "./decorators/HoverableCell";
|
import HoverableCell from "./decorators/HoverableCell";
|
||||||
import CellLabeler from "./decorators/CellLabeler";
|
import CellLabeler from "./decorators/CellLabeler";
|
||||||
|
import * as cdbActions from "../NotebookComponent/actions";
|
||||||
|
|
||||||
export interface NotebookRendererProps {
|
export interface NotebookRendererBaseProps {
|
||||||
contentRef: any;
|
contentRef: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NotebookRendererDispatchProps {
|
||||||
|
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotebookRendererProps = NotebookRendererBaseProps & NotebookRendererDispatchProps;
|
||||||
|
|
||||||
interface PassedEditorProps {
|
interface PassedEditorProps {
|
||||||
id: string;
|
id: string;
|
||||||
contentRef: ContentRef;
|
contentRef: ContentRef;
|
||||||
@ -68,6 +75,8 @@ const decorate = (id: string, contentRef: ContentRef, cell_type: CellType, child
|
|||||||
};
|
};
|
||||||
|
|
||||||
class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
||||||
|
private notebookRendererRef = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
constructor(props: NotebookRendererProps) {
|
constructor(props: NotebookRendererProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@ -78,13 +87,22 @@ class BaseNotebookRenderer extends React.Component<NotebookRendererProps> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
loadTransform(this.props as any);
|
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 {
|
render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="NotebookRendererContainer">
|
<div className="NotebookRendererContainer">
|
||||||
<div className="NotebookRenderer">
|
<div className="NotebookRenderer" ref={this.notebookRendererRef}>
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<KeyboardShortcuts contentRef={this.props.contentRef}>
|
<KeyboardShortcuts contentRef={this.props.contentRef}>
|
||||||
<Cells 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) => {
|
const mapDispatchToProps = (dispatch: Dispatch) => {
|
||||||
return {
|
return {
|
||||||
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
|
addTransform: (transform: React.ComponentType & { MIMETYPE: string }) => {
|
||||||
@ -156,6 +174,14 @@ const makeMapDispatchToProps = (initialDispatch: Dispatch, initialProps: Noteboo
|
|||||||
component: transform
|
component: transform
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
updateNotebookParentDomElt: (contentRef: ContentRef, parentElt: HTMLElement) => {
|
||||||
|
return dispatch(
|
||||||
|
cdbActions.UpdateNotebookParentDomElt({
|
||||||
|
contentRef,
|
||||||
|
parentElt
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,15 @@
|
|||||||
import { NotebookUtil } from "./NotebookUtil";
|
import { NotebookUtil } from "./NotebookUtil";
|
||||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
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 fileName = "file";
|
||||||
const notebookName = "file.ipynb";
|
const notebookName = "file.ipynb";
|
||||||
@ -7,6 +17,57 @@ const filePath = `folder/${fileName}`;
|
|||||||
const notebookPath = `folder/${notebookName}`;
|
const notebookPath = `folder/${notebookName}`;
|
||||||
const gitHubFileUri = GitHubUtils.toContentUri("owner", "repo", "branch", filePath);
|
const gitHubFileUri = GitHubUtils.toContentUri("owner", "repo", "branch", filePath);
|
||||||
const gitHubNotebookUri = GitHubUtils.toContentUri("owner", "repo", "branch", notebookPath);
|
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("NotebookUtil", () => {
|
||||||
describe("isNotebookFile", () => {
|
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 path from "path";
|
||||||
import { ImmutableNotebook } from "@nteract/commutable";
|
import { ImmutableNotebook, ImmutableCodeCell, ImmutableOutput } from "@nteract/commutable";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||||
import { StringUtils } from "../../Utils/StringUtils";
|
import { StringUtils } from "../../Utils/StringUtils";
|
||||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
@ -100,4 +100,30 @@ export class NotebookUtil {
|
|||||||
const basePath = path.split(contentName).shift();
|
const basePath = path.split(contentName).shift();
|
||||||
return `${basePath}${newName}`;
|
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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,156 +1,178 @@
|
|||||||
import ko from "knockout";
|
import ko from "knockout";
|
||||||
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";
|
||||||
import { JunoClient } from "../../Juno/JunoClient";
|
import Explorer from "../Explorer";
|
||||||
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
import { JunoClient } from "../../Juno/JunoClient";
|
||||||
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
|
||||||
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
import Explorer from "../Explorer";
|
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
||||||
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
||||||
|
import { ImmutableNotebook } from "@nteract/commutable/src";
|
||||||
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
import { toJS } from "@nteract/commutable";
|
||||||
parameters: ko.Observable<number>;
|
|
||||||
private isOpened: boolean;
|
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
private isExecuting: boolean;
|
parameters: ko.Observable<number>;
|
||||||
private formError: string;
|
private isOpened: boolean;
|
||||||
private formErrorDetail: string;
|
private isExecuting: boolean;
|
||||||
|
private formError: string;
|
||||||
private name: string;
|
private formErrorDetail: string;
|
||||||
private author: string;
|
|
||||||
private content: string;
|
private name: string;
|
||||||
private description: string;
|
private author: string;
|
||||||
private tags: string;
|
private content: string;
|
||||||
private imageSrc: string;
|
private description: string;
|
||||||
|
private tags: string;
|
||||||
constructor(private container: Explorer, private junoClient: JunoClient) {
|
private imageSrc: string;
|
||||||
this.parameters = ko.observable(Date.now());
|
private notebookObject: ImmutableNotebook;
|
||||||
this.reset();
|
private parentDomElement: HTMLElement;
|
||||||
this.triggerRender();
|
|
||||||
}
|
constructor(private container: Explorer, private junoClient: JunoClient) {
|
||||||
|
this.parameters = ko.observable(Date.now());
|
||||||
public renderComponent(): JSX.Element {
|
this.reset();
|
||||||
if (!this.isOpened) {
|
this.triggerRender();
|
||||||
return undefined;
|
}
|
||||||
}
|
|
||||||
|
public renderComponent(): JSX.Element {
|
||||||
const props: GenericRightPaneProps = {
|
if (!this.isOpened) {
|
||||||
container: this.container,
|
return undefined;
|
||||||
content: this.createContent(),
|
}
|
||||||
formError: this.formError,
|
|
||||||
formErrorDetail: this.formErrorDetail,
|
const props: GenericRightPaneProps = {
|
||||||
id: "publishnotebookpane",
|
container: this.container,
|
||||||
isExecuting: this.isExecuting,
|
content: this.createContent(),
|
||||||
title: "Publish to gallery",
|
formError: this.formError,
|
||||||
submitButtonText: "Publish",
|
formErrorDetail: this.formErrorDetail,
|
||||||
onClose: () => this.close(),
|
id: "publishnotebookpane",
|
||||||
onSubmit: () => this.submit()
|
isExecuting: this.isExecuting,
|
||||||
};
|
title: "Publish to gallery",
|
||||||
|
submitButtonText: "Publish",
|
||||||
return <GenericRightPaneComponent {...props} />;
|
onClose: () => this.close(),
|
||||||
}
|
onSubmit: () => this.submit()
|
||||||
|
};
|
||||||
public triggerRender(): void {
|
|
||||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
return <GenericRightPaneComponent {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
public open(name: string, author: string, content: string): void {
|
public triggerRender(): void {
|
||||||
this.name = name;
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||||
this.author = author;
|
}
|
||||||
this.content = content;
|
|
||||||
|
public open(
|
||||||
this.isOpened = true;
|
name: string,
|
||||||
this.triggerRender();
|
author: string,
|
||||||
}
|
notebookContent: string | ImmutableNotebook,
|
||||||
|
parentDomElement: HTMLElement
|
||||||
public close(): void {
|
): void {
|
||||||
this.reset();
|
this.name = name;
|
||||||
this.triggerRender();
|
this.author = author;
|
||||||
}
|
if (typeof notebookContent === "string") {
|
||||||
|
this.content = notebookContent as string;
|
||||||
public async submit(): Promise<void> {
|
} else {
|
||||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
this.content = JSON.stringify(toJS(notebookContent as ImmutableNotebook));
|
||||||
ConsoleDataType.InProgress,
|
this.notebookObject = notebookContent;
|
||||||
`Publishing ${this.name} to gallery`
|
}
|
||||||
);
|
this.parentDomElement = parentDomElement;
|
||||||
this.isExecuting = true;
|
|
||||||
this.triggerRender();
|
this.isOpened = true;
|
||||||
|
this.triggerRender();
|
||||||
try {
|
}
|
||||||
if (!this.name || !this.description || !this.author) {
|
|
||||||
throw new Error("Name, description, and author are required");
|
public close(): void {
|
||||||
}
|
this.reset();
|
||||||
|
this.triggerRender();
|
||||||
const response = await this.junoClient.publishNotebook(
|
}
|
||||||
this.name,
|
|
||||||
this.description,
|
public async submit(): Promise<void> {
|
||||||
this.tags?.split(","),
|
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||||
this.author,
|
ConsoleDataType.InProgress,
|
||||||
this.imageSrc,
|
`Publishing ${this.name} to gallery`
|
||||||
this.content
|
);
|
||||||
);
|
this.isExecuting = true;
|
||||||
if (!response.data) {
|
this.triggerRender();
|
||||||
throw new Error(`Received HTTP ${response.status} when publishing ${name} to gallery`);
|
|
||||||
}
|
try {
|
||||||
|
if (!this.name || !this.description || !this.author) {
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
throw new Error("Name, description, and author are required");
|
||||||
} catch (error) {
|
}
|
||||||
this.formError = `Failed to publish ${this.name} to gallery`;
|
|
||||||
this.formErrorDetail = `${error}`;
|
const response = await this.junoClient.publishNotebook(
|
||||||
|
this.name,
|
||||||
const message = `${this.formError}: ${this.formErrorDetail}`;
|
this.description,
|
||||||
Logger.logError(message, "PublishNotebookPaneAdapter/submit");
|
this.tags?.split(","),
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
this.author,
|
||||||
return;
|
this.imageSrc,
|
||||||
} finally {
|
this.content
|
||||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
);
|
||||||
this.isExecuting = false;
|
if (!response.data) {
|
||||||
this.triggerRender();
|
throw new Error(`Received HTTP ${response.status} when publishing ${name} to gallery`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.close();
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
||||||
}
|
} catch (error) {
|
||||||
|
this.formError = `Failed to publish ${this.name} to gallery`;
|
||||||
private createFormErrorForLargeImageSelection = (formError: string, formErrorDetail: string, area: string): void => {
|
this.formErrorDetail = `${error}`;
|
||||||
this.formError = formError;
|
|
||||||
this.formErrorDetail = formErrorDetail;
|
const message = `${this.formError}: ${this.formErrorDetail}`;
|
||||||
|
Logger.logError(message, "PublishNotebookPaneAdapter/submit");
|
||||||
const message = `${this.formError}: ${this.formErrorDetail}`;
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
Logger.logError(message, area);
|
return;
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
} finally {
|
||||||
this.triggerRender();
|
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||||
};
|
this.isExecuting = false;
|
||||||
|
this.triggerRender();
|
||||||
private clearFormError = (): void => {
|
}
|
||||||
this.formError = undefined;
|
|
||||||
this.formErrorDetail = undefined;
|
this.close();
|
||||||
this.triggerRender();
|
}
|
||||||
};
|
|
||||||
|
private createFormErrorForLargeImageSelection = (formError: string, formErrorDetail: string, area: string): void => {
|
||||||
private createContent = (): JSX.Element => {
|
this.formError = formError;
|
||||||
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
this.formErrorDetail = formErrorDetail;
|
||||||
notebookName: this.name,
|
|
||||||
notebookDescription: "",
|
const message = `${this.formError}: ${this.formErrorDetail}`;
|
||||||
notebookTags: "",
|
Logger.logError(message, area);
|
||||||
notebookAuthor: this.author,
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
notebookCreatedDate: new Date().toISOString(),
|
this.triggerRender();
|
||||||
onChangeDescription: (newValue: string) => (this.description = newValue),
|
};
|
||||||
onChangeTags: (newValue: string) => (this.tags = newValue),
|
|
||||||
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
private clearFormError = (): void => {
|
||||||
onError: this.createFormErrorForLargeImageSelection,
|
this.formError = undefined;
|
||||||
clearFormError: this.clearFormError
|
this.formErrorDetail = undefined;
|
||||||
};
|
this.triggerRender();
|
||||||
|
};
|
||||||
return <PublishNotebookPaneComponent {...publishNotebookPaneProps} />;
|
|
||||||
};
|
private createContent = (): JSX.Element => {
|
||||||
|
const publishNotebookPaneProps: PublishNotebookPaneProps = {
|
||||||
private reset = (): void => {
|
notebookName: this.name,
|
||||||
this.isOpened = false;
|
notebookDescription: "",
|
||||||
this.isExecuting = false;
|
notebookTags: "",
|
||||||
this.formError = undefined;
|
notebookAuthor: this.author,
|
||||||
this.formErrorDetail = undefined;
|
notebookCreatedDate: new Date().toISOString(),
|
||||||
this.name = undefined;
|
notebookObject: this.notebookObject,
|
||||||
this.author = undefined;
|
notebookParentDomElement: this.parentDomElement,
|
||||||
this.content = undefined;
|
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 => {
|
||||||
|
this.isOpened = false;
|
||||||
|
this.isExecuting = false;
|
||||||
|
this.formError = undefined;
|
||||||
|
this.formErrorDetail = undefined;
|
||||||
|
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",
|
notebookTags: "tag1, tag2",
|
||||||
notebookAuthor: "CosmosDB",
|
notebookAuthor: "CosmosDB",
|
||||||
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
||||||
|
notebookObject: undefined,
|
||||||
|
notebookParentDomElement: undefined,
|
||||||
onChangeDescription: undefined,
|
onChangeDescription: undefined,
|
||||||
onChangeTags: undefined,
|
onChangeTags: undefined,
|
||||||
onChangeImageSrc: undefined,
|
onChangeImageSrc: undefined,
|
||||||
|
@ -3,6 +3,9 @@ import * as React from "react";
|
|||||||
import { GalleryCardComponent } from "../Controls/NotebookGallery/Cards/GalleryCardComponent";
|
import { GalleryCardComponent } from "../Controls/NotebookGallery/Cards/GalleryCardComponent";
|
||||||
import { FileSystemUtil } from "../Notebook/FileSystemUtil";
|
import { FileSystemUtil } from "../Notebook/FileSystemUtil";
|
||||||
import "./PublishNotebookPaneComponent.less";
|
import "./PublishNotebookPaneComponent.less";
|
||||||
|
import Html2Canvas from "html2canvas";
|
||||||
|
import { ImmutableNotebook } from "@nteract/commutable/src";
|
||||||
|
import { NotebookUtil } from "../Notebook/NotebookUtil";
|
||||||
|
|
||||||
export interface PublishNotebookPaneProps {
|
export interface PublishNotebookPaneProps {
|
||||||
notebookName: string;
|
notebookName: string;
|
||||||
@ -10,6 +13,8 @@ export interface PublishNotebookPaneProps {
|
|||||||
notebookTags: string;
|
notebookTags: string;
|
||||||
notebookAuthor: string;
|
notebookAuthor: string;
|
||||||
notebookCreatedDate: string;
|
notebookCreatedDate: string;
|
||||||
|
notebookObject: ImmutableNotebook;
|
||||||
|
notebookParentDomElement: HTMLElement;
|
||||||
onChangeDescription: (newValue: string) => void;
|
onChangeDescription: (newValue: string) => void;
|
||||||
onChangeTags: (newValue: string) => void;
|
onChangeTags: (newValue: string) => void;
|
||||||
onChangeImageSrc: (newValue: string) => void;
|
onChangeImageSrc: (newValue: string) => void;
|
||||||
@ -24,9 +29,15 @@ interface PublishNotebookPaneState {
|
|||||||
imageSrc: string;
|
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> {
|
export class PublishNotebookPaneComponent extends React.Component<PublishNotebookPaneProps, PublishNotebookPaneState> {
|
||||||
private static readonly maxImageSizeInMib = 1.5;
|
private static readonly maxImageSizeInMib = 1.5;
|
||||||
private static readonly ImageTypes = ["URL", "Custom Image"];
|
|
||||||
private descriptionPara1: string;
|
private descriptionPara1: string;
|
||||||
private descriptionPara2: string;
|
private descriptionPara2: string;
|
||||||
private descriptionProps: ITextFieldProps;
|
private descriptionProps: ITextFieldProps;
|
||||||
@ -34,12 +45,13 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
private thumbnailUrlProps: ITextFieldProps;
|
private thumbnailUrlProps: ITextFieldProps;
|
||||||
private thumbnailSelectorProps: IDropdownProps;
|
private thumbnailSelectorProps: IDropdownProps;
|
||||||
private imageToBase64: (file: File, updateImageSrc: (result: string) => void) => void;
|
private imageToBase64: (file: File, updateImageSrc: (result: string) => void) => void;
|
||||||
|
private takeScreenshot: (target: HTMLElement, onError: (error: Error) => void) => void;
|
||||||
|
|
||||||
constructor(props: PublishNotebookPaneProps) {
|
constructor(props: PublishNotebookPaneProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
type: PublishNotebookPaneComponent.ImageTypes[0],
|
type: ImageTypes.Url,
|
||||||
notebookDescription: "",
|
notebookDescription: "",
|
||||||
notebookTags: "",
|
notebookTags: "",
|
||||||
imageSrc: undefined
|
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.descriptionPara1 =
|
||||||
"This notebook has your data. Please make sure you delete any sensitive data/output before publishing.";
|
"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 = {
|
this.thumbnailSelectorProps = {
|
||||||
label: "Cover image",
|
label: "Cover image",
|
||||||
defaultSelectedKey: PublishNotebookPaneComponent.ImageTypes[0],
|
defaultSelectedKey: ImageTypes.Url,
|
||||||
ariaLabel: "Cover image",
|
ariaLabel: "Cover image",
|
||||||
options: PublishNotebookPaneComponent.ImageTypes.map((value: string) => ({ text: value, key: value })),
|
options: [
|
||||||
onChange: (event, 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 });
|
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 {
|
public render(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className="publishNotebookPanelContent">
|
<div className="publishNotebookPanelContent">
|
||||||
@ -135,39 +257,8 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
|||||||
<Dropdown {...this.thumbnailSelectorProps} />
|
<Dropdown {...this.thumbnailSelectorProps} />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|
||||||
{this.state.type === PublishNotebookPaneComponent.ImageTypes[0] ? (
|
<Stack.Item>{this.renderThumbnailSelectors(this.state.type)}</Stack.Item>
|
||||||
<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>
|
<Stack.Item>
|
||||||
<Text>Preview</Text>
|
<Text>Preview</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
@ -56,6 +56,14 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
|||||||
"key": "Custom Image",
|
"key": "Custom Image",
|
||||||
"text": "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 = () => {
|
private publishToGallery = () => {
|
||||||
const notebookContent = this.notebookComponentAdapter.getContent();
|
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) {
|
private traceTelemetry(actionType: number) {
|
||||||
|
Loading…
Reference in New Issue
Block a user