Refactor GenericRightPaneComponent to accept a ReactComponent as its content (#146)

This commit is contained in:
victor-meng 2020-08-12 11:41:19 -07:00 committed by GitHub
parent 455722c316
commit fb71fb4e82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 288 additions and 236 deletions

View File

@ -6,17 +6,8 @@ import { JunoClient, IPinnedRepo } from "../../Juno/JunoClient";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils"; import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent"; import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
import { import { CopyNotebookPaneComponent, CopyNotebookPaneProps } from "./CopyNotebookPaneComponent";
Stack, import { IDropdownOption } from "office-ui-fabric-react";
Label,
Text,
Dropdown,
IDropdownProps,
IDropdownOption,
SelectableOptionMenuItemType,
IRenderFunction,
ISelectableOption
} from "office-ui-fabric-react";
import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService";
import { HttpStatusCodes } from "../../Common/Constants"; import { HttpStatusCodes } from "../../Common/Constants";
import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as GitHubUtils from "../../Utils/GitHubUtils";
@ -60,9 +51,8 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
return undefined; return undefined;
} }
const props: GenericRightPaneProps = { const genericPaneProps: GenericRightPaneProps = {
container: this.container, container: this.container,
content: this.createContent(),
formError: this.formError, formError: this.formError,
formErrorDetail: this.formErrorDetail, formErrorDetail: this.formErrorDetail,
id: "copynotebookpane", id: "copynotebookpane",
@ -73,7 +63,17 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
onSubmit: () => this.submit() onSubmit: () => this.submit()
}; };
return <GenericRightPaneComponent {...props} />; const copyNotebookPaneProps: CopyNotebookPaneProps = {
name: this.name,
pinnedRepos: this.pinnedRepos,
onDropDownChange: this.onDropDownChange
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<CopyNotebookPaneComponent {...copyNotebookPaneProps} />
</GenericRightPaneComponent>
);
} }
public triggerRender(): void { public triggerRender(): void {
@ -181,91 +181,6 @@ export class CopyNotebookPaneAdapter implements ReactAdapter {
return this.container.uploadFile(this.name, this.content, parent); 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 => { private onDropDownChange = (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption): void => {
this.selectedLocation = option?.data; this.selectedLocation = option?.data;
}; };

View File

@ -0,0 +1,119 @@
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,
IRenderFunction,
ISelectableOption
} from "office-ui-fabric-react";
interface Location {
type: "MyNotebooks" | "GitHub";
// GitHub
owner?: string;
repo?: string;
branch?: string;
}
export interface CopyNotebookPaneProps {
name: string;
pinnedRepos: IPinnedRepo[];
onDropDownChange: (_: React.FormEvent<HTMLDivElement>, option?: IDropdownOption) => void;
}
export class CopyNotebookPaneComponent extends React.Component<CopyNotebookPaneProps> {
private static readonly 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 => {
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.props.pinnedRepos && this.props.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.props.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: `${CopyNotebookPaneComponent.BranchNameWhiteSpace}${branch.name}`,
title: `${repoFullName} - ${branch.name}`,
data: {
type: "GitHub",
owner: pinnedRepo.owner,
repo: pinnedRepo.name,
branch: branch.name
} as Location
})
);
});
}
return options;
};
}

View File

@ -8,7 +8,6 @@ import Explorer from "../Explorer";
export interface GenericRightPaneProps { export interface GenericRightPaneProps {
container: Explorer; container: Explorer;
content: JSX.Element;
formError: string; formError: string;
formErrorDetail: string; formErrorDetail: string;
id: string; id: string;
@ -57,18 +56,18 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
> >
<div className="panelContentWrapper"> <div className="panelContentWrapper">
{this.createPanelHeader()} {this.renderPanelHeader()}
{this.createErrorSection()} {this.renderErrorSection()}
{this.props.content} {this.props.children}
{this.createPanelFooter()} {this.renderPanelFooter()}
</div> </div>
{this.createLoadingScreen()} {this.renderLoadingScreen()}
</div> </div>
</div> </div>
); );
} }
private createPanelHeader = (): JSX.Element => { private renderPanelHeader = (): JSX.Element => {
return ( return (
<div className="firstdivbg headerline"> <div className="firstdivbg headerline">
<span id="databaseTitle">{this.props.title}</span> <span id="databaseTitle">{this.props.title}</span>
@ -84,7 +83,7 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
); );
}; };
private createErrorSection = (): JSX.Element => { private renderErrorSection = (): JSX.Element => {
return ( return (
<div className="warningErrorContainer" aria-live="assertive" hidden={!this.props.formError}> <div className="warningErrorContainer" aria-live="assertive" hidden={!this.props.formError}>
<div className="warningErrorContent"> <div className="warningErrorContent">
@ -104,7 +103,7 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
); );
}; };
private createPanelFooter = (): JSX.Element => { private renderPanelFooter = (): JSX.Element => {
return ( return (
<div className="paneFooter"> <div className="paneFooter">
<div className="leftpanel-okbut"> <div className="leftpanel-okbut">
@ -122,7 +121,7 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
); );
}; };
private createLoadingScreen = (): JSX.Element => { private renderLoadingScreen = (): JSX.Element => {
return ( return (
<div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.props.isExecuting}> <div className="dataExplorerLoaderContainer dataExplorerPaneLoaderContainer" hidden={!this.props.isExecuting}>
<img className="dataExplorerLoader" src={LoadingIndicatorIcon} /> <img className="dataExplorerLoader" src={LoadingIndicatorIcon} />

View File

@ -44,7 +44,6 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
const props: GenericRightPaneProps = { const props: GenericRightPaneProps = {
container: this.container, container: this.container,
content: this.createContent(),
formError: this.formError, formError: this.formError,
formErrorDetail: this.formErrorDetail, formErrorDetail: this.formErrorDetail,
id: "publishnotebookpane", id: "publishnotebookpane",
@ -56,7 +55,39 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
isSubmitButtonVisible: this.isCodeOfConductAccepted isSubmitButtonVisible: this.isCodeOfConductAccepted
}; };
return <GenericRightPaneComponent {...props} />; const publishNotebookPaneProps: PublishNotebookPaneProps = {
notebookName: this.name,
notebookDescription: "",
notebookTags: "",
notebookAuthor: this.author,
notebookCreatedDate: new Date().toISOString(),
notebookObject: this.notebookObject,
notebookParentDomElement: this.parentDomElement,
onChangeName: (newValue: string) => (this.name = newValue),
onChangeDescription: (newValue: string) => (this.description = newValue),
onChangeTags: (newValue: string) => (this.tags = newValue),
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
onError: this.createFormErrorForLargeImageSelection,
clearFormError: this.clearFormError
};
return (
<GenericRightPaneComponent {...props}>
{!this.isCodeOfConductAccepted ? (
<div style={{ padding: "15px", marginTop: "10px" }}>
<CodeOfConductComponent
junoClient={this.junoClient}
onAcceptCodeOfConduct={() => {
this.isCodeOfConductAccepted = true;
this.triggerRender();
}}
/>
</div>
) : (
<PublishNotebookPaneComponent {...publishNotebookPaneProps} />
)}
</GenericRightPaneComponent>
);
} }
public triggerRender(): void { public triggerRender(): void {
@ -166,38 +197,6 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
this.triggerRender(); this.triggerRender();
}; };
private createContent = (): JSX.Element => {
const publishNotebookPaneProps: PublishNotebookPaneProps = {
notebookName: this.name,
notebookDescription: "",
notebookTags: "",
notebookAuthor: this.author,
notebookCreatedDate: new Date().toISOString(),
notebookObject: this.notebookObject,
notebookParentDomElement: this.parentDomElement,
onChangeName: (newValue: string) => (this.name = newValue),
onChangeDescription: (newValue: string) => (this.description = newValue),
onChangeTags: (newValue: string) => (this.tags = newValue),
onChangeImageSrc: (newValue: string) => (this.imageSrc = newValue),
onError: this.createFormErrorForLargeImageSelection,
clearFormError: this.clearFormError
};
return !this.isCodeOfConductAccepted ? (
<div style={{ padding: "15px", marginTop: "10px" }}>
<CodeOfConductComponent
junoClient={this.junoClient}
onAcceptCodeOfConduct={() => {
this.isCodeOfConductAccepted = true;
this.triggerRender();
}}
/>
</div>
) : (
<PublishNotebookPaneComponent {...publishNotebookPaneProps} />
);
};
private reset = (): void => { private reset = (): void => {
this.isOpened = false; this.isOpened = false;
this.isExecuting = false; this.isExecuting = false;

View File

@ -1,15 +1,13 @@
import * as Constants from "../../Common/Constants";
import * as ErrorParserUtility from "../../Common/ErrorParserUtility"; import * as ErrorParserUtility from "../../Common/ErrorParserUtility";
import * as ko from "knockout"; import * as ko from "knockout";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import * as React from "react"; import * as React from "react";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { IconButton } from "office-ui-fabric-react/lib/Button";
import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent"; import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRightPaneComponent";
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions"; import { UploadDetailsRecord, UploadDetails } from "../../workers/upload/definitions";
import InfoBubbleIcon from "../../../images/info-bubble.svg"; import { UploadItemsPaneComponent, UploadItemsPaneProps } from "./UploadItemsPaneComponent";
import Explorer from "../Explorer"; import Explorer from "../Explorer";
const UPLOAD_FILE_SIZE_LIMIT = 2097152; const UPLOAD_FILE_SIZE_LIMIT = 2097152;
@ -35,19 +33,30 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
return undefined; return undefined;
} }
const props: GenericRightPaneProps = { const genericPaneProps: GenericRightPaneProps = {
container: this.container, container: this.container,
content: this.createContent(),
formError: this.formError, formError: this.formError,
formErrorDetail: this.formErrorDetail, formErrorDetail: this.formErrorDetail,
id: "uploaditemspane", id: "uploaditemspane",
isExecuting: this.isExecuting, isExecuting: this.isExecuting,
isSubmitButtonVisible: true,
title: "Upload Items", title: "Upload Items",
submitButtonText: "Upload", submitButtonText: "Upload",
onClose: () => this.close(), onClose: () => this.close(),
onSubmit: () => this.submit() onSubmit: () => this.submit()
}; };
return <GenericRightPaneComponent {...props} />;
const uploadItemsPaneProps: UploadItemsPaneProps = {
selectedFilesTitle: this.selectedFilesTitle,
updateSelectedFiles: this.updateSelectedFiles,
uploadFileData: this.uploadFileData
};
return (
<GenericRightPaneComponent {...genericPaneProps}>
<UploadItemsPaneComponent {...uploadItemsPaneProps} />
</GenericRightPaneComponent>
);
} }
public triggerRender(): void { public triggerRender(): void {
@ -110,77 +119,6 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
}); });
} }
private createContent = (): JSX.Element => {
return <div className="panelContent">{this.createMainContentSection()}</div>;
};
private createMainContentSection = (): JSX.Element => {
return (
<div className="paneMainContent">
<div className="renewUploadItemsHeader">
<span> Select JSON Files </span>
<span className="infoTooltip" role="tooltip" tabIndex={0}>
<img className="infoImg" src={InfoBubbleIcon} alt="More information" />
<span className="tooltiptext infoTooltipWidth">
Select one or more JSON files to upload. Each file can contain a single JSON document or an array of JSON
documents. The combined size of all files in an individual upload operation must be less than 2 MB. You
can perform multiple upload operations for larger data sets.
</span>
</span>
</div>
<input
className="importFilesTitle"
type="text"
disabled
value={this.selectedFilesTitle}
aria-label="Select JSON Files"
/>
<input
type="file"
id="importDocsInput"
title="Upload Icon"
multiple
accept="application/json"
role="button"
tabIndex={0}
style={{ display: "none" }}
onChange={this.updateSelectedFiles}
/>
<IconButton
iconProps={{ iconName: "FolderHorizontal" }}
className="fileImportButton"
alt="Select JSON files to upload"
title="Select JSON files to upload"
onClick={this.onImportButtonClick}
onKeyPress={this.onImportButtonKeyPress}
/>
<div className="fileUploadSummaryContainer" hidden={this.uploadFileData.length === 0}>
<b>File upload status</b>
<table className="fileUploadSummary">
<thead>
<tr className="fileUploadSummaryHeader fileUploadSummaryTuple">
<th>FILE NAME</th>
<th>STATUS</th>
</tr>
</thead>
<tbody>
{this.uploadFileData.map(
(data: UploadDetailsRecord): JSX.Element => {
return (
<tr className="fileUploadSummaryTuple" key={data.fileName}>
<td>{data.fileName}</td>
<td>{this.fileUploadSummaryText(data.numSucceeded, data.numFailed)}</td>
</tr>
);
}
)}
</tbody>
</table>
</div>
</div>
);
};
private updateSelectedFiles = (event: React.ChangeEvent<HTMLInputElement>): void => { private updateSelectedFiles = (event: React.ChangeEvent<HTMLInputElement>): void => {
this.selectedFiles = event.target.files; this.selectedFiles = event.target.files;
this._updateSelectedFilesTitle(); this._updateSelectedFilesTitle();
@ -212,21 +150,6 @@ export class UploadItemsPaneAdapter implements ReactAdapter {
return totalFileSize; return totalFileSize;
} }
private fileUploadSummaryText = (numSucceeded: number, numFailed: number): string => {
return `${numSucceeded} items created, ${numFailed} errors`;
};
private onImportButtonClick = (): void => {
document.getElementById("importDocsInput").click();
};
private onImportButtonKeyPress = (event: React.KeyboardEvent<HTMLButtonElement>): void => {
if (event.charCode === Constants.KeyCodes.Enter || event.charCode === Constants.KeyCodes.Space) {
this.onImportButtonClick();
event.stopPropagation();
}
};
private reset = (): void => { private reset = (): void => {
this.isOpened = false; this.isOpened = false;
this.isExecuting = false; this.isExecuting = false;

View File

@ -0,0 +1,97 @@
import * as Constants from "../../Common/Constants";
import * as React from "react";
import { IconButton } from "office-ui-fabric-react/lib/Button";
import { UploadDetailsRecord } from "../../workers/upload/definitions";
import InfoBubbleIcon from "../../../images/info-bubble.svg";
export interface UploadItemsPaneProps {
selectedFilesTitle: string;
updateSelectedFiles: (event: React.ChangeEvent<HTMLInputElement>) => void;
uploadFileData: UploadDetailsRecord[];
}
export class UploadItemsPaneComponent extends React.Component<UploadItemsPaneProps> {
public render(): JSX.Element {
return (
<div className="panelContent">
<div className="paneMainContent">
<div className="renewUploadItemsHeader">
<span> Select JSON Files </span>
<span className="infoTooltip" role="tooltip" tabIndex={0}>
<img className="infoImg" src={InfoBubbleIcon} alt="More information" />
<span className="tooltiptext infoTooltipWidth">
Select one or more JSON files to upload. Each file can contain a single JSON document or an array of
JSON documents. The combined size of all files in an individual upload operation must be less than 2 MB.
You can perform multiple upload operations for larger data sets.
</span>
</span>
</div>
<input
className="importFilesTitle"
type="text"
disabled
value={this.props.selectedFilesTitle}
aria-label="Select JSON Files"
/>
<input
type="file"
id="importDocsInput"
title="Upload Icon"
multiple
accept="application/json"
role="button"
tabIndex={0}
style={{ display: "none" }}
onChange={this.props.updateSelectedFiles}
/>
<IconButton
iconProps={{ iconName: "FolderHorizontal" }}
className="fileImportButton"
alt="Select JSON files to upload"
title="Select JSON files to upload"
onClick={this.onImportButtonClick}
onKeyPress={this.onImportButtonKeyPress}
/>
<div className="fileUploadSummaryContainer" hidden={this.props.uploadFileData.length === 0}>
<b>File upload status</b>
<table className="fileUploadSummary">
<thead>
<tr className="fileUploadSummaryHeader fileUploadSummaryTuple">
<th>FILE NAME</th>
<th>STATUS</th>
</tr>
</thead>
<tbody>
{this.props.uploadFileData.map(
(data: UploadDetailsRecord): JSX.Element => {
return (
<tr className="fileUploadSummaryTuple" key={data.fileName}>
<td>{data.fileName}</td>
<td>{this.fileUploadSummaryText(data.numSucceeded, data.numFailed)}</td>
</tr>
);
}
)}
</tbody>
</table>
</div>
</div>
</div>
);
}
private fileUploadSummaryText = (numSucceeded: number, numFailed: number): string => {
return `${numSucceeded} items created, ${numFailed} errors`;
};
private onImportButtonClick = (): void => {
document.getElementById("importDocsInput").click();
};
private onImportButtonKeyPress = (event: React.KeyboardEvent<HTMLButtonElement>): void => {
if (event.charCode === Constants.KeyCodes.Enter || event.charCode === Constants.KeyCodes.Space) {
this.onImportButtonClick();
event.stopPropagation();
}
};
}