mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-21 09:51:11 +00:00
Migrate Copy Notebook Pane to React (#640)
Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
This commit is contained in:
@@ -1,197 +0,0 @@
|
||||
import ko from "knockout";
|
||||
import { IDropdownOption } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import { HttpStatusCodes } from "../../Common/Constants";
|
||||
import { getErrorMessage, handleError } from "../../Common/ErrorHandlingUtils";
|
||||
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
|
||||
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../Explorer";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "../Notebook/NotebookContentItem";
|
||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
|
||||
import {
|
||||
GenericRightPaneComponent,
|
||||
GenericRightPaneProps,
|
||||
} from "./GenericRightPaneComponent/GenericRightPaneComponent";
|
||||
|
||||
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 genericPaneProps: GenericRightPaneProps = {
|
||||
container: this.container,
|
||||
formError: this.formError,
|
||||
formErrorDetail: this.formErrorDetail,
|
||||
id: "copynotebookpane",
|
||||
isExecuting: this.isExecuting,
|
||||
title: "Copy notebook",
|
||||
submitButtonText: "OK",
|
||||
onClose: () => this.close(),
|
||||
onSubmit: () => this.submit(),
|
||||
};
|
||||
|
||||
const copyNotebookPaneProps: CopyNotebookPaneProps = {
|
||||
name: this.name,
|
||||
pinnedRepos: this.pinnedRepos,
|
||||
onDropDownChange: this.onDropDownChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<GenericRightPaneComponent {...genericPaneProps}>
|
||||
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
|
||||
</GenericRightPaneComponent>
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit");
|
||||
}
|
||||
|
||||
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) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
this.formError = `Failed to copy ${this.name} to ${destination}`;
|
||||
this.formErrorDetail = `${errorMessage}`;
|
||||
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", this.formError);
|
||||
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 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;
|
||||
};
|
||||
}
|
||||
156
src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx
Normal file
156
src/Explorer/Panes/CopyNotebookPane/CopyNotebookPane.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { IDropdownOption } from "office-ui-fabric-react";
|
||||
import React, { FormEvent, FunctionComponent, useEffect, useState } from "react";
|
||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||
import { getErrorMessage, handleError } from "../../../Common/ErrorHandlingUtils";
|
||||
import { GitHubOAuthService } from "../../../GitHub/GitHubOAuthService";
|
||||
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import * as NotificationConsoleUtils from "../../../Utils/NotificationConsoleUtils";
|
||||
import Explorer from "../../Explorer";
|
||||
import { NotebookContentItem, NotebookContentItemType } from "../../Notebook/NotebookContentItem";
|
||||
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
|
||||
import {
|
||||
GenericRightPaneComponent,
|
||||
GenericRightPaneProps,
|
||||
} from "../GenericRightPaneComponent/GenericRightPaneComponent";
|
||||
import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
|
||||
|
||||
interface Location {
|
||||
type: "MyNotebooks" | "GitHub";
|
||||
|
||||
// GitHub
|
||||
owner?: string;
|
||||
repo?: string;
|
||||
branch?: string;
|
||||
}
|
||||
export interface CopyNotebookPanelProps {
|
||||
name: string;
|
||||
content: string;
|
||||
container: Explorer;
|
||||
junoClient: JunoClient;
|
||||
gitHubOAuthService: GitHubOAuthService;
|
||||
closePanel: () => void;
|
||||
}
|
||||
|
||||
export const CopyNotebookPane: FunctionComponent<CopyNotebookPanelProps> = ({
|
||||
name,
|
||||
content,
|
||||
container,
|
||||
junoClient,
|
||||
gitHubOAuthService,
|
||||
closePanel,
|
||||
}: CopyNotebookPanelProps) => {
|
||||
const [isExecuting, setIsExecuting] = useState<boolean>();
|
||||
const [formError, setFormError] = useState<string>("");
|
||||
const [formErrorDetail, setFormErrorDetail] = useState<string>("");
|
||||
const [pinnedRepos, setPinnedRepos] = useState<IPinnedRepo[]>();
|
||||
const [selectedLocation, setSelectedLocation] = useState<Location>();
|
||||
|
||||
useEffect(() => {
|
||||
open();
|
||||
}, []);
|
||||
|
||||
const open = async (): Promise<void> => {
|
||||
if (gitHubOAuthService.isLoggedIn()) {
|
||||
const response = await junoClient.getPinnedRepos(gitHubOAuthService.getTokenObservable()()?.scope);
|
||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||
handleError(`Received HTTP ${response.status} when fetching pinned repos`, "CopyNotebookPaneAdapter/submit");
|
||||
}
|
||||
|
||||
if (response.data?.length > 0) {
|
||||
setPinnedRepos(response.data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async (): Promise<void> => {
|
||||
let destination: string = selectedLocation?.type;
|
||||
let clearMessage: () => void;
|
||||
setIsExecuting(true);
|
||||
|
||||
try {
|
||||
if (!selectedLocation) {
|
||||
throw new Error(`No location selected`);
|
||||
}
|
||||
|
||||
if (selectedLocation.type === "GitHub") {
|
||||
destination = `${destination} - ${GitHubUtils.toRepoFullName(
|
||||
selectedLocation.owner,
|
||||
selectedLocation.repo
|
||||
)} - ${selectedLocation.branch}`;
|
||||
}
|
||||
|
||||
clearMessage = NotificationConsoleUtils.logConsoleProgress(`Copying ${name} to ${destination}`);
|
||||
|
||||
const notebookContentItem = await copyNotebook(selectedLocation);
|
||||
if (!notebookContentItem) {
|
||||
throw new Error(`Failed to upload ${name}`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.logConsoleInfo(`Successfully copied ${name} to ${destination}`);
|
||||
closePanel();
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error);
|
||||
setFormError(`Failed to copy ${name} to ${destination}`);
|
||||
setFormErrorDetail(`${errorMessage}`);
|
||||
handleError(errorMessage, "CopyNotebookPaneAdapter/submit", formError);
|
||||
} finally {
|
||||
clearMessage && clearMessage();
|
||||
setIsExecuting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyNotebook = async (location: Location): Promise<NotebookContentItem> => {
|
||||
let parent: NotebookContentItem;
|
||||
switch (location.type) {
|
||||
case "MyNotebooks":
|
||||
parent = {
|
||||
name: ResourceTreeAdapter.MyNotebooksTitle,
|
||||
path: container.getNotebookBasePath(),
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
break;
|
||||
|
||||
case "GitHub":
|
||||
parent = {
|
||||
name: ResourceTreeAdapter.GitHubReposTitle,
|
||||
path: GitHubUtils.toContentUri(selectedLocation.owner, selectedLocation.repo, selectedLocation.branch, ""),
|
||||
type: NotebookContentItemType.Directory,
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported location type ${location.type}`);
|
||||
}
|
||||
|
||||
return container.uploadFile(name, content, parent);
|
||||
};
|
||||
|
||||
const onDropDownChange = (_: FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
|
||||
setSelectedLocation(option?.data);
|
||||
};
|
||||
|
||||
const genericPaneProps: GenericRightPaneProps = {
|
||||
container,
|
||||
formError,
|
||||
formErrorDetail,
|
||||
id: "copynotebookpane",
|
||||
isExecuting: isExecuting,
|
||||
title: "Copy notebook",
|
||||
submitButtonText: "OK",
|
||||
onClose: closePanel,
|
||||
onSubmit: () => submit(),
|
||||
};
|
||||
|
||||
const copyNotebookPaneProps: CopyNotebookPaneProps = {
|
||||
name,
|
||||
pinnedRepos,
|
||||
onDropDownChange: onDropDownChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<GenericRightPaneComponent {...genericPaneProps}>
|
||||
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
|
||||
</GenericRightPaneComponent>
|
||||
);
|
||||
};
|
||||
@@ -1,18 +1,18 @@
|
||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||
import * as React from "react";
|
||||
import { IPinnedRepo } from "../../Juno/JunoClient";
|
||||
import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter";
|
||||
import {
|
||||
Stack,
|
||||
Label,
|
||||
Text,
|
||||
Dropdown,
|
||||
IDropdownProps,
|
||||
IDropdownOption,
|
||||
SelectableOptionMenuItemType,
|
||||
IDropdownProps,
|
||||
IRenderFunction,
|
||||
ISelectableOption,
|
||||
Label,
|
||||
SelectableOptionMenuItemType,
|
||||
Stack,
|
||||
Text,
|
||||
} from "office-ui-fabric-react";
|
||||
import React, { FormEvent, FunctionComponent } from "react";
|
||||
import { IPinnedRepo } from "../../../Juno/JunoClient";
|
||||
import * as GitHubUtils from "../../../Utils/GitHubUtils";
|
||||
import { ResourceTreeAdapter } from "../../Tree/ResourceTreeAdapter";
|
||||
|
||||
interface Location {
|
||||
type: "MyNotebooks" | "GitHub";
|
||||
@@ -26,46 +26,25 @@ interface Location {
|
||||
export interface CopyNotebookPaneProps {
|
||||
name: string;
|
||||
pinnedRepos: IPinnedRepo[];
|
||||
onDropDownChange: (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption) => void;
|
||||
onDropDownChange: (_: FormEvent<HTMLDivElement>, option?: IDropdownOption) => void;
|
||||
}
|
||||
|
||||
export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneProps> {
|
||||
private static readonly BranchNameWhiteSpace = " ";
|
||||
export const CopyNotebookPaneComponent: FunctionComponent<CopyNotebookPaneProps> = ({
|
||||
name,
|
||||
pinnedRepos,
|
||||
onDropDownChange,
|
||||
}: CopyNotebookPaneProps) => {
|
||||
const BranchNameWhiteSpace = " ";
|
||||
|
||||
public render(): JSX.Element {
|
||||
const dropDownProps: IDropdownProps = {
|
||||
label: "Location",
|
||||
ariaLabel: "Location",
|
||||
placeholder: "Select an option",
|
||||
onRenderTitle: this.onRenderDropDownTitle,
|
||||
onRenderOption: this.onRenderDropDownOption,
|
||||
options: this.getDropDownOptions(),
|
||||
onChange: this.props.onDropDownChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="paneMainContent">
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack.Item>
|
||||
<Label htmlFor="notebookName">Name</Label>
|
||||
<Text id="notebookName">{this.props.name}</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Dropdown {...dropDownProps} />
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => {
|
||||
const onRenderDropDownTitle: IRenderFunction<IDropdownOption[]> = (options: IDropdownOption[]): JSX.Element => {
|
||||
return <span>{options.length && options[0].title}</span>;
|
||||
};
|
||||
|
||||
private onRenderDropDownOption: IRenderFunction<ISelectableOption> = (option: ISelectableOption): JSX.Element => {
|
||||
const onRenderDropDownOption: IRenderFunction<ISelectableOption> = (option: ISelectableOption): JSX.Element => {
|
||||
return <span style={{ whiteSpace: "pre-wrap" }}>{option.text}</span>;
|
||||
};
|
||||
|
||||
private getDropDownOptions = (): IDropdownOption[] => {
|
||||
const getDropDownOptions = (): IDropdownOption[] => {
|
||||
const options: IDropdownOption[] = [];
|
||||
|
||||
options.push({
|
||||
@@ -77,7 +56,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
|
||||
} as Location,
|
||||
});
|
||||
|
||||
if (this.props.pinnedRepos && this.props.pinnedRepos.length > 0) {
|
||||
if (pinnedRepos && pinnedRepos.length > 0) {
|
||||
options.push({
|
||||
key: "GitHub-Header-Divider",
|
||||
text: undefined,
|
||||
@@ -90,7 +69,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
|
||||
itemType: SelectableOptionMenuItemType.Header,
|
||||
});
|
||||
|
||||
this.props.pinnedRepos.forEach((pinnedRepo) => {
|
||||
pinnedRepos.forEach((pinnedRepo) => {
|
||||
const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name);
|
||||
options.push({
|
||||
key: `GitHub-Repo-${repoFullName}`,
|
||||
@@ -101,7 +80,7 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
|
||||
pinnedRepo.branches.forEach((branch) =>
|
||||
options.push({
|
||||
key: `GitHub-Repo-${repoFullName}-${branch.name}`,
|
||||
text: `${CopyNotebookPaneComponent.BranchNameWhiteSpace}${branch.name}`,
|
||||
text: `${BranchNameWhiteSpace}${branch.name}`,
|
||||
title: `${repoFullName} - ${branch.name}`,
|
||||
data: {
|
||||
type: "GitHub",
|
||||
@@ -116,4 +95,26 @@ export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneP
|
||||
|
||||
return options;
|
||||
};
|
||||
}
|
||||
const dropDownProps: IDropdownProps = {
|
||||
label: "Location",
|
||||
ariaLabel: "Location",
|
||||
placeholder: "Select an option",
|
||||
onRenderTitle: onRenderDropDownTitle,
|
||||
onRenderOption: onRenderDropDownOption,
|
||||
options: getDropDownOptions(),
|
||||
onChange: onDropDownChange,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="paneMainContent">
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack.Item>
|
||||
<Label htmlFor="notebookName">Name</Label>
|
||||
<Text id="notebookName">{name}</Text>
|
||||
</Stack.Item>
|
||||
|
||||
<Dropdown {...dropDownProps} />
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -528,7 +528,6 @@ exports[`Settings Pane should render Default properly 1`] = `
|
||||
"hasStorageAnalyticsAfecFeature": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAutoscaleDefaultEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGitHubPaneEnabled": [Function],
|
||||
@@ -1313,7 +1312,6 @@ exports[`Settings Pane should render Gremlin properly 1`] = `
|
||||
"hasStorageAnalyticsAfecFeature": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAutoscaleDefaultEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGitHubPaneEnabled": [Function],
|
||||
|
||||
@@ -528,7 +528,6 @@ exports[`Upload Items Pane should render Default properly 1`] = `
|
||||
"hasStorageAnalyticsAfecFeature": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAutoscaleDefaultEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGitHubPaneEnabled": [Function],
|
||||
|
||||
@@ -529,7 +529,6 @@ exports[`Delete Database Confirmation Pane submit() Should call delete database
|
||||
"hasStorageAnalyticsAfecFeature": [Function],
|
||||
"isAccountReady": [Function],
|
||||
"isAutoscaleDefaultEnabled": [Function],
|
||||
"isCopyNotebookPaneEnabled": [Function],
|
||||
"isEnableMongoCapabilityPresent": [Function],
|
||||
"isFixedCollectionWithSharedThroughputSupported": [Function],
|
||||
"isGitHubPaneEnabled": [Function],
|
||||
|
||||
Reference in New Issue
Block a user