import _ from "underscore"; import { Areas, HttpStatusCodes } from "../../Common/Constants"; import * as ViewModels from "../../Contracts/ViewModels"; 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 { JunoUtils } from "../../Utils/JunoUtils"; import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent"; import { GitHubReposComponent, GitHubReposComponentProps, RepoListItem } from "../Controls/GitHub/GitHubReposComponent"; import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter"; import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../Controls/GitHub/ReposListComponent"; import { ContextualPaneBase } from "./ContextualPaneBase"; import { handleError } from "../../Common/ErrorHandlingUtils"; interface GitHubReposPaneOptions extends ViewModels.PaneOptions { gitHubClient: GitHubClient; junoClient: JunoClient; } export class GitHubReposPane extends ContextualPaneBase { private static readonly PageSize = 30; private gitHubClient: GitHubClient; private junoClient: JunoClient; private branchesProps: Record; private pinnedReposProps: PinnedReposProps; private unpinnedReposProps: UnpinnedReposProps; private gitHubReposProps: GitHubReposComponentProps; private gitHubReposAdapter: GitHubReposComponentAdapter; private allGitHubRepos: IGitHubRepo[]; private allGitHubReposLastPageInfo?: IGitHubPageInfo; private pinnedReposUpdated: boolean; constructor(options: GitHubReposPaneOptions) { super(options); this.gitHubClient = options.gitHubClient; this.junoClient = options.junoClient; this.branchesProps = {}; this.pinnedReposProps = { repos: [], }; this.unpinnedReposProps = { repos: [], hasMore: true, isLoading: true, loadMore: (): Promise => this.loadMoreUnpinnedRepos(), }; this.gitHubReposProps = { showAuthorizeAccess: true, authorizeAccessProps: { scope: this.getOAuthScope(), authorizeAccess: (scope): void => this.connectToGitHub(scope), }, reposListProps: { branchesProps: this.branchesProps, pinnedReposProps: this.pinnedReposProps, unpinnedReposProps: this.unpinnedReposProps, pinRepo: (item): Promise => this.pinRepo(item), unpinRepo: (item): Promise => this.unpinRepo(item), }, addRepoProps: { container: this.container, getRepo: (owner, repo): Promise => this.getRepo(owner, repo), pinRepo: (item): Promise => this.pinRepo(item), }, resetConnection: (): void => this.setup(true), onOkClick: (): Promise => this.submit(), onCancelClick: (): void => this.cancel(), }; this.gitHubReposAdapter = new GitHubReposComponentAdapter(this.gitHubReposProps); this.allGitHubRepos = []; this.allGitHubReposLastPageInfo = undefined; this.pinnedReposUpdated = false; } public open(): void { this.resetData(); this.setup(); super.open(); } public async submit(): Promise { const pinnedReposUpdated = this.pinnedReposUpdated; const reposToPin: IPinnedRepo[] = this.pinnedReposProps.repos.map((repo) => JunoUtils.toPinnedRepo(repo)); // Submit resets data too super.submit(); 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 { // Reset cached branches this.branchesProps = {}; this.gitHubReposProps.reposListProps.branchesProps = this.branchesProps; // Reset cached pinned and unpinned repos this.pinnedReposProps.repos = []; this.unpinnedReposProps.repos = []; // Reset cached repos this.allGitHubRepos = []; this.allGitHubReposLastPageInfo = undefined; // Reset flags this.pinnedReposUpdated = false; this.unpinnedReposProps.hasMore = true; this.unpinnedReposProps.isLoading = true; this.triggerRender(); super.resetData(); } private getOAuthScope(): string { return ( this.container.notebookManager?.gitHubOAuthService.getTokenObservable()()?.scope || AuthorizeAccessComponent.Scopes.Public.key ); } private setup(forceShowConnectToGitHub = false): void { forceShowConnectToGitHub || !this.container.notebookManager?.gitHubOAuthService.isLoggedIn() ? this.setupForConnectToGitHub() : this.setupForManageRepos(); } private setupForConnectToGitHub(): void { this.gitHubReposProps.showAuthorizeAccess = true; this.gitHubReposProps.authorizeAccessProps.scope = this.getOAuthScope(); this.isExecuting(false); this.title(GitHubReposComponent.ConnectToGitHubTitle); // Used for telemetry this.triggerRender(); } private async setupForManageRepos(): Promise { this.gitHubReposProps.showAuthorizeAccess = false; this.isExecuting(false); this.title(GitHubReposComponent.ManageGitHubRepoTitle); // Used for telemetry TelemetryProcessor.trace(Action.NotebooksGitHubManageRepo, ActionModifiers.Mark, { databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience && this.container.defaultExperience(), dataExplorerArea: Areas.Notebook, }); this.triggerRender(); 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 { const branchesProps = this.branchesProps[GitHubUtils.toRepoFullName(repo.owner, repo.name)]; branchesProps.hasMore = true; branchesProps.isLoading = true; this.triggerRender(); try { const response = await this.gitHubClient.getBranchesAsync( repo.owner, repo.name, GitHubReposPane.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.triggerRender(); } private async loadMoreUnpinnedRepos(): Promise { this.unpinnedReposProps.isLoading = true; this.unpinnedReposProps.hasMore = true; this.triggerRender(); try { const response = await this.gitHubClient.getReposAsync( GitHubReposPane.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.triggerRender(); } private async getRepo(owner: string, repo: string): Promise { try { const response = await this.gitHubClient.getRepoAsync(owner, repo); if (response.status !== HttpStatusCodes.OK) { throw new Error(`Received HTTP ${response.status} when fetching repo`); } return response.data; } catch (error) { handleError(error, "GitHubReposPane/getRepo", "Failed to fetch repo"); return Promise.resolve(undefined); } } private async pinRepo(item: RepoListItem): Promise { this.pinnedReposUpdated = true; const initialReposLength = this.pinnedReposProps.repos.length; const existingRepo = _.find(this.pinnedReposProps.repos, (repo) => repo.key === item.key); if (existingRepo) { existingRepo.branches = item.branches; } else { this.pinnedReposProps.repos = [...this.pinnedReposProps.repos, item]; } this.unpinnedReposProps.repos = this.calculateUnpinnedRepos(); this.triggerRender(); if (this.pinnedReposProps.repos.length > initialReposLength) { this.refreshBranchesForPinnedRepos(); } } private async unpinRepo(item: RepoListItem): Promise { this.pinnedReposUpdated = true; this.pinnedReposProps.repos = this.pinnedReposProps.repos.filter((pinnedRepo) => pinnedRepo.key !== item.key); this.unpinnedReposProps.repos = this.calculateUnpinnedRepos(); this.triggerRender(); } private async refreshManageReposComponent(): Promise { await this.refreshPinnedRepoListItems(); this.refreshBranchesForPinnedRepos(); this.refreshUnpinnedRepoListItems(); } private async refreshPinnedRepoListItems(): Promise { this.pinnedReposProps.repos = []; this.triggerRender(); try { const response = await this.junoClient.getPinnedRepos( this.container.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; this.triggerRender(); } } 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 => this.loadMoreBranches(item.repo), }; this.loadMoreBranches(item.repo); } }); } private async refreshUnpinnedRepoListItems(): Promise { this.allGitHubRepos = []; this.allGitHubReposLastPageInfo = undefined; this.unpinnedReposProps.repos = []; this.loadMoreUnpinnedRepos(); } private connectToGitHub(scope: string): void { this.isExecuting(true); TelemetryProcessor.trace(Action.NotebooksGitHubAuthorize, ActionModifiers.Mark, { databaseAccountName: this.container.databaseAccount() && this.container.databaseAccount().name, defaultExperience: this.container.defaultExperience && this.container.defaultExperience(), dataExplorerArea: Areas.Notebook, scopesSelected: scope, }); this.container.notebookManager?.gitHubOAuthService.startOAuth(scope); } private triggerRender(): void { this.gitHubReposAdapter.triggerRender(); } }