Added support for notebook viewer link injection (#124)

* Added support for notebook viewer link injection

* updated tests
This commit is contained in:
Srinath Narayanan 2020-08-10 01:53:51 -07:00 committed by GitHub
parent 455a6ac81b
commit 95f1efc03f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 101 additions and 33 deletions

View File

@ -116,6 +116,7 @@ export class Features {
public static readonly enableTtl = "enablettl"; public static readonly enableTtl = "enablettl";
public static readonly enableNotebooks = "enablenotebooks"; public static readonly enableNotebooks = "enablenotebooks";
public static readonly enableGalleryPublish = "enablegallerypublish"; public static readonly enableGalleryPublish = "enablegallerypublish";
public static readonly enableLinkInjection = "enablelinkinjection";
public static readonly enableSpark = "enablespark"; public static readonly enableSpark = "enablespark";
public static readonly livyEndpoint = "livyendpoint"; public static readonly livyEndpoint = "livyendpoint";
public static readonly notebookServerUrl = "notebookserverurl"; public static readonly notebookServerUrl = "notebookserverurl";

View File

@ -49,6 +49,11 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" }, { key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
{ key: "feature.enablettl", label: "Enable TTL", value: "true" }, { key: "feature.enablettl", label: "Enable TTL", value: "true" },
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" }, { key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
{
key: "feature.enableLinkInjection",
label: "Enable Injecting Notebook Viewer Link into the first cell",
value: "true"
},
{ key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" }, { key: "feature.canexceedmaximumvalue", label: "Can exceed max value", value: "true" },
{ {
key: "feature.enablefixedcollectionwithsharedthroughput", key: "feature.enablefixedcollectionwithsharedthroughput",

View File

@ -163,8 +163,8 @@ exports[`Feature panel renders all flags 1`] = `
/> />
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.canexceedmaximumvalue" key="feature.enableLinkInjection"
label="Can exceed max value" label="Enable Injecting Notebook Viewer Link into the first cell"
onChange={[Function]} onChange={[Function]}
/> />
</Stack> </Stack>
@ -172,6 +172,12 @@ exports[`Feature panel renders all flags 1`] = `
className="checkboxRow" className="checkboxRow"
horizontalAlign="space-between" horizontalAlign="space-between"
> >
<StyledCheckboxBase
checked={false}
key="feature.canexceedmaximumvalue"
label="Can exceed max value"
onChange={[Function]}
/>
<StyledCheckboxBase <StyledCheckboxBase
checked={false} checked={false}
key="feature.enablefixedcollectionwithsharedthroughput" key="feature.enablefixedcollectionwithsharedthroughput"

View File

@ -17,7 +17,8 @@ describe("GalleryCardComponent", () => {
isSample: false, isSample: false,
downloads: 0, downloads: 0,
favorites: 0, favorites: 0,
views: 0 views: 0,
newCellId: undefined
}, },
isFavorite: false, isFavorite: false,
showDownload: true, showDownload: true,

View File

@ -17,7 +17,8 @@ describe("NotebookMetadataComponent", () => {
isSample: false, isSample: false,
downloads: 0, downloads: 0,
favorites: 0, favorites: 0,
views: 0 views: 0,
newCellId: undefined
}, },
isFavorite: false, isFavorite: false,
downloadButtonText: "Download", downloadButtonText: "Download",
@ -45,7 +46,8 @@ describe("NotebookMetadataComponent", () => {
isSample: false, isSample: false,
downloads: 0, downloads: 0,
favorites: 0, favorites: 0,
views: 0 views: 0,
newCellId: undefined
}, },
isFavorite: true, isFavorite: true,
downloadButtonText: "Download", downloadButtonText: "Download",

View File

@ -19,6 +19,7 @@ import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComp
import { NotebookMetadataComponent } from "./NotebookMetadataComponent"; import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less"; import "./NotebookViewerComponent.less";
import Explorer from "../../Explorer"; import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility"; import { SessionStorageUtility } from "../../../Shared/StorageUtility";
export interface NotebookViewerComponentProps { export interface NotebookViewerComponentProps {
@ -86,6 +87,7 @@ export class NotebookViewerComponent extends React.Component<
} }
const notebook: Notebook = await response.json(); const notebook: Notebook = await response.json();
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
this.notebookComponentBootstrapper.setContent("json", notebook); this.notebookComponentBootstrapper.setContent("json", notebook);
this.setState({ content: notebook, showProgressBar: false }); this.setState({ content: notebook, showProgressBar: false });
@ -105,10 +107,21 @@ export class NotebookViewerComponent extends React.Component<
} }
} }
private removeNotebookViewerLink = (notebook: Notebook, newCellId: string): void => {
if (!newCellId) {
return;
}
const notebookV4 = notebook as NotebookV4;
if (notebookV4 && notebookV4.cells[0].source[0].search(newCellId)) {
delete notebookV4.cells[0];
notebook = notebookV4;
}
};
public render(): JSX.Element { public render(): JSX.Element {
return ( return (
<div className="notebookViewerContainer"> <div className="notebookViewerContainer">
{this.props.backNavigationText ? ( {this.props.backNavigationText !== undefined ? (
<Link onClick={this.props.onBackClick}> <Link onClick={this.props.onBackClick}>
<Icon iconName="Back" /> {this.props.backNavigationText} <Icon iconName="Back" /> {this.props.backNavigationText}
</Link> </Link>

View File

@ -206,6 +206,7 @@ export default class Explorer {
// features // features
public isGalleryPublishEnabled: ko.Computed<boolean>; public isGalleryPublishEnabled: ko.Computed<boolean>;
public isLinkInjectionEnabled: ko.Computed<boolean>;
public isGitHubPaneEnabled: ko.Observable<boolean>; public isGitHubPaneEnabled: ko.Observable<boolean>;
public isPublishNotebookPaneEnabled: ko.Observable<boolean>; public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
public isHostedDataExplorerEnabled: ko.Computed<boolean>; public isHostedDataExplorerEnabled: ko.Computed<boolean>;
@ -408,6 +409,9 @@ export default class Explorer {
this.isGalleryPublishEnabled = ko.computed<boolean>(() => this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableGalleryPublish) this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
); );
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
);
this.isGitHubPaneEnabled = ko.observable<boolean>(false); this.isGitHubPaneEnabled = ko.observable<boolean>(false);
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false); this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
@ -2349,7 +2353,7 @@ export default class Explorer {
public publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): void { public publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): void {
if (this.notebookManager) { if (this.notebookManager) {
this.notebookManager.openPublishNotebookPane(name, content, parentDomElement); this.notebookManager.openPublishNotebookPane(name, content, parentDomElement, this.isLinkInjectionEnabled());
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter; this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
this.isPublishNotebookPaneEnabled(true); this.isPublishNotebookPaneEnabled(true);
} }

View File

@ -111,9 +111,10 @@ export default class NotebookManager {
public openPublishNotebookPane( public openPublishNotebookPane(
name: string, name: string,
content: string | ImmutableNotebook, content: string | ImmutableNotebook,
parentDomElement: HTMLElement parentDomElement: HTMLElement,
isLinkInjectionEnabled: boolean
): void { ): void {
this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement); this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement, isLinkInjectionEnabled);
} }
// Octokit's error handler uses any // Octokit's error handler uses any

View File

@ -26,6 +26,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
private imageSrc: string; private imageSrc: string;
private notebookObject: ImmutableNotebook; private notebookObject: ImmutableNotebook;
private parentDomElement: HTMLElement; private parentDomElement: HTMLElement;
private isLinkInjectionEnabled: boolean;
constructor(private container: Explorer, private junoClient: JunoClient) { constructor(private container: Explorer, private junoClient: JunoClient) {
this.parameters = ko.observable(Date.now()); this.parameters = ko.observable(Date.now());
@ -62,19 +63,21 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
name: string, name: string,
author: string, author: string,
notebookContent: string | ImmutableNotebook, notebookContent: string | ImmutableNotebook,
parentDomElement: HTMLElement parentDomElement: HTMLElement,
isLinkInjectionEnabled: boolean
): void { ): void {
this.name = name; this.name = name;
this.author = author; this.author = author;
if (typeof notebookContent === "string") { if (typeof notebookContent === "string") {
this.content = notebookContent as string; this.content = notebookContent as string;
} else { } else {
this.content = JSON.stringify(toJS(notebookContent as ImmutableNotebook)); this.content = JSON.stringify(toJS(notebookContent));
this.notebookObject = notebookContent; this.notebookObject = notebookContent;
} }
this.parentDomElement = parentDomElement; this.parentDomElement = parentDomElement;
this.isOpened = true; this.isOpened = true;
this.isLinkInjectionEnabled = isLinkInjectionEnabled;
this.triggerRender(); this.triggerRender();
} }
@ -102,7 +105,8 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
this.tags?.split(","), this.tags?.split(","),
this.author, this.author,
this.imageSrc, this.imageSrc,
this.content this.content,
this.isLinkInjectionEnabled
); );
if (!response.data) { if (!response.data) {
throw new Error(`Received HTTP ${response.status} when publishing ${name} to gallery`); throw new Error(`Received HTTP ${response.status} when publishing ${name} to gallery`);

View File

@ -276,7 +276,8 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
isSample: false, isSample: false,
downloads: 0, downloads: 0,
favorites: 0, favorites: 0,
views: 0 views: 0,
newCellId: undefined
}} }}
isFavorite={false} isFavorite={false}
showDownload={true} showDownload={true}

View File

@ -93,6 +93,7 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
"id": undefined, "id": undefined,
"isSample": false, "isSample": false,
"name": "SampleNotebook.ipynb", "name": "SampleNotebook.ipynb",
"newCellId": undefined,
"tags": Array [ "tags": Array [
"", "",
], ],

View File

@ -47,7 +47,8 @@ const sampleGalleryItems: IGalleryItem[] = [
isSample: false, isSample: false,
downloads: 0, downloads: 0,
favorites: 0, favorites: 0,
views: 0 views: 0,
newCellId: undefined
} }
]; ];
@ -185,7 +186,7 @@ describe("Gallery", () => {
json: () => undefined as any json: () => undefined as any
}); });
const response = await junoClient.getNotebook(id); const response = await junoClient.getNotebookInfo(id);
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`); expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`);
@ -353,7 +354,7 @@ describe("Gallery", () => {
json: () => undefined as any json: () => undefined as any
}); });
const response = await junoClient.publishNotebook(name, description, tags, author, thumbnailUrl, content); const response = await junoClient.publishNotebook(name, description, tags, author, thumbnailUrl, content, false);
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);

View File

@ -36,6 +36,7 @@ export interface IGalleryItem {
downloads: number; downloads: number;
favorites: number; favorites: number;
views: number; views: number;
newCellId: string;
} }
export interface IUserGallery { export interface IUserGallery {
@ -162,7 +163,7 @@ export class JunoClient {
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`); return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
} }
public async getNotebook(id: string): Promise<IJunoResponse<IGalleryItem>> { public async getNotebookInfo(id: string): Promise<IJunoResponse<IGalleryItem>> {
const response = await window.fetch(this.getNotebookInfoUrl(id)); const response = await window.fetch(this.getNotebookInfoUrl(id));
let data: IGalleryItem; let data: IGalleryItem;
@ -292,19 +293,31 @@ export class JunoClient {
tags: string[], tags: string[],
author: string, author: string,
thumbnailUrl: string, thumbnailUrl: string,
content: string content: string,
isLinkInjectionEnabled: boolean
): Promise<IJunoResponse<IGalleryItem>> { ): Promise<IJunoResponse<IGalleryItem>> {
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery`, { const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery`, {
method: "PUT", method: "PUT",
headers: JunoClient.getHeaders(), headers: JunoClient.getHeaders(),
body: JSON.stringify({
name, body: isLinkInjectionEnabled
description, ? JSON.stringify({
tags, name,
author, description,
thumbnailUrl, tags,
content: JSON.parse(content) author,
} as IPublishNotebookRequest) thumbnailUrl,
content: JSON.parse(content),
addLinkToNotebookViewer: isLinkInjectionEnabled
} as IPublishNotebookRequest)
: JSON.stringify({
name,
description,
tags,
author,
thumbnailUrl,
content: JSON.parse(content)
} as IPublishNotebookRequest)
}); });
let data: IGalleryItem; let data: IGalleryItem;

View File

@ -11,34 +11,48 @@ import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import * as GalleryUtils from "../Utils/GalleryUtils"; import * as GalleryUtils from "../Utils/GalleryUtils";
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent"; import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
import { FileSystemUtil } from "../Explorer/Notebook/FileSystemUtil"; import { FileSystemUtil } from "../Explorer/Notebook/FileSystemUtil";
import { config } from "../Config";
const onInit = async () => { const onInit = async () => {
initializeIcons(); initializeIcons();
await initializeConfiguration(); await initializeConfiguration();
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search); const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search); const notebookViewerProps = GalleryUtils.getNotebookViewerProps(window.location.search);
const backNavigationText = galleryViewerProps.selectedTab && GalleryUtils.getTabTitle(galleryViewerProps.selectedTab); let backNavigationText: string;
let onBackClick: () => void;
if (galleryViewerProps.selectedTab !== undefined) {
backNavigationText = GalleryUtils.getTabTitle(galleryViewerProps.selectedTab);
onBackClick = () => (window.location.href = `${config.hostedExplorerURL}gallery.html`);
}
const hideInputs = notebookViewerProps.hideInputs; const hideInputs = notebookViewerProps.hideInputs;
const notebookUrl = decodeURIComponent(notebookViewerProps.notebookUrl); const notebookUrl = decodeURIComponent(notebookViewerProps.notebookUrl);
render(notebookUrl, backNavigationText, hideInputs);
const galleryItemId = notebookViewerProps.galleryItemId; const galleryItemId = notebookViewerProps.galleryItemId;
let galleryItem: IGalleryItem;
if (galleryItemId) { if (galleryItemId) {
const junoClient = new JunoClient(); const junoClient = new JunoClient();
const notebook = await junoClient.getNotebook(galleryItemId); const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId);
render(notebookUrl, backNavigationText, hideInputs, notebook.data); galleryItem = galleryItemJunoResponse.data;
} }
render(notebookUrl, backNavigationText, hideInputs, galleryItem, onBackClick);
}; };
const render = (notebookUrl: string, backNavigationText: string, hideInputs: boolean, galleryItem?: IGalleryItem) => { const render = (
notebookUrl: string,
backNavigationText: string,
hideInputs: boolean,
galleryItem?: IGalleryItem,
onBackClick?: () => void
) => {
const props: NotebookViewerComponentProps = { const props: NotebookViewerComponentProps = {
junoClient: galleryItem ? new JunoClient() : undefined, junoClient: galleryItem ? new JunoClient() : undefined,
notebookUrl, notebookUrl,
galleryItem, galleryItem,
backNavigationText, backNavigationText,
hideInputs, hideInputs,
onBackClick: undefined, onBackClick: onBackClick,
onTagClick: undefined onTagClick: undefined
}; };

View File

@ -16,7 +16,8 @@ const galleryItem: IGalleryItem = {
isSample: false, isSample: false,
downloads: 0, downloads: 0,
favorites: 0, favorites: 0,
views: 0 views: 0,
newCellId: undefined
}; };
describe("GalleryUtils", () => { describe("GalleryUtils", () => {