Move GitHub repos panel to react (#638)

Co-authored-by: Steve Faulkner <southpolesteve@gmail.com>
This commit is contained in:
vaidankarswapnil
2021-05-04 06:26:47 +05:30
committed by GitHub
parent 4efacace16
commit 038f3ee684
17 changed files with 6231 additions and 203 deletions

View File

@@ -0,0 +1,453 @@
import React from "react";
import { Areas, HttpStatusCodes } from "../../../Common/Constants";
import { handleError } from "../../../Common/ErrorHandlingUtils";
import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../../GitHub/GitHubClient";
import { IPinnedRepo, JunoClient } from "../../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as GitHubUtils from "../../../Utils/GitHubUtils";
import * as JunoUtils from "../../../Utils/JunoUtils";
import { AuthorizeAccessComponent } from "../../Controls/GitHub/AuthorizeAccessComponent";
import {
GitHubReposComponent,
GitHubReposComponentProps,
RepoListItem,
} from "../../Controls/GitHub/GitHubReposComponent";
import { ContentMainStyle } from "../../Controls/GitHub/GitHubStyleConstants";
import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../../Controls/GitHub/ReposListComponent";
import Explorer from "../../Explorer";
import { PanelInfoErrorComponent } from "../PanelInfoErrorComponent";
import { PanelLoadingScreen } from "../PanelLoadingScreen";
interface IGitHubReposPanelProps {
explorer: Explorer;
closePanel: () => void;
gitHubClientProp: GitHubClient;
junoClientProp: JunoClient;
openNotificationConsole: () => void;
}
interface IGitHubReposPanelState {
showAuthorizationAcessState: boolean;
isExecuting: boolean;
errorMessage: string;
showErrorDetails: boolean;
gitHubReposState: GitHubReposComponentProps;
}
export class GitHubReposPanel extends React.Component<IGitHubReposPanelProps, IGitHubReposPanelState> {
private static readonly PageSize = 30;
private isAddedRepo = false;
private gitHubClient: GitHubClient;
private junoClient: JunoClient;
private branchesProps: Record<string, BranchesProps>;
private pinnedReposProps: PinnedReposProps;
private unpinnedReposProps: UnpinnedReposProps;
private allGitHubRepos: IGitHubRepo[];
private allGitHubReposLastPageInfo?: IGitHubPageInfo;
private pinnedReposUpdated: boolean;
constructor(props: IGitHubReposPanelProps) {
super(props);
this.unpinnedReposProps = {
repos: [],
hasMore: true,
isLoading: true,
loadMore: (): Promise<void> => this.loadMoreUnpinnedRepos(),
};
this.branchesProps = {};
this.pinnedReposProps = {
repos: [],
};
this.allGitHubRepos = [];
this.allGitHubReposLastPageInfo = undefined;
this.pinnedReposUpdated = false;
this.state = {
showAuthorizationAcessState: true,
isExecuting: false,
errorMessage: "",
showErrorDetails: false,
gitHubReposState: {
showAuthorizeAccess: !this.props.explorer.notebookManager?.gitHubOAuthService.isLoggedIn(),
authorizeAccessProps: {
scope: this.getOAuthScope(),
authorizeAccess: (scope): void => this.connectToGitHub(scope),
},
reposListProps: {
branchesProps: this.branchesProps,
pinnedReposProps: this.pinnedReposProps,
unpinnedReposProps: this.unpinnedReposProps,
pinRepo: (item): Promise<void> => this.pinRepo(item),
unpinRepo: (item): Promise<void> => this.unpinRepo(item),
},
addRepoProps: {
container: this.props.explorer,
getRepo: (owner, repo): Promise<IGitHubRepo> => this.getRepo(owner, repo),
pinRepo: (item): Promise<void> => this.pinRepo(item),
},
resetConnection: (): void => this.setup(true),
onOkClick: (): Promise<void> => this.submit(),
onCancelClick: (): void => this.props.closePanel(),
},
};
this.gitHubClient = this.props.gitHubClientProp;
this.junoClient = this.props.junoClientProp;
}
componentDidMount(): void {
this.open();
}
public open(): void {
this.resetData();
this.setup();
}
public async submit(): Promise<void> {
const pinnedReposUpdated = this.pinnedReposUpdated;
const reposToPin: IPinnedRepo[] = this.pinnedReposProps.repos.map((repo) => JunoUtils.toPinnedRepo(repo));
if (pinnedReposUpdated) {
try {
const response = await this.junoClient.updatePinnedRepos(reposToPin);
if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received HTTP ${response.status} when saving pinned repos`);
}
} catch (error) {
handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos");
}
}
}
public resetData(): void {
this.branchesProps = {};
this.pinnedReposProps.repos = [];
this.unpinnedReposProps.repos = [];
this.allGitHubRepos = [];
this.allGitHubReposLastPageInfo = undefined;
this.pinnedReposUpdated = false;
this.unpinnedReposProps.hasMore = true;
this.unpinnedReposProps.isLoading = true;
}
private getOAuthScope(): string {
return (
this.props.explorer.notebookManager?.gitHubOAuthService.getTokenObservable()()?.scope ||
AuthorizeAccessComponent.Scopes.Public.key
);
}
private setup(forceShowConnectToGitHub = false): void {
forceShowConnectToGitHub || !this.props.explorer.notebookManager?.gitHubOAuthService.isLoggedIn()
? this.setupForConnectToGitHub()
: this.setupForManageRepos();
}
private setupForConnectToGitHub(): void {
this.setState({
isExecuting: false,
});
}
private async setupForManageRepos(): Promise<void> {
this.setState({
isExecuting: false,
});
TelemetryProcessor.trace(Action.NotebooksGitHubManageRepo, ActionModifiers.Mark, {
dataExplorerArea: Areas.Notebook,
});
this.refreshManageReposComponent();
}
private calculateUnpinnedRepos(): RepoListItem[] {
const unpinnedGitHubRepos = this.allGitHubRepos.filter(
(gitHubRepo) =>
this.pinnedReposProps.repos.findIndex(
(pinnedRepo) => pinnedRepo.key === GitHubUtils.toRepoFullName(gitHubRepo.owner, gitHubRepo.name)
) === -1
);
return unpinnedGitHubRepos.map((gitHubRepo) => ({
key: GitHubUtils.toRepoFullName(gitHubRepo.owner, gitHubRepo.name),
repo: gitHubRepo,
branches: [],
}));
}
private async loadMoreBranches(repo: IGitHubRepo): Promise<void> {
const branchesProps = this.branchesProps[GitHubUtils.toRepoFullName(repo.owner, repo.name)];
branchesProps.hasMore = true;
branchesProps.isLoading = true;
try {
const response = await this.gitHubClient.getBranchesAsync(
repo.owner,
repo.name,
GitHubReposPanel.PageSize,
branchesProps.lastPageInfo?.endCursor
);
if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received HTTP ${response.status} when fetching branches`);
}
if (response.data) {
branchesProps.branches = branchesProps.branches.concat(response.data);
branchesProps.lastPageInfo = response.pageInfo;
}
} catch (error) {
handleError(error, "GitHubReposPane/loadMoreBranches", "Failed to fetch branches");
}
branchesProps.isLoading = false;
branchesProps.hasMore = branchesProps.lastPageInfo?.hasNextPage;
this.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
branchesProps: {
...this.state.gitHubReposState.reposListProps.branchesProps,
[GitHubUtils.toRepoFullName(repo.owner, repo.name)]: branchesProps,
},
pinnedReposProps: {
repos: this.pinnedReposProps.repos,
},
unpinnedReposProps: {
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
repos: this.unpinnedReposProps.repos,
},
},
},
});
}
private async loadMoreUnpinnedRepos(): Promise<void> {
this.unpinnedReposProps.isLoading = true;
this.unpinnedReposProps.hasMore = true;
try {
const response = await this.gitHubClient.getReposAsync(
GitHubReposPanel.PageSize,
this.allGitHubReposLastPageInfo?.endCursor
);
if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received HTTP ${response.status} when fetching unpinned repos`);
}
if (response.data) {
this.allGitHubRepos = this.allGitHubRepos.concat(response.data);
this.allGitHubReposLastPageInfo = response.pageInfo;
this.unpinnedReposProps.repos = this.calculateUnpinnedRepos();
}
} catch (error) {
handleError(error, "GitHubReposPane/loadMoreUnpinnedRepos", "Failed to fetch unpinned repos");
}
this.unpinnedReposProps.isLoading = false;
this.unpinnedReposProps.hasMore = this.allGitHubReposLastPageInfo?.hasNextPage;
this.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
unpinnedReposProps: {
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
isLoading: this.unpinnedReposProps.isLoading,
hasMore: this.unpinnedReposProps.hasMore,
repos: this.unpinnedReposProps.repos,
},
},
},
});
}
private async getRepo(owner: string, repo: string): Promise<IGitHubRepo> {
try {
const response = await this.gitHubClient.getRepoAsync(owner, repo);
if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received HTTP ${response.status} when fetching repo`);
}
this.isAddedRepo = true;
return response.data;
} catch (error) {
handleError(error, "GitHubReposPane/getRepo", "Failed to fetch repo");
return Promise.resolve(undefined);
}
}
private async pinRepo(item: RepoListItem): Promise<void> {
this.pinnedReposUpdated = true;
const initialReposLength = this.pinnedReposProps.repos.length;
const existingRepo = this.pinnedReposProps.repos.find((repo) => repo.key === item.key);
if (existingRepo) {
existingRepo.branches = item.branches;
} else {
this.pinnedReposProps.repos = [...this.pinnedReposProps.repos, item];
}
this.unpinnedReposProps.repos = this.calculateUnpinnedRepos();
if (this.pinnedReposProps.repos.length > initialReposLength) {
this.refreshBranchesForPinnedRepos();
}
}
private async unpinRepo(item: RepoListItem): Promise<void> {
this.pinnedReposUpdated = true;
this.pinnedReposProps.repos = this.pinnedReposProps.repos.filter((pinnedRepo) => pinnedRepo.key !== item.key);
this.unpinnedReposProps.repos = this.calculateUnpinnedRepos();
this.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
pinnedReposProps: {
repos: this.pinnedReposProps.repos,
},
unpinnedReposProps: {
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
repos: this.unpinnedReposProps.repos,
},
},
},
});
}
private async refreshManageReposComponent(): Promise<void> {
await this.refreshPinnedRepoListItems();
this.refreshBranchesForPinnedRepos();
this.refreshUnpinnedRepoListItems();
}
private async refreshPinnedRepoListItems(): Promise<void> {
this.pinnedReposProps.repos = [];
try {
const response = await this.junoClient.getPinnedRepos(
this.props.explorer.notebookManager?.gitHubOAuthService.getTokenObservable()()?.scope
);
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
throw new Error(`Received HTTP ${response.status} when fetching pinned repos`);
}
if (response.data) {
const pinnedRepos = response.data.map(
(pinnedRepo) =>
({
key: GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name),
branches: pinnedRepo.branches,
repo: JunoUtils.toGitHubRepo(pinnedRepo),
} as RepoListItem)
);
this.pinnedReposProps.repos = pinnedRepos;
}
} catch (error) {
handleError(error, "GitHubReposPane/refreshPinnedReposListItems", "Failed to fetch pinned repos");
}
}
private refreshBranchesForPinnedRepos(): void {
this.pinnedReposProps.repos.map((item) => {
if (!this.branchesProps[item.key]) {
this.branchesProps[item.key] = {
branches: [],
lastPageInfo: undefined,
hasMore: true,
isLoading: true,
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo),
};
this.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
branchesProps: {
...this.state.gitHubReposState.reposListProps.branchesProps,
[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)]: this.branchesProps[item.key],
},
pinnedReposProps: {
repos: this.pinnedReposProps.repos,
},
unpinnedReposProps: {
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
repos: this.unpinnedReposProps.repos,
},
},
},
});
this.loadMoreBranches(item.repo);
} else {
if (this.isAddedRepo === false) {
this.setState({
gitHubReposState: {
...this.state.gitHubReposState,
reposListProps: {
...this.state.gitHubReposState.reposListProps,
pinnedReposProps: {
repos: this.pinnedReposProps.repos,
},
unpinnedReposProps: {
...this.state.gitHubReposState.reposListProps.unpinnedReposProps,
repos: this.unpinnedReposProps.repos,
},
},
},
});
}
}
});
this.isAddedRepo = false;
}
private async refreshUnpinnedRepoListItems(): Promise<void> {
this.allGitHubRepos = [];
this.allGitHubReposLastPageInfo = undefined;
this.unpinnedReposProps.repos = [];
this.loadMoreUnpinnedRepos();
}
private connectToGitHub(scope: string): void {
this.setState({
isExecuting: true,
});
TelemetryProcessor.trace(Action.NotebooksGitHubAuthorize, ActionModifiers.Mark, {
dataExplorerArea: Areas.Notebook,
scopesSelected: scope,
});
this.props.explorer.notebookManager?.gitHubOAuthService.startOAuth(scope);
}
render(): JSX.Element {
return (
<form className="panelFormWrapper">
{this.state.errorMessage && (
<PanelInfoErrorComponent
message={this.state.errorMessage}
messageType="error"
showErrorDetails={this.state.showErrorDetails}
openNotificationConsole={this.props.openNotificationConsole}
/>
)}
<div className="panelMainContent" style={ContentMainStyle}>
<GitHubReposComponent {...this.state.gitHubReposState} />
</div>
{this.state.isExecuting && <PanelLoadingScreen />}
</form>
);
}
}