Copy To functionality for notebooks (#141)
* Add Copy To functionality for notebooks * Fix formatting * Fix linting errors * Fixes * Fix build failure * Rebase and address feedback * Increase test coverage
This commit is contained in:
parent
7a3e54d43e
commit
5886db81e9
|
@ -385,12 +385,11 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||||
|
|
||||||
private isGalleryItemPresent(searchText: string, item: IGalleryItem): boolean {
|
private isGalleryItemPresent(searchText: string, item: IGalleryItem): boolean {
|
||||||
const toSearch = searchText.trim().toUpperCase();
|
const toSearch = searchText.trim().toUpperCase();
|
||||||
const searchData: string[] = [
|
const searchData: string[] = [item.author.toUpperCase(), item.description.toUpperCase(), item.name.toUpperCase()];
|
||||||
item.author.toUpperCase(),
|
|
||||||
item.description.toUpperCase(),
|
if (item.tags) {
|
||||||
item.name.toUpperCase(),
|
searchData.push(...item.tags.map(tag => tag.toUpperCase()));
|
||||||
...item.tags?.map(tag => tag.toUpperCase())
|
}
|
||||||
];
|
|
||||||
|
|
||||||
for (const data of searchData) {
|
for (const data of searchData) {
|
||||||
if (data?.indexOf(toSearch) !== -1) {
|
if (data?.indexOf(toSearch) !== -1) {
|
||||||
|
|
|
@ -203,6 +203,7 @@ export default class Explorer {
|
||||||
public setupNotebooksPane: SetupNotebooksPane;
|
public setupNotebooksPane: SetupNotebooksPane;
|
||||||
public gitHubReposPane: ContextualPaneBase;
|
public gitHubReposPane: ContextualPaneBase;
|
||||||
public publishNotebookPaneAdapter: ReactAdapter;
|
public publishNotebookPaneAdapter: ReactAdapter;
|
||||||
|
public copyNotebookPaneAdapter: ReactAdapter;
|
||||||
|
|
||||||
// features
|
// features
|
||||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||||
|
@ -210,6 +211,7 @@ export default class Explorer {
|
||||||
public isLinkInjectionEnabled: 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 isCopyNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
public isHostedDataExplorerEnabled: ko.Computed<boolean>;
|
||||||
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
public isRightPanelV2Enabled: ko.Computed<boolean>;
|
||||||
public canExceedMaximumValue: ko.Computed<boolean>;
|
public canExceedMaximumValue: ko.Computed<boolean>;
|
||||||
|
@ -418,6 +420,7 @@ export default class Explorer {
|
||||||
);
|
);
|
||||||
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
this.isGitHubPaneEnabled = ko.observable<boolean>(false);
|
||||||
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
this.isPublishNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
|
this.isCopyNotebookPaneEnabled = ko.observable<boolean>(false);
|
||||||
|
|
||||||
this.canExceedMaximumValue = ko.computed<boolean>(() =>
|
this.canExceedMaximumValue = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
this.isFeatureEnabled(Constants.Features.canExceedMaximumValue)
|
||||||
|
@ -2305,7 +2308,7 @@ export default class Explorer {
|
||||||
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
|
return _.find(offers, (offer: DataModels.Offer) => offer.resource === resourceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
public uploadFile(name: string, content: string, parent: NotebookContentItem): Promise<NotebookContentItem> {
|
||||||
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
if (!this.isNotebookEnabled() || !this.notebookManager?.notebookContentClient) {
|
||||||
const error = "Attempt to upload notebook, but notebook is not enabled";
|
const error = "Attempt to upload notebook, but notebook is not enabled";
|
||||||
Logger.logError(error, "Explorer/uploadFile");
|
Logger.logError(error, "Explorer/uploadFile");
|
||||||
|
@ -2374,6 +2377,14 @@ export default class Explorer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public copyNotebook(name: string, content: string): void {
|
||||||
|
if (this.notebookManager) {
|
||||||
|
this.notebookManager.openCopyNotebookPane(name, content);
|
||||||
|
this.copyNotebookPaneAdapter = this.notebookManager.copyNotebookPaneAdapter;
|
||||||
|
this.isCopyNotebookPaneEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public showOkModalDialog(title: string, msg: string): void {
|
public showOkModalDialog(title: string, msg: string): void {
|
||||||
this._dialogProps({
|
this._dialogProps({
|
||||||
isModal: true,
|
isModal: true,
|
||||||
|
@ -2730,6 +2741,7 @@ export default class Explorer {
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.resourceTree.initialize();
|
await this.resourceTree.initialize();
|
||||||
|
this.notebookManager?.refreshPinnedRepos();
|
||||||
if (this.notebookToImport) {
|
if (this.notebookToImport) {
|
||||||
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
this.importAndOpenContent(this.notebookToImport.name, this.notebookToImport.content);
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ export class NotebookContentClient {
|
||||||
throw new Error(`Parent must be a directory: ${parent}`);
|
throw new Error(`Parent must be a directory: ${parent}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filepath = `${parent.path}/${name}`;
|
const filepath = NotebookUtil.getFilePath(parent.path, name);
|
||||||
if (await this.checkIfFilepathExists(filepath)) {
|
if (await this.checkIfFilepathExists(filepath)) {
|
||||||
throw new Error(`File already exists: ${filepath}`);
|
throw new Error(`File already exists: ${filepath}`);
|
||||||
}
|
}
|
||||||
|
@ -116,12 +116,7 @@ export class NotebookContentClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkIfFilepathExists(filepath: string): Promise<boolean> {
|
private async checkIfFilepathExists(filepath: string): Promise<boolean> {
|
||||||
const basename = filepath.split("/").pop();
|
const parentDirPath = NotebookUtil.getParentPath(filepath);
|
||||||
let parentDirPath = filepath
|
|
||||||
.split(basename)
|
|
||||||
.shift()
|
|
||||||
.replace(/\/$/, ""); // no trailling slash
|
|
||||||
|
|
||||||
const items = await this.fetchNotebookFiles(parentDirPath);
|
const items = await this.fetchNotebookFiles(parentDirPath);
|
||||||
return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
|
return items.some(value => FileSystemUtil.isPathEqual(value.path, filepath));
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { getFullName } from "../../Utils/UserUtils";
|
||||||
import { ImmutableNotebook } from "@nteract/commutable";
|
import { ImmutableNotebook } from "@nteract/commutable";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
|
import { ContextualPaneBase } from "../Panes/ContextualPaneBase";
|
||||||
|
import { CopyNotebookPaneAdapter } from "../Panes/CopyNotebookPane";
|
||||||
|
|
||||||
export interface NotebookManagerOptions {
|
export interface NotebookManagerOptions {
|
||||||
container: Explorer;
|
container: Explorer;
|
||||||
|
@ -49,6 +50,7 @@ export default class NotebookManager {
|
||||||
|
|
||||||
public gitHubReposPane: ContextualPaneBase;
|
public gitHubReposPane: ContextualPaneBase;
|
||||||
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
|
public publishNotebookPaneAdapter: PublishNotebookPaneAdapter;
|
||||||
|
public copyNotebookPaneAdapter: CopyNotebookPaneAdapter;
|
||||||
|
|
||||||
public initialize(params: NotebookManagerOptions): void {
|
public initialize(params: NotebookManagerOptions): void {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
|
@ -90,6 +92,12 @@ export default class NotebookManager {
|
||||||
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
|
this.publishNotebookPaneAdapter = new PublishNotebookPaneAdapter(this.params.container, this.junoClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.copyNotebookPaneAdapter = new CopyNotebookPaneAdapter(
|
||||||
|
this.params.container,
|
||||||
|
this.junoClient,
|
||||||
|
this.gitHubOAuthService
|
||||||
|
);
|
||||||
|
|
||||||
this.gitHubOAuthService.getTokenObservable().subscribe(token => {
|
this.gitHubOAuthService.getTokenObservable().subscribe(token => {
|
||||||
this.gitHubClient.setToken(token?.access_token);
|
this.gitHubClient.setToken(token?.access_token);
|
||||||
|
|
||||||
|
@ -108,6 +116,10 @@ export default class NotebookManager {
|
||||||
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public refreshPinnedRepos(): void {
|
||||||
|
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||||
|
}
|
||||||
|
|
||||||
public async openPublishNotebookPane(
|
public async openPublishNotebookPane(
|
||||||
name: string,
|
name: string,
|
||||||
content: string | ImmutableNotebook,
|
content: string | ImmutableNotebook,
|
||||||
|
@ -125,6 +137,10 @@ export default class NotebookManager {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public openCopyNotebookPane(name: string, content: string): void {
|
||||||
|
this.copyNotebookPaneAdapter.open(name, content);
|
||||||
|
}
|
||||||
|
|
||||||
// Octokit's error handler uses any
|
// Octokit's error handler uses any
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
private onGitHubClientError = (error: any): void => {
|
private onGitHubClientError = (error: any): void => {
|
||||||
|
|
|
@ -13,8 +13,10 @@ import { List, Map } from "immutable";
|
||||||
|
|
||||||
const fileName = "file";
|
const fileName = "file";
|
||||||
const notebookName = "file.ipynb";
|
const notebookName = "file.ipynb";
|
||||||
const filePath = `folder/${fileName}`;
|
const folderPath = "folder";
|
||||||
const notebookPath = `folder/${notebookName}`;
|
const filePath = `${folderPath}/${fileName}`;
|
||||||
|
const notebookPath = `${folderPath}/${notebookName}`;
|
||||||
|
const gitHubFolderUri = GitHubUtils.toContentUri("owner", "repo", "branch", folderPath);
|
||||||
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({
|
const notebookRecord = makeNotebookRecord({
|
||||||
|
@ -80,6 +82,26 @@ describe("NotebookUtil", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getFilePath", () => {
|
||||||
|
it("works for jupyter file paths", () => {
|
||||||
|
expect(NotebookUtil.getFilePath(folderPath, fileName)).toEqual(filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works for github file uris", () => {
|
||||||
|
expect(NotebookUtil.getFilePath(gitHubFolderUri, fileName)).toEqual(gitHubFileUri);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getParentPath", () => {
|
||||||
|
it("works for jupyter file paths", () => {
|
||||||
|
expect(NotebookUtil.getParentPath(filePath)).toEqual(folderPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works for github file uris", () => {
|
||||||
|
expect(NotebookUtil.getParentPath(gitHubFileUri)).toEqual(gitHubFolderUri);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("getName", () => {
|
describe("getName", () => {
|
||||||
it("works for jupyter file paths", () => {
|
it("works for jupyter file paths", () => {
|
||||||
expect(NotebookUtil.getName(filePath)).toEqual(fileName);
|
expect(NotebookUtil.getName(filePath)).toEqual(fileName);
|
||||||
|
|
|
@ -70,6 +70,46 @@ export class NotebookUtil {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static getFilePath(path: string, fileName: string): string {
|
||||||
|
const contentInfo = GitHubUtils.fromContentUri(path);
|
||||||
|
if (contentInfo) {
|
||||||
|
let path = fileName;
|
||||||
|
if (contentInfo.path) {
|
||||||
|
path = `${contentInfo.path}/${path}`;
|
||||||
|
}
|
||||||
|
return GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${path}/${fileName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getParentPath(filepath: string): undefined | string {
|
||||||
|
const basename = NotebookUtil.getName(filepath);
|
||||||
|
if (basename) {
|
||||||
|
const contentInfo = GitHubUtils.fromContentUri(filepath);
|
||||||
|
if (contentInfo) {
|
||||||
|
const parentPath = contentInfo.path.split(basename).shift();
|
||||||
|
if (parentPath === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GitHubUtils.toContentUri(
|
||||||
|
contentInfo.owner,
|
||||||
|
contentInfo.repo,
|
||||||
|
contentInfo.branch,
|
||||||
|
parentPath.replace(/\/$/, "") // no trailling slash
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentPath = filepath.split(basename).shift();
|
||||||
|
if (parentPath) {
|
||||||
|
return parentPath.replace(/\/$/, ""); // no trailling slash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
public static getName(path: string): undefined | string {
|
public static getName(path: string): undefined | string {
|
||||||
let relativePath: string = path;
|
let relativePath: string = path;
|
||||||
const contentInfo = GitHubUtils.fromContentUri(path);
|
const contentInfo = GitHubUtils.fromContentUri(path);
|
||||||
|
|
|
@ -0,0 +1,283 @@
|
||||||
|
import ko from "knockout";
|
||||||
|
import * as React from "react";
|
||||||
|
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||||
|
import * as Logger from "../../Common/Logger";
|
||||||
|
import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient";
|
||||||
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||||
|
import Explorer from "../Explorer";
|
||||||
|
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
Label,
|
||||||
|
Text,
|
||||||
|
Dropdown,
|
||||||
|
IDropdownProps,
|
||||||
|
IDropdownOption,
|
||||||
|
SelectableOptionMenuItemType,
|
||||||
|
IRenderFunction,
|
||||||
|
ISelectableOption
|
||||||
|
} from "office-ui-fabric-react";
|
||||||
|
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||||
|
import { HttpStatusCodes } from "../../Common/Constants";
|
||||||
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
import { NotebookContentItemType, NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
|
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||||
|
|
||||||
|
interface Location {
|
||||||
|
type: "MyNotebooks" | "GitHub";
|
||||||
|
|
||||||
|
// GitHub
|
||||||
|
owner?: string;
|
||||||
|
repo?: string;
|
||||||
|
branch?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CopyNotebookPaneAdapter implements ReactAdapter {
|
||||||
|
private static readonly BranchNameWhiteSpace = " ";
|
||||||
|
|
||||||
|
parameters: ko.Observable<number>;
|
||||||
|
private isOpened: boolean;
|
||||||
|
private isExecuting: boolean;
|
||||||
|
private formError: string;
|
||||||
|
private formErrorDetail: string;
|
||||||
|
private name: string;
|
||||||
|
private content: string;
|
||||||
|
private pinnedRepos: IPinnedRepo[];
|
||||||
|
private selectedLocation: Location;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private container: Explorer,
|
||||||
|
private junoClient: JunoClient,
|
||||||
|
private gitHubOAuthService: GitHubOAuthService
|
||||||
|
) {
|
||||||
|
this.parameters = ko.observable(Date.now());
|
||||||
|
this.reset();
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderComponent(): JSX.Element {
|
||||||
|
if (!this.isOpened) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props: GenericRightPaneProps = {
|
||||||
|
container: this.container,
|
||||||
|
content: this.createContent(),
|
||||||
|
formError: this.formError,
|
||||||
|
formErrorDetail: this.formErrorDetail,
|
||||||
|
id: "copynotebookpane",
|
||||||
|
isExecuting: this.isExecuting,
|
||||||
|
title: "Copy notebook",
|
||||||
|
submitButtonText: "OK",
|
||||||
|
onClose: () => this.close(),
|
||||||
|
onSubmit: () => this.submit()
|
||||||
|
};
|
||||||
|
|
||||||
|
return <GenericRightPaneComponent {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
public triggerRender(): void {
|
||||||
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async open(name: string, content: string): Promise<void> {
|
||||||
|
this.name = name;
|
||||||
|
this.content = content;
|
||||||
|
|
||||||
|
this.isOpened = true;
|
||||||
|
this.triggerRender();
|
||||||
|
|
||||||
|
if (this.gitHubOAuthService.isLoggedIn()) {
|
||||||
|
const response = await this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||||
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
|
const message = `Received HTTP ${response.status} when fetching pinned repos`;
|
||||||
|
Logger.logError(message, "CopyNotebookPaneAdapter/submit");
|
||||||
|
NotificationConsoleUtils.logConsoleError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data?.length > 0) {
|
||||||
|
this.pinnedRepos = response.data;
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
this.reset();
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async submit(): Promise<void> {
|
||||||
|
let destination: string = this.selectedLocation?.type;
|
||||||
|
let clearMessage: () => void;
|
||||||
|
this.isExecuting = true;
|
||||||
|
this.triggerRender();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.selectedLocation) {
|
||||||
|
throw new Error(`No location selected`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedLocation.type === "GitHub") {
|
||||||
|
destination = `${destination} - ${GitHubUtils.toRepoFullName(
|
||||||
|
this.selectedLocation.owner,
|
||||||
|
this.selectedLocation.repo
|
||||||
|
)} - ${this.selectedLocation.branch}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${this.name} to ${destination}`);
|
||||||
|
|
||||||
|
const notebookContentItem = await this.copyNotebook(this.selectedLocation);
|
||||||
|
if (!notebookContentItem) {
|
||||||
|
throw new Error(`Failed to upload ${this.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${this.name} to ${destination}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.formError = `Failed to copy ${this.name} to ${destination}`;
|
||||||
|
this.formErrorDetail = `${error}`;
|
||||||
|
|
||||||
|
const message = `${this.formError}: ${this.formErrorDetail}`;
|
||||||
|
Logger.logError(message, "CopyNotebookPaneAdapter/submit");
|
||||||
|
NotificationConsoleUtils.logConsoleError(message);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
clearMessage && clearMessage();
|
||||||
|
this.isExecuting = false;
|
||||||
|
this.triggerRender();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
|
||||||
|
let parent: NotebookContentItem;
|
||||||
|
switch (location.type) {
|
||||||
|
case "MyNotebooks":
|
||||||
|
parent = {
|
||||||
|
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||||
|
path: this.container.getNotebookBasePath(),
|
||||||
|
type: NotebookContentItemType.Directory
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "GitHub":
|
||||||
|
parent = {
|
||||||
|
name: ResourceTreeAdapter.GitHubReposTitle,
|
||||||
|
path: GitHubUtils.toContentUri(
|
||||||
|
this.selectedLocation.owner,
|
||||||
|
this.selectedLocation.repo,
|
||||||
|
this.selectedLocation.branch,
|
||||||
|
""
|
||||||
|
),
|
||||||
|
type: NotebookContentItemType.Directory
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported location type ${location.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.container.uploadFile(this.name, this.content, parent);
|
||||||
|
};
|
||||||
|
|
||||||
|
private createContent = (): JSX.Element => {
|
||||||
|
const dropDownProps: IDropdownProps = {
|
||||||
|
label: "Location",
|
||||||
|
ariaLabel: "Location",
|
||||||
|
placeholder: "Select an option",
|
||||||
|
onRenderTitle: this.onRenderDropDownTitle,
|
||||||
|
onRenderOption: this.onRenderDropDownOption,
|
||||||
|
options: this.getDropDownOptions(),
|
||||||
|
onChange: this.onDropDownChange
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="paneMainContent">
|
||||||
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
|
<Stack.Item>
|
||||||
|
<Label htmlFor="notebookName">Name</Label>
|
||||||
|
<Text id="notebookName">{this.name}</Text>
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Dropdown {...dropDownProps} />
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => {
|
||||||
|
return <span>{options.length && options[0].title}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRenderDropDownOption: IRenderFunction<ISelectableOption> = (option: ISelectableOption): JSX.Element => {
|
||||||
|
return <span style={{ whiteSpace: "pre-wrap" }}>{option.text}</span>;
|
||||||
|
};
|
||||||
|
|
||||||
|
private getDropDownOptions = (): IDropdownOption[] => {
|
||||||
|
const options: IDropdownOption[] = [];
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
key: "MyNotebooks-Item",
|
||||||
|
text: ResourceTreeAdapter.MyNotebooksTitle,
|
||||||
|
title: ResourceTreeAdapter.MyNotebooksTitle,
|
||||||
|
data: {
|
||||||
|
type: "MyNotebooks"
|
||||||
|
} as Location
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.pinnedRepos && this.pinnedRepos.length > 0) {
|
||||||
|
options.push({
|
||||||
|
key: "GitHub-Header-Divider",
|
||||||
|
text: undefined,
|
||||||
|
itemType: SelectableOptionMenuItemType.Divider
|
||||||
|
});
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
key: "GitHub-Header",
|
||||||
|
text: ResourceTreeAdapter.GitHubReposTitle,
|
||||||
|
itemType: SelectableOptionMenuItemType.Header
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pinnedRepos.forEach(pinnedRepo => {
|
||||||
|
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
||||||
|
options.push({
|
||||||
|
key: `GitHub-Repo-${repoFullName}`,
|
||||||
|
text: repoFullName,
|
||||||
|
disabled: true
|
||||||
|
});
|
||||||
|
|
||||||
|
pinnedRepo.branches.forEach(branch =>
|
||||||
|
options.push({
|
||||||
|
key: `GitHub-Repo-${repoFullName}-${branch.name}`,
|
||||||
|
text: `${CopyNotebookPaneAdapter.BranchNameWhiteSpace}${branch.name}`,
|
||||||
|
title: `${repoFullName} - ${branch.name}`,
|
||||||
|
data: {
|
||||||
|
type: "GitHub",
|
||||||
|
owner: pinnedRepo.owner,
|
||||||
|
repo: pinnedRepo.name,
|
||||||
|
branch: branch.name
|
||||||
|
} as Location
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
};
|
||||||
|
|
||||||
|
private onDropDownChange = (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
|
||||||
|
this.selectedLocation = option?.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
private reset = (): void => {
|
||||||
|
this.isOpened = false;
|
||||||
|
this.isExecuting = false;
|
||||||
|
this.formError = undefined;
|
||||||
|
this.formErrorDetail = undefined;
|
||||||
|
this.name = undefined;
|
||||||
|
this.content = undefined;
|
||||||
|
this.pinnedRepos = undefined;
|
||||||
|
this.selectedLocation = undefined;
|
||||||
|
};
|
||||||
|
}
|
|
@ -175,6 +175,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
notebookCreatedDate: new Date().toISOString(),
|
notebookCreatedDate: new Date().toISOString(),
|
||||||
notebookObject: this.notebookObject,
|
notebookObject: this.notebookObject,
|
||||||
notebookParentDomElement: this.parentDomElement,
|
notebookParentDomElement: this.parentDomElement,
|
||||||
|
onChangeName: (newValue: string) => (this.name = newValue),
|
||||||
onChangeDescription: (newValue: string) => (this.description = newValue),
|
onChangeDescription: (newValue: string) => (this.description = newValue),
|
||||||
onChangeTags: (newValue: string) => (this.tags = newValue),
|
onChangeTags: (newValue: string) => (this.tags = newValue),
|
||||||
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
|
||||||
|
|
|
@ -12,6 +12,7 @@ describe("PublishNotebookPaneComponent", () => {
|
||||||
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
notebookCreatedDate: "2020-07-17T00:00:00Z",
|
||||||
notebookObject: undefined,
|
notebookObject: undefined,
|
||||||
notebookParentDomElement: undefined,
|
notebookParentDomElement: undefined,
|
||||||
|
onChangeName: undefined,
|
||||||
onChangeDescription: undefined,
|
onChangeDescription: undefined,
|
||||||
onChangeTags: undefined,
|
onChangeTags: undefined,
|
||||||
onChangeImageSrc: undefined,
|
onChangeImageSrc: undefined,
|
||||||
|
|
|
@ -15,6 +15,7 @@ export interface PublishNotebookPaneProps {
|
||||||
notebookCreatedDate: string;
|
notebookCreatedDate: string;
|
||||||
notebookObject: ImmutableNotebook;
|
notebookObject: ImmutableNotebook;
|
||||||
notebookParentDomElement: HTMLElement;
|
notebookParentDomElement: HTMLElement;
|
||||||
|
onChangeName: (newValue: string) => void;
|
||||||
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,6 +25,7 @@ export interface PublishNotebookPaneProps {
|
||||||
|
|
||||||
interface PublishNotebookPaneState {
|
interface PublishNotebookPaneState {
|
||||||
type: string;
|
type: string;
|
||||||
|
notebookName: string;
|
||||||
notebookDescription: string;
|
notebookDescription: string;
|
||||||
notebookTags: string;
|
notebookTags: string;
|
||||||
imageSrc: string;
|
imageSrc: string;
|
||||||
|
@ -40,6 +42,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
||||||
private static readonly maxImageSizeInMib = 1.5;
|
private static readonly maxImageSizeInMib = 1.5;
|
||||||
private descriptionPara1: string;
|
private descriptionPara1: string;
|
||||||
private descriptionPara2: string;
|
private descriptionPara2: string;
|
||||||
|
private nameProps: ITextFieldProps;
|
||||||
private descriptionProps: ITextFieldProps;
|
private descriptionProps: ITextFieldProps;
|
||||||
private tagsProps: ITextFieldProps;
|
private tagsProps: ITextFieldProps;
|
||||||
private thumbnailUrlProps: ITextFieldProps;
|
private thumbnailUrlProps: ITextFieldProps;
|
||||||
|
@ -52,6 +55,7 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
type: ImageTypes.Url,
|
type: ImageTypes.Url,
|
||||||
|
notebookName: props.notebookName,
|
||||||
notebookDescription: "",
|
notebookDescription: "",
|
||||||
notebookTags: "",
|
notebookTags: "",
|
||||||
imageSrc: undefined
|
imageSrc: undefined
|
||||||
|
@ -165,6 +169,17 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.nameProps = {
|
||||||
|
label: "Name",
|
||||||
|
ariaLabel: "Name",
|
||||||
|
defaultValue: this.props.notebookName,
|
||||||
|
required: true,
|
||||||
|
onChange: (event, newValue) => {
|
||||||
|
this.props.onChangeName(newValue);
|
||||||
|
this.setState({ notebookName: newValue });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.descriptionProps = {
|
this.descriptionProps = {
|
||||||
label: "Description",
|
label: "Description",
|
||||||
ariaLabel: "Description",
|
ariaLabel: "Description",
|
||||||
|
@ -245,6 +260,10 @@ export class PublishNotebookPaneComponent extends React.Component<PublishNoteboo
|
||||||
<Text>{this.descriptionPara2}</Text>
|
<Text>{this.descriptionPara2}</Text>
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|
||||||
|
<Stack.Item>
|
||||||
|
<TextField {...this.nameProps} />
|
||||||
|
</Stack.Item>
|
||||||
|
|
||||||
<Stack.Item>
|
<Stack.Item>
|
||||||
<TextField {...this.descriptionProps} />
|
<TextField {...this.descriptionProps} />
|
||||||
</Stack.Item>
|
</Stack.Item>
|
||||||
|
|
|
@ -22,6 +22,15 @@ exports[`PublishNotebookPaneComponent renders 1`] = `
|
||||||
Would you like to publish and share "SampleNotebook" to the gallery?
|
Would you like to publish and share "SampleNotebook" to the gallery?
|
||||||
</Text>
|
</Text>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<StyledTextFieldBase
|
||||||
|
ariaLabel="Name"
|
||||||
|
defaultValue="SampleNotebook.ipynb"
|
||||||
|
label="Name"
|
||||||
|
onChange={[Function]}
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
<StackItem>
|
<StackItem>
|
||||||
<StyledTextFieldBase
|
<StyledTextFieldBase
|
||||||
ariaLabel="Description"
|
ariaLabel="Description"
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { configContext } from "../../ConfigContext";
|
||||||
import Explorer from "../Explorer";
|
import Explorer from "../Explorer";
|
||||||
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem } from "../Notebook/NotebookContentItem";
|
||||||
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
import { CommandButtonComponentProps } from "../Controls/CommandButton/CommandButtonComponent";
|
||||||
|
import { toJS, stringifyNotebook } from "@nteract/commutable";
|
||||||
|
|
||||||
export interface NotebookTabOptions extends ViewModels.TabOptions {
|
export interface NotebookTabOptions extends ViewModels.TabOptions {
|
||||||
account: DataModels.DatabaseAccount;
|
account: DataModels.DatabaseAccount;
|
||||||
|
@ -122,6 +123,7 @@ export default class NotebookTabV2 extends TabsBase {
|
||||||
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
const availableKernels = NotebookTabV2.clientManager.getAvailableKernelSpecs();
|
||||||
|
|
||||||
const saveLabel = "Save";
|
const saveLabel = "Save";
|
||||||
|
const copyToLabel = "Copy to ...";
|
||||||
const publishLabel = "Publish to gallery";
|
const publishLabel = "Publish to gallery";
|
||||||
const workspaceLabel = "No Workspace";
|
const workspaceLabel = "No Workspace";
|
||||||
const kernelLabel = "No Kernel";
|
const kernelLabel = "No Kernel";
|
||||||
|
@ -164,6 +166,14 @@ export default class NotebookTabV2 extends TabsBase {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
ariaLabel: saveLabel
|
ariaLabel: saveLabel
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
iconName: "Copy",
|
||||||
|
onCommandClick: () => this.copyNotebook(),
|
||||||
|
commandButtonLabel: copyToLabel,
|
||||||
|
hasPopup: false,
|
||||||
|
disabled: false,
|
||||||
|
ariaLabel: copyToLabel
|
||||||
|
},
|
||||||
{
|
{
|
||||||
iconName: "PublishContent",
|
iconName: "PublishContent",
|
||||||
onCommandClick: async () => await this.publishToGallery(),
|
onCommandClick: async () => await this.publishToGallery(),
|
||||||
|
@ -465,6 +475,18 @@ export default class NotebookTabV2 extends TabsBase {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private copyNotebook = () => {
|
||||||
|
const notebookContent = this.notebookComponentAdapter.getContent();
|
||||||
|
let content: string;
|
||||||
|
if (typeof notebookContent.content === "string") {
|
||||||
|
content = notebookContent.content;
|
||||||
|
} else {
|
||||||
|
content = stringifyNotebook(toJS(notebookContent.content));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.container.copyNotebook(notebookContent.name, content);
|
||||||
|
};
|
||||||
|
|
||||||
private traceTelemetry(actionType: number) {
|
private traceTelemetry(actionType: number) {
|
||||||
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
TelemetryProcessor.trace(actionType, ActionModifiers.Mark, {
|
||||||
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||||
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
import { ResourceTreeContextMenuButtonFactory } from "../ContextMenuButtonFactory";
|
||||||
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
|
||||||
|
import CopyIcon from "../../../images/notebook/Notebook-copy.svg";
|
||||||
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
import CosmosDBIcon from "../../../images/Azure-Cosmos-DB.svg";
|
||||||
import CollectionIcon from "../../../images/tree-collection.svg";
|
import CollectionIcon from "../../../images/tree-collection.svg";
|
||||||
import DeleteIcon from "../../../images/delete.svg";
|
import DeleteIcon from "../../../images/delete.svg";
|
||||||
|
@ -33,6 +34,9 @@ import TabsBase from "../Tabs/TabsBase";
|
||||||
import { userContext } from "../../UserContext";
|
import { userContext } from "../../UserContext";
|
||||||
|
|
||||||
export class ResourceTreeAdapter implements ReactAdapter {
|
export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
|
public static readonly MyNotebooksTitle = "My Notebooks";
|
||||||
|
public static readonly GitHubReposTitle = "GitHub repos";
|
||||||
|
|
||||||
private static readonly DataTitle = "DATA";
|
private static readonly DataTitle = "DATA";
|
||||||
private static readonly NotebooksTitle = "NOTEBOOKS";
|
private static readonly NotebooksTitle = "NOTEBOOKS";
|
||||||
private static readonly PseudoDirPath = "PsuedoDir";
|
private static readonly PseudoDirPath = "PsuedoDir";
|
||||||
|
@ -104,7 +108,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.myNotebooksContentRoot = {
|
this.myNotebooksContentRoot = {
|
||||||
name: "My Notebooks",
|
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||||
path: this.container.getNotebookBasePath(),
|
path: this.container.getNotebookBasePath(),
|
||||||
type: NotebookContentItemType.Directory
|
type: NotebookContentItemType.Directory
|
||||||
};
|
};
|
||||||
|
@ -118,7 +122,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
|
|
||||||
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
if (this.container.notebookManager?.gitHubOAuthService.isLoggedIn()) {
|
||||||
this.gitHubNotebooksContentRoot = {
|
this.gitHubNotebooksContentRoot = {
|
||||||
name: "GitHub repos",
|
name: ResourceTreeAdapter.GitHubReposTitle,
|
||||||
path: ResourceTreeAdapter.PseudoDirPath,
|
path: ResourceTreeAdapter.PseudoDirPath,
|
||||||
type: NotebookContentItemType.Directory
|
type: NotebookContentItemType.Directory
|
||||||
};
|
};
|
||||||
|
@ -563,6 +567,11 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Copy to ...",
|
||||||
|
iconSrc: CopyIcon,
|
||||||
|
onClick: () => this.copyNotebook(item)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Download",
|
label: "Download",
|
||||||
iconSrc: NotebookIcon,
|
iconSrc: NotebookIcon,
|
||||||
|
@ -574,6 +583,13 @@ export class ResourceTreeAdapter implements ReactAdapter {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private copyNotebook = async (item: NotebookContentItem) => {
|
||||||
|
const content = await this.container.readFile(item);
|
||||||
|
if (content) {
|
||||||
|
this.container.copyNotebook(item.name, content);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
|
private createDirectoryContextMenu(item: NotebookContentItem): TreeNodeMenuItem[] {
|
||||||
let items: TreeNodeMenuItem[] = [
|
let items: TreeNodeMenuItem[] = [
|
||||||
{
|
{
|
||||||
|
|
|
@ -317,6 +317,13 @@ export class GitHubClient {
|
||||||
objectExpression: `refs/heads/${branch}:${path || ""}`
|
objectExpression: `refs/heads/${branch}:${path || ""}`
|
||||||
} as ContentsQueryParams)) as ContentsQueryResponse;
|
} as ContentsQueryParams)) as ContentsQueryResponse;
|
||||||
|
|
||||||
|
if (!response.repository.object) {
|
||||||
|
return {
|
||||||
|
status: HttpStatusCodes.NotFound,
|
||||||
|
data: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let data: IGitHubFile | IGitHubFile[];
|
let data: IGitHubFile | IGitHubFile[];
|
||||||
const entries = response.repository.object.entries;
|
const entries = response.repository.object.entries;
|
||||||
const gitHubRepo = GitHubClient.toGitHubRepo(response.repository);
|
const gitHubRepo = GitHubClient.toGitHubRepo(response.repository);
|
||||||
|
|
|
@ -2,10 +2,11 @@ import { Notebook, stringifyNotebook, makeNotebookRecord, toJS } from "@nteract/
|
||||||
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
|
import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core";
|
||||||
import { from, Observable, of } from "rxjs";
|
import { from, Observable, of } from "rxjs";
|
||||||
import { AjaxResponse } from "rxjs/ajax";
|
import { AjaxResponse } from "rxjs/ajax";
|
||||||
|
import * as Base64Utils from "../Utils/Base64Utils";
|
||||||
import { HttpStatusCodes } from "../Common/Constants";
|
import { HttpStatusCodes } from "../Common/Constants";
|
||||||
import * as Logger from "../Common/Logger";
|
import * as Logger from "../Common/Logger";
|
||||||
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
|
||||||
import { GitHubClient, IGitHubFile, IGitHubResponse, IGitHubCommit, IGitHubBranch } from "./GitHubClient";
|
import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient";
|
||||||
import * as GitHubUtils from "../Utils/GitHubUtils";
|
import * as GitHubUtils from "../Utils/GitHubUtils";
|
||||||
import UrlUtility from "../Common/UrlUtility";
|
import UrlUtility from "../Common/UrlUtility";
|
||||||
|
|
||||||
|
@ -131,7 +132,7 @@ export class GitHubContentProvider implements IContentProvider {
|
||||||
throw new GitHubContentProviderError(`Failed to parse ${uri}`);
|
throw new GitHubContentProviderError(`Failed to parse ${uri}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = btoa(stringifyNotebook(toJS(makeNotebookRecord())));
|
const content = Base64Utils.utf8ToB64(stringifyNotebook(toJS(makeNotebookRecord())));
|
||||||
const options: Intl.DateTimeFormatOptions = {
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
|
@ -195,34 +196,63 @@ export class GitHubContentProvider implements IContentProvider {
|
||||||
return from(
|
return from(
|
||||||
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
this.getContent(uri).then(async (content: IGitHubResponse<IGitHubFile | IGitHubFile[]>) => {
|
||||||
try {
|
try {
|
||||||
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Save", "Save");
|
let commitMsg: string;
|
||||||
|
if (content.status === HttpStatusCodes.NotFound) {
|
||||||
|
// We'll create a new file since it doesn't exist
|
||||||
|
commitMsg = await this.params.promptForCommitMsg("Save", "Save");
|
||||||
|
if (!commitMsg) {
|
||||||
|
throw new GitHubContentProviderError("Couldn't get a commit message");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commitMsg = await this.validateContentAndGetCommitMsg(content, "Save", "Save");
|
||||||
|
}
|
||||||
|
|
||||||
let updatedContent: string;
|
let updatedContent: string;
|
||||||
if (model.type === "notebook") {
|
if (model.type === "notebook") {
|
||||||
updatedContent = btoa(stringifyNotebook(model.content as Notebook));
|
updatedContent = Base64Utils.utf8ToB64(stringifyNotebook(model.content as Notebook));
|
||||||
} else if (model.type === "file") {
|
} else if (model.type === "file") {
|
||||||
updatedContent = model.content as string;
|
updatedContent = model.content as string;
|
||||||
if (model.format !== "base64") {
|
if (model.format !== "base64") {
|
||||||
updatedContent = btoa(updatedContent);
|
updatedContent = Base64Utils.utf8ToB64(updatedContent);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new GitHubContentProviderError("Unsupported content type");
|
throw new GitHubContentProviderError("Unsupported content type");
|
||||||
}
|
}
|
||||||
|
|
||||||
const gitHubFile = content.data as IGitHubFile;
|
const contentInfo = GitHubUtils.fromContentUri(uri);
|
||||||
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
let gitHubFile: IGitHubFile;
|
||||||
gitHubFile.repo.owner,
|
if (content.data) {
|
||||||
gitHubFile.repo.name,
|
gitHubFile = content.data as IGitHubFile;
|
||||||
gitHubFile.branch.name,
|
|
||||||
gitHubFile.path,
|
|
||||||
commitMsg,
|
|
||||||
updatedContent,
|
|
||||||
gitHubFile.sha
|
|
||||||
);
|
|
||||||
if (response.status !== HttpStatusCodes.OK) {
|
|
||||||
throw new GitHubContentProviderError("Failed to update", response.status);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const response = await this.params.gitHubClient.createOrUpdateFileAsync(
|
||||||
|
contentInfo.owner,
|
||||||
|
contentInfo.repo,
|
||||||
|
contentInfo.branch,
|
||||||
|
contentInfo.path,
|
||||||
|
commitMsg,
|
||||||
|
updatedContent,
|
||||||
|
gitHubFile?.sha
|
||||||
|
);
|
||||||
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.Created) {
|
||||||
|
throw new GitHubContentProviderError("Failed to create or update", response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gitHubFile) {
|
||||||
gitHubFile.commit = response.data;
|
gitHubFile.commit = response.data;
|
||||||
|
} else {
|
||||||
|
const contentResponse = await this.params.gitHubClient.getContentsAsync(
|
||||||
|
contentInfo.owner,
|
||||||
|
contentInfo.repo,
|
||||||
|
contentInfo.branch,
|
||||||
|
contentInfo.path
|
||||||
|
);
|
||||||
|
if (contentResponse.status !== HttpStatusCodes.OK) {
|
||||||
|
throw new GitHubContentProviderError("Failed to get content", response.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
gitHubFile = contentResponse.data as IGitHubFile;
|
||||||
|
}
|
||||||
|
|
||||||
return this.createSuccessAjaxResponse(
|
return this.createSuccessAjaxResponse(
|
||||||
HttpStatusCodes.OK,
|
HttpStatusCodes.OK,
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import * as Base64Utils from "./Base64Utils";
|
||||||
|
|
||||||
|
describe("Base64Utils", () => {
|
||||||
|
describe("utf8ToB64", () => {
|
||||||
|
it("should convert utf8 to base64", () => {
|
||||||
|
expect(Base64Utils.utf8ToB64("abcd")).toEqual(btoa("abcd"));
|
||||||
|
expect(Base64Utils.utf8ToB64("小飼弾")).toEqual("5bCP6aO85by+");
|
||||||
|
expect(Base64Utils.utf8ToB64("à mon hôpital préféré")).toEqual("w6AgbW9uIGjDtHBpdGFsIHByw6lmw6lyw6k=");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const utf8ToB64 = (utf8Str: string): string => {
|
||||||
|
return btoa(
|
||||||
|
encodeURIComponent(utf8Str).replace(/%([0-9A-F]{2})/g, (_, args) => {
|
||||||
|
return String.fromCharCode(parseInt(args, 16));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
|
@ -296,6 +296,10 @@
|
||||||
<div data-bind="react: publishNotebookPaneAdapter"></div>
|
<div data-bind="react: publishNotebookPaneAdapter"></div>
|
||||||
<!-- /ko -->
|
<!-- /ko -->
|
||||||
|
|
||||||
|
<!-- ko if: isCopyNotebookPaneEnabled -->
|
||||||
|
<div data-bind="react: copyNotebookPaneAdapter"></div>
|
||||||
|
<!-- /ko -->
|
||||||
|
|
||||||
<!-- Global access token expiration dialog - Start -->
|
<!-- Global access token expiration dialog - Start -->
|
||||||
<div
|
<div
|
||||||
id="dataAccessTokenModal"
|
id="dataAccessTokenModal"
|
||||||
|
|
Loading…
Reference in New Issue