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 enableNotebooks = "enablenotebooks";
public static readonly enableGalleryPublish = "enablegallerypublish";
public static readonly enableLinkInjection = "enablelinkinjection";
public static readonly enableSpark = "enablespark";
public static readonly livyEndpoint = "livyendpoint";
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.enablettl", label: "Enable TTL", 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.enablefixedcollectionwithsharedthroughput",

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ import { DialogComponent, DialogProps } from "../DialogReactComponent/DialogComp
import { NotebookMetadataComponent } from "./NotebookMetadataComponent";
import "./NotebookViewerComponent.less";
import Explorer from "../../Explorer";
import { NotebookV4 } from "@nteract/commutable/lib/v4";
import { SessionStorageUtility } from "../../../Shared/StorageUtility";
export interface NotebookViewerComponentProps {
@ -86,6 +87,7 @@ export class NotebookViewerComponent extends React.Component<
}
const notebook: Notebook = await response.json();
this.removeNotebookViewerLink(notebook, this.props.galleryItem?.newCellId);
this.notebookComponentBootstrapper.setContent("json", notebook);
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 {
return (
<div className="notebookViewerContainer">
{this.props.backNavigationText ? (
{this.props.backNavigationText !== undefined ? (
<Link onClick={this.props.onBackClick}>
<Icon iconName="Back" /> {this.props.backNavigationText}
</Link>

View File

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

View File

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

View File

@ -26,6 +26,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
private imageSrc: string;
private notebookObject: ImmutableNotebook;
private parentDomElement: HTMLElement;
private isLinkInjectionEnabled: boolean;
constructor(private container: Explorer, private junoClient: JunoClient) {
this.parameters = ko.observable(Date.now());
@ -62,19 +63,21 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
name: string,
author: string,
notebookContent: string | ImmutableNotebook,
parentDomElement: HTMLElement
parentDomElement: HTMLElement,
isLinkInjectionEnabled: boolean
): void {
this.name = name;
this.author = author;
if (typeof notebookContent === "string") {
this.content = notebookContent as string;
} else {
this.content = JSON.stringify(toJS(notebookContent as ImmutableNotebook));
this.content = JSON.stringify(toJS(notebookContent));
this.notebookObject = notebookContent;
}
this.parentDomElement = parentDomElement;
this.isOpened = true;
this.isLinkInjectionEnabled = isLinkInjectionEnabled;
this.triggerRender();
}
@ -102,7 +105,8 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
this.tags?.split(","),
this.author,
this.imageSrc,
this.content
this.content,
this.isLinkInjectionEnabled
);
if (!response.data) {
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,
downloads: 0,
favorites: 0,
views: 0
views: 0,
newCellId: undefined
}}
isFavorite={false}
showDownload={true}

View File

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

View File

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

View File

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

View File

@ -11,34 +11,48 @@ import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
import * as GalleryUtils from "../Utils/GalleryUtils";
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
import { FileSystemUtil } from "../Explorer/Notebook/FileSystemUtil";
import { config } from "../Config";
const onInit = async () => {
initializeIcons();
await initializeConfiguration();
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(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 notebookUrl = decodeURIComponent(notebookViewerProps.notebookUrl);
render(notebookUrl, backNavigationText, hideInputs);
const galleryItemId = notebookViewerProps.galleryItemId;
let galleryItem: IGalleryItem;
if (galleryItemId) {
const junoClient = new JunoClient();
const notebook = await junoClient.getNotebook(galleryItemId);
render(notebookUrl, backNavigationText, hideInputs, notebook.data);
const galleryItemJunoResponse = await junoClient.getNotebookInfo(galleryItemId);
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 = {
junoClient: galleryItem ? new JunoClient() : undefined,
notebookUrl,
galleryItem,
backNavigationText,
hideInputs,
onBackClick: undefined,
onBackClick: onBackClick,
onTagClick: undefined
};

View File

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