diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index 5919971d5..44d106669 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -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"; diff --git a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx index c93e542dc..bbbb3d96d 100644 --- a/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx +++ b/src/Explorer/Controls/FeaturePanel/FeaturePanelComponent.tsx @@ -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", diff --git a/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap b/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap index c801e9649..885ace128 100644 --- a/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap +++ b/src/Explorer/Controls/FeaturePanel/__snapshots__/FeaturePanelComponent.test.tsx.snap @@ -163,8 +163,8 @@ exports[`Feature panel renders all flags 1`] = ` /> @@ -172,6 +172,12 @@ exports[`Feature panel renders all flags 1`] = ` className="checkboxRow" horizontalAlign="space-between" > + { isSample: false, downloads: 0, favorites: 0, - views: 0 + views: 0, + newCellId: undefined }, isFavorite: false, showDownload: true, diff --git a/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.test.tsx b/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.test.tsx index 1fb97e48e..e81f2cf42 100644 --- a/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.test.tsx +++ b/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.test.tsx @@ -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", diff --git a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx index e1c461961..d32d4186b 100644 --- a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx +++ b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx @@ -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 (
- {this.props.backNavigationText ? ( + {this.props.backNavigationText !== undefined ? ( {this.props.backNavigationText} diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index 684ded5d8..0d6fb54b9 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -206,6 +206,7 @@ export default class Explorer { // features public isGalleryPublishEnabled: ko.Computed; + public isLinkInjectionEnabled: ko.Computed; public isGitHubPaneEnabled: ko.Observable; public isPublishNotebookPaneEnabled: ko.Observable; public isHostedDataExplorerEnabled: ko.Computed; @@ -408,6 +409,9 @@ export default class Explorer { this.isGalleryPublishEnabled = ko.computed(() => this.isFeatureEnabled(Constants.Features.enableGalleryPublish) ); + this.isLinkInjectionEnabled = ko.computed(() => + this.isFeatureEnabled(Constants.Features.enableLinkInjection) + ); this.isGitHubPaneEnabled = ko.observable(false); this.isPublishNotebookPaneEnabled = ko.observable(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); } diff --git a/src/Explorer/Notebook/NotebookManager.ts b/src/Explorer/Notebook/NotebookManager.ts index eeafd2cbf..b1d606985 100644 --- a/src/Explorer/Notebook/NotebookManager.ts +++ b/src/Explorer/Notebook/NotebookManager.ts @@ -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 diff --git a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx b/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx index c6ef6a7b5..0796bb052 100644 --- a/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx +++ b/src/Explorer/Panes/PublishNotebookPaneAdapter.tsx @@ -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`); diff --git a/src/Explorer/Panes/PublishNotebookPaneComponent.tsx b/src/Explorer/Panes/PublishNotebookPaneComponent.tsx index cbb248de7..29aab162c 100644 --- a/src/Explorer/Panes/PublishNotebookPaneComponent.tsx +++ b/src/Explorer/Panes/PublishNotebookPaneComponent.tsx @@ -276,7 +276,8 @@ export class PublishNotebookPaneComponent extends React.Component { 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); diff --git a/src/Juno/JunoClient.ts b/src/Juno/JunoClient.ts index 032ddf8f3..1f5677de6 100644 --- a/src/Juno/JunoClient.ts +++ b/src/Juno/JunoClient.ts @@ -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> { + public async getNotebookInfo(id: string): Promise> { 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> { 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; diff --git a/src/NotebookViewer/NotebookViewer.tsx b/src/NotebookViewer/NotebookViewer.tsx index 31c38b62d..84d034b0e 100644 --- a/src/NotebookViewer/NotebookViewer.tsx +++ b/src/NotebookViewer/NotebookViewer.tsx @@ -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 }; diff --git a/src/Utils/GalleryUtils.test.ts b/src/Utils/GalleryUtils.test.ts index 1a95147e3..95365af40 100644 --- a/src/Utils/GalleryUtils.test.ts +++ b/src/Utils/GalleryUtils.test.ts @@ -16,7 +16,8 @@ const galleryItem: IGalleryItem = { isSample: false, downloads: 0, favorites: 0, - views: 0 + views: 0, + newCellId: undefined }; describe("GalleryUtils", () => {