diff --git a/docs/remove-notebooks-plan.md b/docs/remove-notebooks-plan.md index f2108425a..6070eaf3e 100644 --- a/docs/remove-notebooks-plan.md +++ b/docs/remove-notebooks-plan.md @@ -199,7 +199,34 @@ Schema Analyzer (see decision below), and remove all UI entry points that open n Schema Analyzer removal was pulled forward into Phase 2 because it is rendered by the nteract engine deleted there. No separate work remains for this phase. -### Phase 4 — Remove GitHub integration +### Phase 4 — Remove GitHub integration ✅ COMPLETED +> **Status:** Implemented on branch `users/jawelton/removenotebooks-phase3-062226`. Full +> verification sweep is green: `compile`, `compile:strict`, `lint` (0 errors), `format:check`, +> `test` (1900 passing), and `build:ci` (webpack) all pass. +> +> **Implementation notes / deviations:** +> - **Localization was a no-op:** there are **zero** GitHub keys in any locale +> `Resources.json`. The GitHub UI strings were hardcoded literals that were deleted with +> their components, so no resource-file edits were needed. +> - **`JunoClient` was trimmed, not deleted** (deletion stays in Phase 6). Removed only the +> GitHub-only members (`cachedPinnedRepos`, `subscribeToPinnedRepos`, `getPinnedRepos`, +> `updatePinnedRepos`, `deleteGitHubInfo`, `getGitHubToken`, `deleteAppAuthorization`, +> `getGitHubClientParams`, `IPinnedRepo`/`IPinnedBranch`) plus the now-orphaned private +> helpers `getNotebooksSubscriptionIdAccountUrl`/`getAccount`/`getSubscriptionId` and the +> `knockout` import. Kept the Schema (`requestSchema`/`getSchema`, used by `Database.tsx`) +> and gallery methods. Deleted `JunoClient.test.ts` (it only covered removed GitHub methods). +> - **`src/Utils/JunoUtils.ts` (+ test) was also deleted** — its `toPinnedRepo`/`toGitHubRepo` +> helpers only converted GitHub pinned-repo shapes and had no other consumers. +> - **`NotebookUtil.ts` (deferred to Phase 5) was edited here** to drop its `GitHubUtils` +> dependency: the dead `github://` content-URI branches in `getFilePath`/`getParentPath`/ +> `getName`/`replaceName` were removed (its `isNotebookFile` consumer in `ResourceTreeAdapter` +> is unaffected). `NotebookUtil.test.ts` lost its github-uri cases. +> - **`ConfigContext.ts`:** removed the GitHub-only config fields `GITHUB_CLIENT_ID`, +> `GITHUB_TEST_ENV_CLIENT_ID`, `GITHUB_CLIENT_SECRET` (their only consumers were deleted). +> - **Telemetry deferred (as planned):** the `Action.NotebooksGitHub*` enum members were left +> in `TelemetryConstants.ts` for the Phase 6 cleanup (enum-numbering-safe); only their +> now-deleted usages were removed. + - Delete `src/GitHub/`, `src/Explorer/Controls/GitHub/`, `src/Explorer/Panes/GitHubReposPanel/`, `src/Utils/GitHubUtils.ts`, `src/connectToGitHub.html`, and the `connectToGitHub` webpack entry & HTML plugin. diff --git a/src/ConfigContext.ts b/src/ConfigContext.ts index 969d6617f..76574a117 100644 --- a/src/ConfigContext.ts +++ b/src/ConfigContext.ts @@ -48,9 +48,6 @@ export interface ConfigContext { CASSANDRA_PROXY_ENDPOINT: string; PROXY_PATH?: string; JUNO_ENDPOINT: string; - GITHUB_CLIENT_ID: string; - GITHUB_TEST_ENV_CLIENT_ID: string; - GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it. isPhoenixEnabled: boolean; hostedExplorerURL: string; armAPIVersion?: string; @@ -95,8 +92,6 @@ let configContext: Readonly = { CATALOG_API_KEY: "", ARCADIA_ENDPOINT: "https://workspaceartifacts.projectarcadia.net", ARCADIA_LIVY_ENDPOINT_DNS_ZONE: "dev.azuresynapse.net", - GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1189306 - GITHUB_TEST_ENV_CLIENT_ID: "b63fc8cbf87fd3c6e2eb", // Registered OAuth app: https://github.com/organizations/AzureCosmosDBNotebooks/settings/applications/1777772 JUNO_ENDPOINT: JunoEndpoints.Prod, PORTAL_BACKEND_ENDPOINT: PortalBackendEndpoints.Prod, MONGO_PROXY_ENDPOINT: MongoProxyEndpoints.Prod, diff --git a/src/Explorer/Controls/GitHub/AddRepoComponent.tsx b/src/Explorer/Controls/GitHub/AddRepoComponent.tsx deleted file mode 100644 index 08f227ff1..000000000 --- a/src/Explorer/Controls/GitHub/AddRepoComponent.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "@fluentui/react"; -import * as React from "react"; -import * as Constants from "../../../Common/Constants"; -import * as UrlUtility from "../../../Common/UrlUtility"; -import { IGitHubRepo } from "../../../GitHub/GitHubClient"; -import { Action } from "../../../Shared/Telemetry/TelemetryConstants"; -import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor"; -import * as GitHubUtils from "../../../Utils/GitHubUtils"; -import Explorer from "../../Explorer"; -import { RepoListItem } from "./GitHubReposComponent"; -import { ChildrenMargin } from "./GitHubStyleConstants"; - -export interface AddRepoComponentProps { - container: Explorer; - getRepo: (owner: string, repo: string) => Promise; - pinRepo: (item: RepoListItem) => void; -} - -interface AddRepoComponentState { - textFieldValue: string; - textFieldErrorMessage: string; -} - -export class AddRepoComponent extends React.Component { - private static readonly DescriptionText = - "Don't see what you're looking for? Add your repo/branch, or any public repo (read-access only) by entering the URL: "; - private static readonly ButtonText = "Add"; - private static readonly TextFieldPlaceholder = "https://github.com/owner/repo/tree/branch"; - private static readonly TextFieldErrorMessage = "Invalid url"; - - constructor(props: AddRepoComponentProps) { - super(props); - - this.state = { - textFieldValue: "", - textFieldErrorMessage: undefined, - }; - } - - public render(): JSX.Element { - const textFieldProps: ITextFieldProps = { - placeholder: AddRepoComponent.TextFieldPlaceholder, - autoFocus: true, - value: this.state.textFieldValue, - errorMessage: this.state.textFieldErrorMessage, - onChange: this.onTextFieldChange, - }; - - const buttonProps: IButtonProps = { - text: AddRepoComponent.ButtonText, - ariaLabel: AddRepoComponent.ButtonText, - onClick: this.onAddRepoButtonClick, - }; - - return ( - <> -

{AddRepoComponent.DescriptionText}

- - - - ); - } - - private onTextFieldChange = ( - event: React.FormEvent, - newValue?: string, - ): void => { - this.setState({ - textFieldValue: newValue || "", - textFieldErrorMessage: undefined, - }); - }; - - private onAddRepoButtonClick = async (): Promise => { - const startKey: number = TelemetryProcessor.traceStart(Action.NotebooksGitHubManualRepoAdd, { - dataExplorerArea: Constants.Areas.Notebook, - }); - let enteredUrl = this.state.textFieldValue; - if (enteredUrl.indexOf("/tree/") === -1) { - enteredUrl = UrlUtility.createUri(enteredUrl, `tree/`); - } - - const repoInfo = GitHubUtils.fromRepoUri(enteredUrl); - if (repoInfo) { - this.setState({ - textFieldValue: "", - textFieldErrorMessage: undefined, - }); - - const repo = await this.props.getRepo(repoInfo.owner, repoInfo.repo); - if (repo) { - const item: RepoListItem = { - key: GitHubUtils.toRepoFullName(repo.owner, repo.name), - repo, - branches: repoInfo.branch ? [{ name: repoInfo.branch }] : [], - }; - - TelemetryProcessor.traceSuccess( - Action.NotebooksGitHubManualRepoAdd, - { - dataExplorerArea: Constants.Areas.Notebook, - }, - startKey, - ); - return this.props.pinRepo(item); - } - } - - this.setState({ - textFieldErrorMessage: AddRepoComponent.TextFieldErrorMessage, - }); - TelemetryProcessor.traceFailure( - Action.NotebooksGitHubManualRepoAdd, - { - dataExplorerArea: Constants.Areas.Notebook, - error: AddRepoComponent.TextFieldErrorMessage, - }, - startKey, - ); - }; -} diff --git a/src/Explorer/Controls/GitHub/AuthorizeAccessComponent.tsx b/src/Explorer/Controls/GitHub/AuthorizeAccessComponent.tsx deleted file mode 100644 index 2241bd081..000000000 --- a/src/Explorer/Controls/GitHub/AuthorizeAccessComponent.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { ChoiceGroup, IButtonProps, IChoiceGroupProps, PrimaryButton, IChoiceGroupOption } from "@fluentui/react"; -import * as React from "react"; -import { ChildrenMargin } from "./GitHubStyleConstants"; - -export interface AuthorizeAccessComponentProps { - scope: string; - authorizeAccess: (scope: string) => void; -} - -export interface AuthorizeAccessComponentState { - scope: string; -} - -export class AuthorizeAccessComponent extends React.Component< - AuthorizeAccessComponentProps, - AuthorizeAccessComponentState -> { - // Scopes supported by GitHub OAuth. We're only interested in ones which allow us access to repos. - // https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/ - public static readonly Scopes = { - Public: { - key: "public_repo", - text: "Public repos only", - }, - PublicAndPrivate: { - key: "repo", - text: "Public and private repos", - }, - }; - - private static readonly DescriptionPara1 = - "Connect your notebooks workspace to GitHub. You'll be able to view, edit, and run notebooks stored in your GitHub repositories in Data Explorer."; - private static readonly DescriptionPara2 = - "Complete setup by authorizing Azure Cosmos DB to access the repositories in your GitHub account: "; - private static readonly AuthorizeButtonText = "Authorize access"; - - private onChoiceGroupChange = (event: React.SyntheticEvent, option: IChoiceGroupOption): void => - this.setState({ - scope: option.key, - }); - - private onButtonClick = (): void => this.props.authorizeAccess(this.state.scope); - - constructor(props: AuthorizeAccessComponentProps) { - super(props); - - this.state = { - scope: this.props.scope, - }; - } - - public render(): JSX.Element { - const choiceGroupProps: IChoiceGroupProps = { - options: [ - { - key: AuthorizeAccessComponent.Scopes.Public.key, - text: AuthorizeAccessComponent.Scopes.Public.text, - ariaLabel: AuthorizeAccessComponent.Scopes.Public.text, - }, - { - key: AuthorizeAccessComponent.Scopes.PublicAndPrivate.key, - text: AuthorizeAccessComponent.Scopes.PublicAndPrivate.text, - ariaLabel: AuthorizeAccessComponent.Scopes.PublicAndPrivate.text, - }, - ], - selectedKey: this.state.scope, - onChange: this.onChoiceGroupChange, - }; - - const buttonProps: IButtonProps = { - text: AuthorizeAccessComponent.AuthorizeButtonText, - ariaLabel: AuthorizeAccessComponent.AuthorizeButtonText, - onClick: this.onButtonClick, - }; - - return ( - <> -

{AuthorizeAccessComponent.DescriptionPara1}

-

{AuthorizeAccessComponent.DescriptionPara2}

- - - - ); - } -} diff --git a/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx b/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx deleted file mode 100644 index d6b28775d..000000000 --- a/src/Explorer/Controls/GitHub/GitHubReposComponent.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { DefaultButton, IButtonProps, Link, PrimaryButton } from "@fluentui/react"; -import * as React from "react"; -import { IGitHubBranch, IGitHubRepo } from "../../../GitHub/GitHubClient"; -import { AddRepoComponent, AddRepoComponentProps } from "./AddRepoComponent"; -import { AuthorizeAccessComponent, AuthorizeAccessComponentProps } from "./AuthorizeAccessComponent"; -import { ButtonsFooterStyle, ChildrenMargin, ContentFooterStyle } from "./GitHubStyleConstants"; -import { ReposListComponent, ReposListComponentProps } from "./ReposListComponent"; - -export interface GitHubReposComponentProps { - showAuthorizeAccess: boolean; - authorizeAccessProps: AuthorizeAccessComponentProps; - reposListProps: ReposListComponentProps; - addRepoProps: AddRepoComponentProps; - resetConnection: () => void; - onOkClick: () => void; - onCancelClick: () => void; -} - -export interface RepoListItem { - key: string; - repo: IGitHubRepo; - branches: IGitHubBranch[]; -} - -export class GitHubReposComponent extends React.Component { - private static readonly ManageGitHubRepoDescription = - "Select your GitHub repos and branch(es) to pin to your notebooks workspace."; - private static readonly ManageGitHubRepoResetConnection = "View or change your GitHub authorization settings."; - private static readonly OKButtonText = "OK"; - private static readonly CancelButtonText = "Cancel"; - - public render(): JSX.Element { - const content: JSX.Element = this.props.showAuthorizeAccess ? ( - - ) : ( - <> -

{GitHubReposComponent.ManageGitHubRepoDescription}

- - {GitHubReposComponent.ManageGitHubRepoResetConnection} - - - - ); - - const okProps: IButtonProps = { - text: GitHubReposComponent.OKButtonText, - ariaLabel: GitHubReposComponent.OKButtonText, - onClick: this.props.onOkClick, - }; - - const cancelProps: IButtonProps = { - text: GitHubReposComponent.CancelButtonText, - ariaLabel: GitHubReposComponent.CancelButtonText, - onClick: this.props.onCancelClick, - }; - - return ( - <> -
{content}
- {!this.props.showAuthorizeAccess && ( - <> -
- -
-
- - -
- - )} - - ); - } -} diff --git a/src/Explorer/Controls/GitHub/GitHubStyleConstants.ts b/src/Explorer/Controls/GitHub/GitHubStyleConstants.ts deleted file mode 100644 index 8018b866b..000000000 --- a/src/Explorer/Controls/GitHub/GitHubStyleConstants.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { - ICheckboxStyleProps, - ICheckboxStyles, - IDropdownStyleProps, - IDropdownStyles, - IStyleFunctionOrObject, -} from "@fluentui/react"; - -export const ButtonsFooterStyle: React.CSSProperties = { - paddingTop: 14, - height: "auto", - borderTop: "2px solid lightGray", -}; - -export const ContentFooterStyle: React.CSSProperties = { - paddingTop: "10px", - height: "auto", - borderTop: "2px solid lightGray", -}; - -export const ChildrenMargin = 10; -export const FontSize = 12; - -export const ReposListCheckboxStyles: IStyleFunctionOrObject = { - label: { - margin: 0, - padding: "2 0 2 0", - }, - text: { - fontSize: FontSize, - }, -}; - -export const BranchesDropdownCheckboxStyles: IStyleFunctionOrObject = { - label: { - margin: 0, - padding: 0, - fontSize: FontSize, - }, - root: { - padding: 0, - }, - text: { - fontSize: FontSize, - }, -}; - -export const BranchesDropdownStyles: IStyleFunctionOrObject = { - title: { - fontSize: FontSize, - }, -}; - -export const BranchesDropdownOptionContainerStyle: React.CSSProperties = { - padding: 8, -}; - -export const ContentMainStyle: React.CSSProperties = { - display: "flex", - flexDirection: "column", -}; - -export const ReposListRepoColumnMinWidth = 192; -export const ReposListBranchesColumnWidth = 116; -export const BranchesDropdownWidth = 200; diff --git a/src/Explorer/Controls/GitHub/ReposListComponent.tsx b/src/Explorer/Controls/GitHub/ReposListComponent.tsx deleted file mode 100644 index 7614e95c4..000000000 --- a/src/Explorer/Controls/GitHub/ReposListComponent.tsx +++ /dev/null @@ -1,309 +0,0 @@ -import { - Checkbox, - DetailsList, - DetailsRow, - Dropdown, - ICheckboxProps, - IDetailsFooterProps, - IDetailsListProps, - IDetailsRowBaseProps, - IDropdown, - IDropdownOption, - IDropdownProps, - ILinkProps, - ISelectableDroppableTextProps, - Link, - ResponsiveMode, - SelectionMode, - Text, -} from "@fluentui/react"; -import * as React from "react"; -import { IGitHubBranch, IGitHubPageInfo } from "../../../GitHub/GitHubClient"; -import * as GitHubUtils from "../../../Utils/GitHubUtils"; -import { RepoListItem } from "./GitHubReposComponent"; -import { - BranchesDropdownCheckboxStyles, - BranchesDropdownOptionContainerStyle, - BranchesDropdownStyles, - BranchesDropdownWidth, - ReposListBranchesColumnWidth, - ReposListCheckboxStyles, - ReposListRepoColumnMinWidth, -} from "./GitHubStyleConstants"; - -export interface ReposListComponentProps { - branchesProps: Record; // key'd by repo key - pinnedReposProps: PinnedReposProps; - unpinnedReposProps: UnpinnedReposProps; - pinRepo: (repo: RepoListItem) => void; - unpinRepo: (repo: RepoListItem) => void; -} - -export interface BranchesProps { - branches: IGitHubBranch[]; - lastPageInfo?: IGitHubPageInfo; - hasMore: boolean; - isLoading: boolean; - defaultBranchName: string; - loadMore: () => void; -} - -export interface PinnedReposProps { - repos: RepoListItem[]; -} - -export interface UnpinnedReposProps { - repos: RepoListItem[]; - hasMore: boolean; - isLoading: boolean; - loadMore: () => void; -} - -export class ReposListComponent extends React.Component { - private static readonly PinnedReposColumnName = "Pinned repos"; - private static readonly UnpinnedReposColumnName = "Unpinned repos"; - private static readonly BranchesColumnName = "Branches"; - private static readonly LoadingText = "Loading..."; - private static readonly LoadMoreText = "Load more"; - private static readonly DefaultBranchNames = "master/main"; - private static readonly FooterIndex = -1; - - public render(): JSX.Element { - const pinnedReposListProps: IDetailsListProps = { - styles: { - contentWrapper: { - height: this.props.pinnedReposProps.repos.length ? undefined : 0, - }, - }, - items: this.props.pinnedReposProps.repos, - getKey: ReposListComponent.getKey, - selectionMode: SelectionMode.none, - compact: true, - columns: [ - { - key: ReposListComponent.PinnedReposColumnName, - name: ReposListComponent.PinnedReposColumnName, - ariaLabel: ReposListComponent.PinnedReposColumnName, - minWidth: ReposListRepoColumnMinWidth, - onRender: this.onRenderPinnedReposColumnItem, - }, - { - key: ReposListComponent.BranchesColumnName, - name: ReposListComponent.BranchesColumnName, - ariaLabel: ReposListComponent.BranchesColumnName, - minWidth: ReposListBranchesColumnWidth, - maxWidth: ReposListBranchesColumnWidth, - onRender: this.onRenderPinnedReposBranchesColumnItem, - }, - ], - onRenderDetailsFooter: this.props.pinnedReposProps.repos.length ? undefined : this.onRenderReposFooter, - }; - - const unpinnedReposListProps: IDetailsListProps = { - items: this.props.unpinnedReposProps.repos, - getKey: ReposListComponent.getKey, - selectionMode: SelectionMode.none, - compact: true, - columns: [ - { - key: ReposListComponent.UnpinnedReposColumnName, - name: ReposListComponent.UnpinnedReposColumnName, - ariaLabel: ReposListComponent.UnpinnedReposColumnName, - minWidth: ReposListRepoColumnMinWidth, - onRender: this.onRenderUnpinnedReposColumnItem, - }, - { - key: ReposListComponent.BranchesColumnName, - name: ReposListComponent.BranchesColumnName, - ariaLabel: ReposListComponent.BranchesColumnName, - minWidth: ReposListBranchesColumnWidth, - maxWidth: ReposListBranchesColumnWidth, - onRender: this.onRenderUnpinnedReposBranchesColumnItem, - }, - ], - onRenderDetailsFooter: - this.props.unpinnedReposProps.isLoading || this.props.unpinnedReposProps.hasMore - ? this.onRenderReposFooter - : undefined, - }; - - return ( - <> - - - - ); - } - - private onRenderPinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => { - if (index === ReposListComponent.FooterIndex) { - return None; - } - - const checkboxProps: ICheckboxProps = { - ...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)), - styles: ReposListCheckboxStyles, - defaultChecked: true, - onChange: () => this.props.unpinRepo(item), - }; - - return ; - }; - - private onRenderPinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => { - if (index === ReposListComponent.FooterIndex) { - return <>; - } - - const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)]; - if (item.branches.length === 0 && branchesProps.defaultBranchName) { - item.branches = [{ name: branchesProps.defaultBranchName }]; - } - - const options: IDropdownOption[] = branchesProps.branches.map((branch) => ({ - key: branch.name, - text: branch.name, - data: item, - disabled: item.branches.length === 1 && branch.name === item.branches[0].name, - selected: item.branches.findIndex((element) => element.name === branch.name) !== -1, - })); - - if (branchesProps.hasMore || branchesProps.isLoading) { - const text = branchesProps.isLoading ? ReposListComponent.LoadingText : ReposListComponent.LoadMoreText; - options.push({ - key: text, - text, - data: item, - index: ReposListComponent.FooterIndex, - }); - } - - const dropdownProps: IDropdownProps = { - styles: BranchesDropdownStyles, - dropdownWidth: BranchesDropdownWidth, - responsiveMode: ResponsiveMode.large, - options, - onRenderList: this.onRenderBranchesDropdownList, - }; - - if (item.branches.length === 1) { - dropdownProps.placeholder = item.branches[0].name; - } else if (item.branches.length > 1) { - dropdownProps.placeholder = `${item.branches.length} branches`; - } - - return ; - }; - - private onRenderUnpinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => { - if (index === ReposListComponent.FooterIndex) { - return <>; - } - - const dropdownProps: IDropdownProps = { - styles: BranchesDropdownStyles, - options: [], - placeholder: ReposListComponent.DefaultBranchNames, - disabled: true, - }; - - return ; - }; - - private onRenderBranchesDropdownList = ( - props: ISelectableDroppableTextProps, - ): JSX.Element => { - const renderedList: JSX.Element[] = []; - props.options.forEach((option: IDropdownOption) => { - const item = ( -
- {this.onRenderPinnedReposBranchesDropdownOption(option)} -
- ); - renderedList.push(item); - }); - - return <>{renderedList}; - }; - - private onRenderPinnedReposBranchesDropdownOption(option: IDropdownOption): JSX.Element { - const item: RepoListItem = option.data; - const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)]; - - if (option.index === ReposListComponent.FooterIndex) { - const linkProps: ILinkProps = { - disabled: branchesProps.isLoading, - onClick: branchesProps.loadMore, - }; - - return {option.text}; - } - - const checkboxProps: ICheckboxProps = { - ...ReposListComponent.getCheckboxPropsForLabel(option.text), - styles: BranchesDropdownCheckboxStyles, - defaultChecked: option.selected, - disabled: option.disabled, - onChange: (event, checked) => { - const repoListItem = { ...item }; - const branch: IGitHubBranch = { name: option.text }; - repoListItem.branches = repoListItem.branches.filter((element) => element.name !== branch.name); - if (checked) { - repoListItem.branches.push(branch); - } - - this.props.pinRepo(repoListItem); - }, - }; - - return ; - } - - private onRenderUnpinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => { - if (index === ReposListComponent.FooterIndex) { - const linkProps: ILinkProps = { - disabled: this.props.unpinnedReposProps.isLoading, - onClick: this.props.unpinnedReposProps.loadMore, - }; - - const linkText = this.props.unpinnedReposProps.isLoading - ? ReposListComponent.LoadingText - : ReposListComponent.LoadMoreText; - return {linkText}; - } - - const checkboxProps: ICheckboxProps = { - ...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)), - styles: ReposListCheckboxStyles, - onChange: () => { - const repoListItem = { ...item }; - repoListItem.branches = []; - this.props.pinRepo(repoListItem); - }, - }; - - return ; - }; - - private onRenderReposFooter = (detailsFooterProps: IDetailsFooterProps): JSX.Element => { - const props: IDetailsRowBaseProps = { - ...detailsFooterProps, - item: {}, - itemIndex: ReposListComponent.FooterIndex, - }; - - return ; - }; - - private static getCheckboxPropsForLabel(label: string): ICheckboxProps { - return { - label, - title: label, - ariaLabel: label, - }; - } - - private static getKey(item: RepoListItem): string { - return item.key; - } -} diff --git a/src/Explorer/Explorer.tsx b/src/Explorer/Explorer.tsx index e554bbfaf..bcacb8d65 100644 --- a/src/Explorer/Explorer.tsx +++ b/src/Explorer/Explorer.tsx @@ -33,7 +33,6 @@ import * as DataModels from "../Contracts/DataModels"; import { ContainerConnectionInfo, IPhoenixServiceInfo, IProvisionData, IResponse } from "../Contracts/DataModels"; import * as ViewModels from "../Contracts/ViewModels"; import { UploadDetailsRecord } from "../Contracts/ViewModels"; -import { GitHubOAuthService } from "../GitHub/GitHubOAuthService"; import MetricScenario from "../Metrics/MetricEvents"; import { ApplicationMetricPhase } from "../Metrics/ScenarioConfig"; import { scenarioMonitor } from "../Metrics/ScenarioMonitor"; @@ -79,8 +78,6 @@ export default class Explorer { // Tabs public isTabsContentExpanded: ko.Observable; - public gitHubOAuthService: GitHubOAuthService; - // Notebooks public notebookManager?: NotebookManager; diff --git a/src/Explorer/Notebook/NotebookManager.tsx b/src/Explorer/Notebook/NotebookManager.tsx index 1be3cb096..175c76061 100644 --- a/src/Explorer/Notebook/NotebookManager.tsx +++ b/src/Explorer/Notebook/NotebookManager.tsx @@ -2,21 +2,11 @@ * Contains all notebook related stuff meant to be dynamically loaded by explorer */ -import React from "react"; -import { HttpStatusCodes } from "../../Common/Constants"; -import { getErrorMessage } from "../../Common/ErrorHandlingUtils"; -import * as Logger from "../../Common/Logger"; -import { GitHubClient } from "../../GitHub/GitHubClient"; -import { GitHubOAuthService } from "../../GitHub/GitHubOAuthService"; -import { useSidePanel } from "../../hooks/useSidePanel"; import { JunoClient } from "../../Juno/JunoClient"; import { userContext } from "../../UserContext"; -import { useDialog } from "../Controls/Dialog"; import Explorer from "../Explorer"; -import { GitHubReposPanel } from "../Panes/GitHubReposPanel/GitHubReposPanel"; import { ResourceTreeAdapter } from "../Tree/ResourceTreeAdapter"; import { NotebookContainerClient } from "./NotebookContainerClient"; -import { useNotebook } from "./useNotebook"; export interface NotebookManagerOptions { container: Explorer; @@ -31,85 +21,12 @@ export default class NotebookManager { public notebookClient: NotebookContainerClient; - public gitHubOAuthService: GitHubOAuthService; - public gitHubClient: GitHubClient; - public initialize(params: NotebookManagerOptions): void { this.params = params; this.junoClient = new JunoClient(); - this.gitHubOAuthService = new GitHubOAuthService(this.junoClient); - this.gitHubClient = new GitHubClient(this.onGitHubClientError); - this.notebookClient = new NotebookContainerClient(() => this.params.container.initNotebooks(userContext?.databaseAccount), ); - - this.gitHubOAuthService.getTokenObservable().subscribe((token) => { - this.gitHubClient.setToken(token?.access_token); - if (this?.gitHubOAuthService.isLoggedIn()) { - useSidePanel.getState().closeSidePanel(); - setTimeout(() => { - useSidePanel - .getState() - .openSidePanel( - "Manage GitHub settings", - , - ); - }, 200); - } - - this.params.refreshCommandBarButtons(); - this.params.refreshNotebookList(); - }); - - this.junoClient.subscribeToPinnedRepos((pinnedRepos) => { - this.params.resourceTree.initializeGitHubRepos(pinnedRepos); - this.params.resourceTree.triggerRender(); - useNotebook.getState().initializeGitHubRepos(pinnedRepos); - }); - this.refreshPinnedRepos(); } - - public refreshPinnedRepos(): void { - const token = this.gitHubOAuthService.getTokenObservable()(); - if (token) { - this.junoClient.getPinnedRepos(token.scope); - } - } - - // Octokit's error handler uses any - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private onGitHubClientError = (error: any): void => { - Logger.logError(getErrorMessage(error), "NotebookManager/onGitHubClientError"); - - if (error.status === HttpStatusCodes.Unauthorized) { - this.gitHubOAuthService.resetToken(); - - useDialog - .getState() - .showOkCancelModalDialog( - undefined, - "Cosmos DB cannot access your Github account anymore. Please connect to GitHub again.", - "Connect to GitHub", - () => - useSidePanel - .getState() - .openSidePanel( - "Connect to GitHub", - , - ), - "Cancel", - undefined, - ); - } - }; } diff --git a/src/Explorer/Notebook/NotebookUtil.test.ts b/src/Explorer/Notebook/NotebookUtil.test.ts index 73b7f465f..97ff4514a 100644 --- a/src/Explorer/Notebook/NotebookUtil.test.ts +++ b/src/Explorer/Notebook/NotebookUtil.test.ts @@ -1,4 +1,3 @@ -import * as GitHubUtils from "../../Utils/GitHubUtils"; import { NotebookUtil } from "./NotebookUtil"; const fileName = "file"; @@ -6,9 +5,6 @@ const notebookName = "file.ipynb"; const folderPath = "folder"; 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 gitHubNotebookUri = GitHubUtils.toContentUri("owner", "repo", "branch", notebookPath); describe("NotebookUtil", () => { describe("isNotebookFile", () => { @@ -16,31 +12,18 @@ describe("NotebookUtil", () => { expect(NotebookUtil.isNotebookFile(filePath)).toBeFalsy(); expect(NotebookUtil.isNotebookFile(notebookPath)).toBeTruthy(); }); - - it("works for github file uris", () => { - expect(NotebookUtil.isNotebookFile(gitHubFileUri)).toBeFalsy(); - expect(NotebookUtil.isNotebookFile(gitHubNotebookUri)).toBeTruthy(); - }); }); 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", () => { @@ -48,11 +31,6 @@ describe("NotebookUtil", () => { expect(NotebookUtil.getName(filePath)).toEqual(fileName); expect(NotebookUtil.getName(notebookPath)).toEqual(notebookName); }); - - it("works for github file uris", () => { - expect(NotebookUtil.getName(gitHubFileUri)).toEqual(fileName); - expect(NotebookUtil.getName(gitHubNotebookUri)).toEqual(notebookName); - }); }); describe("replaceName", () => { @@ -60,12 +38,5 @@ describe("NotebookUtil", () => { expect(NotebookUtil.replaceName(filePath, "newName")).toEqual(filePath.replace(fileName, "newName")); expect(NotebookUtil.replaceName(notebookPath, "newName")).toEqual(notebookPath.replace(notebookName, "newName")); }); - - it("works for github file uris", () => { - expect(NotebookUtil.replaceName(gitHubFileUri, "newName")).toEqual(gitHubFileUri.replace(fileName, "newName")); - expect(NotebookUtil.replaceName(gitHubNotebookUri, "newName")).toEqual( - gitHubNotebookUri.replace(notebookName, "newName"), - ); - }); }); }); diff --git a/src/Explorer/Notebook/NotebookUtil.ts b/src/Explorer/Notebook/NotebookUtil.ts index 26e801e72..d8e752f3d 100644 --- a/src/Explorer/Notebook/NotebookUtil.ts +++ b/src/Explorer/Notebook/NotebookUtil.ts @@ -1,4 +1,3 @@ -import * as GitHubUtils from "../../Utils/GitHubUtils"; import * as StringUtils from "../../Utils/StringUtils"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; @@ -51,36 +50,12 @@ 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 @@ -91,27 +66,10 @@ export class NotebookUtil { } public static getName(path: string): undefined | string { - let relativePath: string = path; - const contentInfo = GitHubUtils.fromContentUri(path); - if (contentInfo) { - relativePath = contentInfo.path; - } - - return relativePath.split("/").pop(); + return path.split("/").pop(); } public static replaceName(path: string, newName: string): string { - const contentInfo = GitHubUtils.fromContentUri(path); - if (contentInfo) { - const contentName = contentInfo.path.split("/").pop(); - if (!contentName) { - throw new Error(`Failed to extract name from github path ${contentInfo.path}`); - } - - const basePath = contentInfo.path.split(contentName).shift(); - return GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, `${basePath}${newName}`); - } - const contentName = path.split("/").pop(); if (!contentName) { throw new Error(`Failed to extract name from path ${path}`); diff --git a/src/Explorer/Notebook/useNotebook.ts b/src/Explorer/Notebook/useNotebook.ts index c59241371..608358cf7 100644 --- a/src/Explorer/Notebook/useNotebook.ts +++ b/src/Explorer/Notebook/useNotebook.ts @@ -8,15 +8,12 @@ import * as Logger from "../../Common/Logger"; import { configContext } from "../../ConfigContext"; import * as DataModels from "../../Contracts/DataModels"; import { ContainerConnectionInfo, ContainerInfo } from "../../Contracts/DataModels"; -import { IPinnedRepo } from "../../Juno/JunoClient"; import { Action } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { getAuthorizationHeader } from "../../Utils/AuthorizationUtils"; -import * as GitHubUtils from "../../Utils/GitHubUtils"; import { useTabs } from "../../hooks/useTabs"; import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem"; -import NotebookManager from "./NotebookManager"; interface NotebookState { isNotebookEnabled: boolean; @@ -29,7 +26,6 @@ interface NotebookState { notebookBasePath: string; isInitializingNotebooks: boolean; myNotebooksContentRoot: NotebookContentItem; - gitHubNotebooksContentRoot: NotebookContentItem; galleryContentRoot: NotebookContentItem; connectionInfo: ContainerConnectionInfo; notebookFolderName: string; @@ -49,11 +45,10 @@ interface NotebookState { setNotebookFolderName: (notebookFolderName: string) => void; refreshNotebooksEnabledStateForAccount: () => Promise; findItem: (root: NotebookContentItem, item: NotebookContentItem) => NotebookContentItem; - insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean) => void; - updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; - deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean) => void; - initializeNotebooksTree: (notebookManager: NotebookManager) => Promise; - initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]) => void; + insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem) => void; + updateNotebookItem: (item: NotebookContentItem) => void; + deleteNotebookItem: (item: NotebookContentItem) => void; + initializeNotebooksTree: () => Promise; setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => void; setIsAllocating: (isAllocating: boolean) => void; resetContainerConnection: (connectionStatus: ContainerConnectionInfo) => void; @@ -83,7 +78,6 @@ export const useNotebook: UseStore = create((set, get) => ({ notebookBasePath: Constants.Notebook.defaultBasePath, isInitializingNotebooks: false, myNotebooksContentRoot: undefined, - gitHubNotebooksContentRoot: undefined, galleryContentRoot: undefined, connectionInfo: { status: ConnectionStatusType.Connect, @@ -183,8 +177,8 @@ export const useNotebook: UseStore = create((set, get) => ({ return undefined; }, - insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem, isGithubTree?: boolean): void => { - const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); + insertNotebookItem: (parent: NotebookContentItem, item: NotebookContentItem): void => { + const root = cloneDeep(get().myNotebooksContentRoot); const parentItem = get().findItem(root, parent); item.parent = parentItem; if (parentItem.children) { @@ -192,23 +186,23 @@ export const useNotebook: UseStore = create((set, get) => ({ } else { parentItem.children = [item]; } - isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); + set({ myNotebooksContentRoot: root }); }, - updateNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => { - const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); + updateNotebookItem: (item: NotebookContentItem): void => { + const root = cloneDeep(get().myNotebooksContentRoot); const parentItem = get().findItem(root, item.parent); parentItem.children = parentItem.children.filter((child) => child.path !== item.path); parentItem.children.push(item); item.parent = parentItem; - isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); + set({ myNotebooksContentRoot: root }); }, - deleteNotebookItem: (item: NotebookContentItem, isGithubTree?: boolean): void => { - const root = isGithubTree ? cloneDeep(get().gitHubNotebooksContentRoot) : cloneDeep(get().myNotebooksContentRoot); + deleteNotebookItem: (item: NotebookContentItem): void => { + const root = cloneDeep(get().myNotebooksContentRoot); const parentItem = get().findItem(root, item.parent); parentItem.children = parentItem.children.filter((child) => child.path !== item.path); - isGithubTree ? set({ gitHubNotebooksContentRoot: root }) : set({ myNotebooksContentRoot: root }); + set({ myNotebooksContentRoot: root }); }, - initializeNotebooksTree: async (notebookManager: NotebookManager): Promise => { + initializeNotebooksTree: async (): Promise => { const notebookFolderName = get().isPhoenixNotebooks ? "Temporary Notebooks" : "My Notebooks"; set({ notebookFolderName }); const myNotebooksContentRoot = { @@ -221,49 +215,12 @@ export const useNotebook: UseStore = create((set, get) => ({ path: "Gallery", type: NotebookContentItemType.File, }; - const gitHubNotebooksContentRoot = notebookManager?.gitHubOAuthService?.isLoggedIn() - ? { - name: "GitHub repos", - path: "PsuedoDir", - type: NotebookContentItemType.Directory, - } - : undefined; set({ myNotebooksContentRoot, galleryContentRoot, - gitHubNotebooksContentRoot, }); }, - initializeGitHubRepos: (pinnedRepos: IPinnedRepo[]): void => { - const gitHubNotebooksContentRoot = cloneDeep(get().gitHubNotebooksContentRoot); - if (gitHubNotebooksContentRoot) { - gitHubNotebooksContentRoot.children = []; - pinnedRepos?.forEach((pinnedRepo) => { - const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); - const repoTreeItem: NotebookContentItem = { - name: repoFullName, - path: "PsuedoDir", - type: NotebookContentItemType.Directory, - children: [], - parent: gitHubNotebooksContentRoot, - }; - - pinnedRepo.branches.forEach((branch) => { - repoTreeItem.children.push({ - name: branch.name, - path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), - type: NotebookContentItemType.Directory, - parent: repoTreeItem, - }); - }); - - gitHubNotebooksContentRoot.children.push(repoTreeItem); - }); - - set({ gitHubNotebooksContentRoot }); - } - }, setConnectionInfo: (connectionInfo: ContainerConnectionInfo) => set({ connectionInfo }), setIsAllocating: (isAllocating: boolean) => set({ isAllocating }), resetContainerConnection: (connectionStatus: ContainerConnectionInfo): void => { diff --git a/src/Explorer/Panes/GitHubReposPanel/GitHubReposPanel.test.tsx b/src/Explorer/Panes/GitHubReposPanel/GitHubReposPanel.test.tsx deleted file mode 100644 index 3d12139f5..000000000 --- a/src/Explorer/Panes/GitHubReposPanel/GitHubReposPanel.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { shallow } from "enzyme"; -import React from "react"; -import { GitHubClient } from "../../../GitHub/GitHubClient"; -import { JunoClient } from "../../../Juno/JunoClient"; -import Explorer from "../../Explorer"; -import { GitHubReposPanel } from "./GitHubReposPanel"; -const props = { - explorer: new Explorer(), - closePanel: (): void => undefined, - gitHubClientProp: new GitHubClient((): void => undefined), - junoClientProp: new JunoClient(), - openNotificationConsole: (): void => undefined, -}; -describe("GitHub Repos Panel", () => { - it("should render Default properly", () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/src/Explorer/Panes/GitHubReposPanel/GitHubReposPanel.tsx b/src/Explorer/Panes/GitHubReposPanel/GitHubReposPanel.tsx deleted file mode 100644 index 447c5c9f1..000000000 --- a/src/Explorer/Panes/GitHubReposPanel/GitHubReposPanel.tsx +++ /dev/null @@ -1,466 +0,0 @@ -import React from "react"; -import { Areas, HttpStatusCodes } from "../../../Common/Constants"; -import { handleError } from "../../../Common/ErrorHandlingUtils"; -import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../../GitHub/GitHubClient"; -import { useSidePanel } from "../../../hooks/useSidePanel"; -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; - gitHubClientProp: GitHubClient; - junoClientProp: JunoClient; -} - -interface IGitHubReposPanelState { - showAuthorizationAcessState: boolean; - isExecuting: boolean; - errorMessage: string; - showErrorDetails: boolean; - gitHubReposState: GitHubReposComponentProps; -} -export class GitHubReposPanel extends React.Component { - private static readonly PageSize = 30; - private static readonly MasterBranchName = "master"; - private static readonly MainBranchName = "main"; - - private isAddedRepo = false; - private gitHubClient: GitHubClient; - private junoClient: JunoClient; - - private branchesProps: Record; - 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 => 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 => this.pinRepo(item), - unpinRepo: (item): Promise => this.unpinRepo(item), - }, - addRepoProps: { - container: this.props.explorer, - 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 => useSidePanel.getState().closeSidePanel(), - }, - }; - 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 { - 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`); - } - - this.props.explorer.notebookManager?.refreshPinnedRepos(); - } catch (error) { - handleError(error, "GitHubReposPane/submit", "Failed to save pinned repos"); - } - } - useSidePanel.getState().closeSidePanel(); - } - - 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(forceShowConnectToGitHub) - : this.setupForManageRepos(); - } - - private setupForConnectToGitHub(forceShowConnectToGitHub: boolean): void { - if (forceShowConnectToGitHub) { - const newState = { ...this.state.gitHubReposState }; - newState.showAuthorizeAccess = forceShowConnectToGitHub; - this.setState({ - gitHubReposState: newState, - }); - } - this.setState({ - isExecuting: false, - }); - } - - private async setupForManageRepos(): Promise { - 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 { - 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; - branchesProps.defaultBranchName = branchesProps.branches[0].name; - const defaultbranchName = branchesProps.branches.find( - (branch) => - branch.name === GitHubReposPanel.MasterBranchName || branch.name === GitHubReposPanel.MainBranchName, - )?.name; - if (defaultbranchName) { - branchesProps.defaultBranchName = defaultbranchName; - } - } - } 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 { - 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 { - 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 { - 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; - this.setState({ - gitHubReposState: { - ...this.state.gitHubReposState, - reposListProps: { - ...this.state.gitHubReposState.reposListProps, - pinnedReposProps: { - repos: this.pinnedReposProps.repos, - }, - }, - }, - }); - } 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 { - 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 { - await this.refreshPinnedRepoListItems(); - this.refreshBranchesForPinnedRepos(); - this.refreshUnpinnedRepoListItems(); - } - - private async refreshPinnedRepoListItems(): Promise { - 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, - defaultBranchName: undefined, - loadMore: (): Promise => this.loadMoreBranches(item.repo), - }; - this.loadMoreBranches(item.repo); - } - }); - - this.setState({ - gitHubReposState: { - ...this.state.gitHubReposState, - reposListProps: { - ...this.state.gitHubReposState.reposListProps, - branchesProps: { - ...this.branchesProps, - }, - pinnedReposProps: { - repos: this.pinnedReposProps.repos, - }, - unpinnedReposProps: { - ...this.state.gitHubReposState.reposListProps.unpinnedReposProps, - repos: this.unpinnedReposProps.repos, - }, - }, - }, - }); - this.isAddedRepo = false; - } - - private async refreshUnpinnedRepoListItems(): Promise { - 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 ( -
- {this.state.errorMessage && ( - - )} -
- -
- - {this.state.isExecuting && } - - ); - } -} diff --git a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap b/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap deleted file mode 100644 index 70160c878..000000000 --- a/src/Explorer/Panes/GitHubReposPanel/__snapshots__/GitHubReposPanel.test.tsx.snap +++ /dev/null @@ -1,77 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`GitHub Repos Panel should render Default properly 1`] = ` -
-
- -
-
-`; diff --git a/src/Explorer/Tree/ResourceTree.tsx b/src/Explorer/Tree/ResourceTree.tsx index 4e240e5bd..dfcf53422 100644 --- a/src/Explorer/Tree/ResourceTree.tsx +++ b/src/Explorer/Tree/ResourceTree.tsx @@ -24,7 +24,6 @@ import Explorer from "../Explorer"; import { useNotebook } from "../Notebook/useNotebook"; export const MyNotebooksTitle = "My Notebooks"; -export const GitHubReposTitle = "GitHub repos"; interface ResourceTreeProps { explorer: Explorer; diff --git a/src/Explorer/Tree/ResourceTreeAdapter.tsx b/src/Explorer/Tree/ResourceTreeAdapter.tsx index 672dabd1e..e25b76a45 100644 --- a/src/Explorer/Tree/ResourceTreeAdapter.tsx +++ b/src/Explorer/Tree/ResourceTreeAdapter.tsx @@ -16,12 +16,10 @@ import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import { isPublicInternetAccessAllowed } from "../../Common/DatabaseAccountUtility"; import * as DataModels from "../../Contracts/DataModels"; import * as ViewModels from "../../Contracts/ViewModels"; -import { IPinnedRepo } from "../../Juno/JunoClient"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import * as TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import { userContext } from "../../UserContext"; import { isServerlessAccount } from "../../Utils/CapabilityUtils"; -import * as GitHubUtils from "../../Utils/GitHubUtils"; import { useTabs } from "../../hooks/useTabs"; import * as ResourceTreeContextMenuButtonFactory from "../ContextMenuButtonFactory"; import { useDialog } from "../Controls/Dialog"; @@ -40,7 +38,6 @@ import UserDefinedFunction from "./UserDefinedFunction"; 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 NotebooksTitle = "NOTEBOOKS"; @@ -49,7 +46,6 @@ export class ResourceTreeAdapter implements ReactAdapter { public parameters: ko.Observable; public myNotebooksContentRoot: NotebookContentItem; - public gitHubNotebooksContentRoot: NotebookContentItem; public constructor(private container: Explorer) { this.parameters = ko.observable(Date.now()); @@ -106,42 +102,9 @@ export class ResourceTreeAdapter implements ReactAdapter { type: NotebookContentItemType.Directory, }; - this.gitHubNotebooksContentRoot = { - name: ResourceTreeAdapter.GitHubReposTitle, - path: ResourceTreeAdapter.PseudoDirPath, - type: NotebookContentItemType.Directory, - }; - return Promise.all(refreshTasks); } - public initializeGitHubRepos(pinnedRepos: IPinnedRepo[]): void { - if (this.gitHubNotebooksContentRoot) { - this.gitHubNotebooksContentRoot.children = []; - pinnedRepos?.forEach((pinnedRepo) => { - const repoFullName = GitHubUtils.toRepoFullName(pinnedRepo.owner, pinnedRepo.name); - const repoTreeItem: NotebookContentItem = { - name: repoFullName, - path: ResourceTreeAdapter.PseudoDirPath, - type: NotebookContentItemType.Directory, - children: [], - }; - - pinnedRepo.branches.forEach((branch) => { - repoTreeItem.children.push({ - name: branch.name, - path: GitHubUtils.toContentUri(pinnedRepo.owner, pinnedRepo.name, branch.name, ""), - type: NotebookContentItemType.Directory, - }); - }); - - this.gitHubNotebooksContentRoot.children.push(repoTreeItem); - }); - - this.triggerRender(); - } - } - private buildDataTree(): LegacyTreeNode { const databaseTreeNodes: LegacyTreeNode[] = useDatabases .getState() diff --git a/src/GitHub/GitHubClient.ts b/src/GitHub/GitHubClient.ts deleted file mode 100644 index c60be04a4..000000000 --- a/src/GitHub/GitHubClient.ts +++ /dev/null @@ -1,582 +0,0 @@ -import { Octokit } from "@octokit/rest"; -import { HttpStatusCodes } from "../Common/Constants"; -import * as Logger from "../Common/Logger"; -import * as UrlUtility from "../Common/UrlUtility"; -import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; -import { getErrorMessage } from "../Common/ErrorHandlingUtils"; - -export interface IGitHubPageInfo { - endCursor: string; - hasNextPage: boolean; -} - -export interface IGitHubResponse { - status: number; - data: T; - pageInfo?: IGitHubPageInfo; -} - -export interface IGitHubRepo { - name: string; - owner: string; - private: boolean; - children?: IGitHubFile[]; -} - -export interface IGitHubFile { - type: "blob" | "tree"; - size?: number; - name: string; - path: string; - content?: string; - sha?: string; - children?: IGitHubFile[]; - repo: IGitHubRepo; - branch: IGitHubBranch; - commit: IGitHubCommit; -} - -export interface IGitHubCommit { - sha: string; - message: string; - commitDate: string; -} - -export interface IGitHubBranch { - name: string; -} - -// graphql schema -interface Collection { - pageInfo?: PageInfo; - nodes: T[]; -} - -interface Repository { - isPrivate: boolean; - name: string; - owner: { - login: string; - }; -} - -interface Ref { - name: string; -} - -interface History { - history: Collection; -} - -interface Commit { - committer: { - date: string; - }; - message: string; - oid: string; -} - -interface Tree extends Blob { - entries: TreeEntry[]; -} - -interface TreeEntry { - name: string; - type: string; - object: Blob; -} - -interface Blob { - byteSize?: number; - oid?: string; -} - -interface PageInfo { - endCursor: string; - hasNextPage: boolean; -} - -// graphql queries and types -const repositoryQuery = `query($owner: String!, $repo: String!) { - repository(owner: $owner, name: $repo) { - owner { - login - } - name - isPrivate - } -}`; -type RepositoryQueryParams = { - owner: string; - repo: string; -}; -type RepositoryQueryResponse = { - repository: Repository; -}; - -const repositoriesQuery = `query($pageSize: Int!, $endCursor: String) { - viewer { - repositories(first: $pageSize, after: $endCursor) { - pageInfo { - endCursor, - hasNextPage - } - nodes { - owner { - login - } - name - isPrivate - } - } - } -}`; -type RepositoriesQueryParams = { - pageSize: number; - endCursor?: string; -}; -type RepositoriesQueryResponse = { - viewer: { - repositories: Collection; - }; -}; - -const branchesQuery = `query($owner: String!, $repo: String!, $refPrefix: String!, $pageSize: Int!, $endCursor: String) { - repository(owner: $owner, name: $repo) { - refs(refPrefix: $refPrefix, first: $pageSize, after: $endCursor) { - pageInfo { - endCursor, - hasNextPage - } - nodes { - name - } - } - } -}`; -type BranchesQueryParams = { - owner: string; - repo: string; - refPrefix: string; - pageSize: number; - endCursor?: string; -}; -type BranchesQueryResponse = { - repository: { - refs: Collection; - }; -}; - -const contentsQuery = `query($owner: String!, $repo: String!, $ref: String!, $path: String, $objectExpression: String!) { - repository(owner: $owner, name: $repo) { - owner { - login - } - name - isPrivate - ref(qualifiedName: $ref) { - name - target { - ... on Commit { - history(first: 1, path: $path) { - nodes { - oid - message - committer { - date - } - } - } - } - } - } - object(expression: $objectExpression) { - ... on Blob { - oid - byteSize - } - ... on Tree { - entries { - name - type - object { - ... on Blob { - oid - byteSize - } - } - } - } - } - } -}`; -type ContentsQueryParams = { - owner: string; - repo: string; - ref: string; - path?: string; - objectExpression: string; -}; -type ContentsQueryResponse = { - repository: Repository & { ref: Ref & { target: History } } & { object: Tree }; -}; - -export class GitHubClient { - private static readonly SelfErrorCode = 599; - private ocktokit: Octokit; - - constructor(private errorCallback: (error: any) => void) { - this.initOctokit(); - } - - public setToken(token: string): void { - this.initOctokit(token); - } - - public async getRepoAsync(owner: string, repo: string): Promise> { - try { - const response = (await this.ocktokit.graphql(repositoryQuery, { - owner, - repo, - } as RepositoryQueryParams)) as RepositoryQueryResponse; - - return { - status: HttpStatusCodes.OK, - data: GitHubClient.toGitHubRepo(response.repository), - }; - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getRepoAsync failed"); - return { - status: GitHubClient.SelfErrorCode, - data: undefined, - }; - } - } - - public async getReposAsync(pageSize: number, endCursor?: string): Promise> { - try { - const response = (await this.ocktokit.graphql(repositoriesQuery, { - pageSize, - endCursor, - } as RepositoriesQueryParams)) as RepositoriesQueryResponse; - - return { - status: HttpStatusCodes.OK, - data: response.viewer.repositories.nodes.map((repo) => GitHubClient.toGitHubRepo(repo)), - pageInfo: GitHubClient.toGitHubPageInfo(response.viewer.repositories.pageInfo), - }; - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getRepoAsync failed"); - return { - status: GitHubClient.SelfErrorCode, - data: undefined, - }; - } - } - - public async getBranchesAsync( - owner: string, - repo: string, - pageSize: number, - endCursor?: string, - ): Promise> { - try { - const response = (await this.ocktokit.graphql(branchesQuery, { - owner, - repo, - refPrefix: "refs/heads/", - pageSize, - endCursor, - } as BranchesQueryParams)) as BranchesQueryResponse; - - return { - status: HttpStatusCodes.OK, - data: response.repository.refs.nodes.map((ref) => GitHubClient.toGitHubBranch(ref)), - pageInfo: GitHubClient.toGitHubPageInfo(response.repository.refs.pageInfo), - }; - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getBranchesAsync failed"); - return { - status: GitHubClient.SelfErrorCode, - data: undefined, - }; - } - } - - public async getContentsAsync( - owner: string, - repo: string, - branch: string, - path?: string, - ): Promise> { - try { - const response = (await this.ocktokit.graphql(contentsQuery, { - owner, - repo, - ref: `refs/heads/${branch}`, - path: path || undefined, - objectExpression: `refs/heads/${branch}:${path || ""}`, - } as ContentsQueryParams)) as ContentsQueryResponse; - - if (!response.repository.object) { - return { - status: HttpStatusCodes.NotFound, - data: undefined, - }; - } - - let data: IGitHubFile | IGitHubFile[]; - const entries = response.repository.object.entries; - const gitHubRepo = GitHubClient.toGitHubRepo(response.repository); - const gitHubBranch = GitHubClient.toGitHubBranch(response.repository.ref); - const gitHubCommit = GitHubClient.toGitHubCommit(response.repository.ref.target.history.nodes[0]); - - if (Array.isArray(entries)) { - data = entries.map((entry) => - GitHubClient.toGitHubFile( - entry, - (path && UrlUtility.createUri(path, entry.name)) || entry.name, - gitHubRepo, - gitHubBranch, - gitHubCommit, - ), - ); - } else { - data = GitHubClient.toGitHubFile( - { - name: NotebookUtil.getName(path), - type: "blob", - object: response.repository.object, - }, - path, - gitHubRepo, - gitHubBranch, - gitHubCommit, - ); - } - - return { - status: HttpStatusCodes.OK, - data, - }; - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubClient.Octokit", "GitHubClient.getContentsAsync failed"); - return { - status: GitHubClient.SelfErrorCode, - data: undefined, - }; - } - } - - public async createOrUpdateFileAsync( - owner: string, - repo: string, - branch: string, - path: string, - message: string, - content: string, - sha?: string, - ): Promise> { - const response = await this.ocktokit.repos.createOrUpdateFile({ - owner, - repo, - branch, - path, - message, - content, - sha, - }); - - let data: IGitHubCommit; - if (response.data) { - data = GitHubClient.toGitHubCommit(response.data.commit); - } - - return { status: response.status, data }; - } - - public async renameFileAsync( - owner: string, - repo: string, - branch: string, - message: string, - oldPath: string, - newPath: string, - ): Promise> { - const ref = `heads/${branch}`; - const currentRef = await this.ocktokit.git.getRef({ - owner, - repo, - ref, - headers: { - "If-None-Match": "", // disable 60s cache - }, - }); - - const currentTree = await this.ocktokit.git.getTree({ - owner, - repo, - tree_sha: currentRef.data.object.sha, - recursive: "1", - headers: { - "If-None-Match": "", // disable 60s cache - }, - }); - - // API infers tree from paths so we need to filter them out - const currentTreeItems = currentTree.data.tree.filter((item) => item.type !== "tree"); - currentTreeItems.forEach((item) => { - if (item.path === newPath) { - throw new Error("File with the path already exists"); - } - }); - - const updatedTree = await this.ocktokit.git.createTree({ - owner, - repo, - tree: currentTreeItems.map((item) => ({ - path: item.path === oldPath ? newPath : item.path, - mode: item.mode as "100644" | "100755" | "040000" | "160000" | "120000", - type: item.type as "blob" | "tree" | "commit", - sha: item.sha, - })), - }); - - const newCommit = await this.ocktokit.git.createCommit({ - owner, - repo, - message, - parents: [currentRef.data.object.sha], - tree: updatedTree.data.sha, - }); - - const updatedRef = await this.ocktokit.git.updateRef({ - owner, - repo, - ref, - sha: newCommit.data.sha, - }); - - return { - status: updatedRef.status, - data: GitHubClient.toGitHubCommit(newCommit.data), - }; - } - - public async deleteFileAsync(file: IGitHubFile, message: string): Promise> { - const response = await this.ocktokit.repos.deleteFile({ - owner: file.repo.owner, - repo: file.repo.name, - path: file.path, - message, - sha: file.sha, - branch: file.branch.name, - }); - - let data: IGitHubCommit; - if (response.data) { - data = GitHubClient.toGitHubCommit(response.data.commit); - } - - return { status: response.status, data }; - } - - public async getBlobAsync(owner: string, repo: string, sha: string): Promise> { - const response = await this.ocktokit.git.getBlob({ - owner, - repo, - file_sha: sha, - mediaType: { - format: "raw", - }, - headers: { - "If-None-Match": "", // disable 60s cache - }, - }); - - return { status: response.status, data: (response.data) }; - } - - private async initOctokit(token?: string) { - this.ocktokit = new Octokit({ - auth: token, - log: { - debug: () => {}, - info: (message?: any) => GitHubClient.log(Logger.logInfo, message), - warn: (message?: any) => GitHubClient.log(Logger.logWarning, message), - error: (error?: any) => Logger.logError(getErrorMessage(error), "GitHubClient.Octokit"), - }, - }); - - this.ocktokit.hook.error("request", (error) => { - this.errorCallback(error); - throw error; - }); - } - - private static log(logger: (message: string, area: string) => void, message?: any) { - if (message) { - message = typeof message === "string" ? message : JSON.stringify(message); - logger(message, "GitHubClient.Octokit"); - } - } - - private static toGitHubRepo(object: Repository): IGitHubRepo { - return { - owner: object.owner.login, - name: object.name, - private: object.isPrivate, - }; - } - - private static toGitHubBranch(object: Ref): IGitHubBranch { - return { - name: object.name, - }; - } - - private static toGitHubCommit(object: { - message: string; - committer: { - date: string; - }; - sha?: string; - oid?: string; - }): IGitHubCommit { - return { - sha: object.sha || object.oid, - message: object.message, - commitDate: object.committer.date, - }; - } - - private static toGitHubPageInfo(object: PageInfo): IGitHubPageInfo { - return { - endCursor: object.endCursor, - hasNextPage: object.hasNextPage, - }; - } - - private static toGitHubFile( - entry: TreeEntry, - path: string, - repo: IGitHubRepo, - branch: IGitHubBranch, - commit: IGitHubCommit, - ): IGitHubFile { - if (entry.type !== "blob" && entry.type !== "tree") { - throw new Error(`Unsupported file type: ${entry.type}`); - } - - return { - type: entry.type, - name: entry.name, - path, - repo, - branch, - commit, - size: entry.object?.byteSize, - sha: entry.object?.oid, - }; - } -} diff --git a/src/GitHub/GitHubConnector.ts b/src/GitHub/GitHubConnector.ts deleted file mode 100644 index 4a88cbdd9..000000000 --- a/src/GitHub/GitHubConnector.ts +++ /dev/null @@ -1,27 +0,0 @@ -import postRobot from "post-robot"; - -export interface IGitHubConnectorParams { - state: string; - code: string; -} - -export const GitHubConnectorMsgType = "GitHubConnectorMsgType"; - -window.addEventListener("load", async () => { - const openerWindow = window.opener; - if (openerWindow) { - const params = new URLSearchParams(document.location.search); - await postRobot.send( - openerWindow, - GitHubConnectorMsgType, - { - state: params.get("state"), - code: params.get("code"), - } as IGitHubConnectorParams, - { - domain: window.location.origin, - }, - ); - window.close(); - } -}); diff --git a/src/GitHub/GitHubContentProvider.test.ts b/src/GitHub/GitHubContentProvider.test.ts deleted file mode 100644 index c902b4b25..000000000 --- a/src/GitHub/GitHubContentProvider.test.ts +++ /dev/null @@ -1,356 +0,0 @@ -import { IContent } from "@nteract/core"; -import { fixture } from "@nteract/fixtures"; -import { HttpStatusCodes } from "../Common/Constants"; -import * as GitHubUtils from "../Utils/GitHubUtils"; -import { GitHubClient, IGitHubCommit, IGitHubFile } from "./GitHubClient"; -import { GitHubContentProvider } from "./GitHubContentProvider"; - -const gitHubClient = new GitHubClient(() => { - /**/ -}); -const gitHubContentProvider = new GitHubContentProvider({ - gitHubClient, - promptForCommitMsg: () => Promise.resolve("commit msg"), -}); -const gitHubCommit: IGitHubCommit = { - sha: "sha", - message: "message", - commitDate: "date", -}; -const sampleFile: IGitHubFile = { - type: "blob", - size: 0, - name: "name.ipynb", - path: "dir/name.ipynb", - content: fixture, - sha: "sha", - repo: { - owner: "owner", - name: "repo", - private: false, - }, - branch: { - name: "branch", - }, - commit: gitHubCommit, -}; -const sampleGitHubUri = GitHubUtils.toContentUri( - sampleFile.repo.owner, - sampleFile.repo.name, - sampleFile.branch.name, - sampleFile.path, -); -const sampleNotebookModel: IContent<"notebook"> = { - name: sampleFile.name, - path: sampleGitHubUri, - type: "notebook", - writable: true, - created: "", - last_modified: "date", - mimetype: "application/x-ipynb+json", - content: sampleFile.content ? JSON.parse(sampleFile.content) : undefined, - format: "json", -}; - -describe("GitHubContentProvider remove", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("errors on invalid path", async () => { - jest.spyOn(GitHubClient.prototype, "getContentsAsync"); - - const response = await gitHubContentProvider.remove(undefined, "invalid path").toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - expect(gitHubClient.getContentsAsync).not.toHaveBeenCalled(); - }); - - it("errors on failed read", async () => { - jest - .spyOn(GitHubClient.prototype, "getContentsAsync") - .mockReturnValue(Promise.resolve({ status: 888, data: undefined })); - - const response = await gitHubContentProvider.remove(undefined, sampleGitHubUri).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toHaveBeenCalled(); - }); - - it("errors on failed delete", async () => { - jest - .spyOn(GitHubClient.prototype, "getContentsAsync") - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })); - jest - .spyOn(GitHubClient.prototype, "deleteFileAsync") - .mockReturnValue(Promise.resolve({ status: 888, data: undefined })); - - const response = await gitHubContentProvider.remove(undefined, sampleGitHubUri).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toHaveBeenCalled(); - expect(gitHubClient.deleteFileAsync).toHaveBeenCalled(); - }); - - it("removes notebook", async () => { - jest - .spyOn(GitHubClient.prototype, "getContentsAsync") - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })); - jest - .spyOn(GitHubClient.prototype, "deleteFileAsync") - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })); - - const response = await gitHubContentProvider.remove(undefined, sampleGitHubUri).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(HttpStatusCodes.NoContent); - expect(gitHubClient.deleteFileAsync).toHaveBeenCalled(); - expect(response.response).toBeUndefined(); - }); -}); - -describe("GitHubContentProvider get", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("errors on invalid path", async () => { - jest.spyOn(GitHubClient.prototype, "getContentsAsync"); - - const response = await gitHubContentProvider.get(undefined, "invalid path", undefined).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - expect(gitHubClient.getContentsAsync).not.toHaveBeenCalled(); - }); - - it("errors on failed read", async () => { - jest - .spyOn(GitHubClient.prototype, "getContentsAsync") - .mockReturnValue(Promise.resolve({ status: 888, data: undefined })); - - const response = await gitHubContentProvider.get(undefined, sampleGitHubUri, undefined).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toHaveBeenCalled(); - }); - - it("reads notebook", async () => { - jest - .spyOn(GitHubClient.prototype, "getContentsAsync") - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })); - - const response = await gitHubContentProvider.get(undefined, sampleGitHubUri, {}).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(gitHubClient.getContentsAsync).toHaveBeenCalled(); - expect(response.response).toEqual(sampleNotebookModel); - }); -}); - -describe("GitHubContentProvider update", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("errors on invalid path", async () => { - jest.spyOn(GitHubClient.prototype, "getContentsAsync"); - - const response = await gitHubContentProvider.update(undefined, "invalid path", undefined).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - expect(gitHubClient.getContentsAsync).not.toHaveBeenCalled(); - }); - - it("errors on failed read", async () => { - jest - .spyOn(GitHubClient.prototype, "getContentsAsync") - .mockReturnValue(Promise.resolve({ status: 888, data: undefined })); - - const response = await gitHubContentProvider.update(undefined, sampleGitHubUri, undefined).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toHaveBeenCalled(); - }); - - it("errors on failed rename", async () => { - jest - .spyOn(GitHubClient.prototype, "getContentsAsync") - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })); - jest - .spyOn(GitHubClient.prototype, "renameFileAsync") - .mockReturnValue(Promise.resolve({ status: 888, data: undefined })); - - const response = await gitHubContentProvider.update(undefined, sampleGitHubUri, sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toHaveBeenCalled(); - expect(gitHubClient.renameFileAsync).toHaveBeenCalled(); - }); - - it("updates notebook", async () => { - jest - .spyOn(GitHubClient.prototype, "getContentsAsync") - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })); - jest - .spyOn(GitHubClient.prototype, "renameFileAsync") - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })); - - const response = await gitHubContentProvider.update(undefined, sampleGitHubUri, sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(gitHubClient.getContentsAsync).toHaveBeenCalled(); - expect(gitHubClient.renameFileAsync).toHaveBeenCalled(); - expect(response.response.type).toEqual(sampleNotebookModel.type); - expect(response.response.name).toEqual(sampleNotebookModel.name); - expect(response.response.path).toEqual(sampleNotebookModel.path); - expect(response.response.content).toBeUndefined(); - }); -}); - -describe("GitHubContentProvider create", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("errors on invalid path", async () => { - jest.spyOn(GitHubClient.prototype, "createOrUpdateFileAsync"); - - const response = await gitHubContentProvider.create(undefined, "invalid path", sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - expect(gitHubClient.createOrUpdateFileAsync).not.toHaveBeenCalled(); - }); - - it("errors on failed create", async () => { - jest - .spyOn(GitHubClient.prototype, "createOrUpdateFileAsync") - .mockReturnValue(Promise.resolve({ status: 888, data: undefined })); - - const response = await gitHubContentProvider.create(undefined, sampleGitHubUri, sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.createOrUpdateFileAsync).toHaveBeenCalled(); - }); - - it("creates notebook", async () => { - jest - .spyOn(GitHubClient.prototype, "createOrUpdateFileAsync") - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit })); - - const response = await gitHubContentProvider.create(undefined, sampleGitHubUri, sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(HttpStatusCodes.Created); - expect(gitHubClient.createOrUpdateFileAsync).toHaveBeenCalled(); - expect(response.response.type).toEqual(sampleNotebookModel.type); - expect(response.response.name).toBeDefined(); - expect(response.response.path).toBeDefined(); - expect(response.response.content).toBeUndefined(); - }); -}); - -describe("GitHubContentProvider save", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("errors on invalid path", async () => { - jest.spyOn(GitHubClient.prototype, "getContentsAsync"); - - const response = await gitHubContentProvider.save(undefined, "invalid path", undefined).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - expect(gitHubClient.getContentsAsync).not.toHaveBeenCalled(); - }); - - it("errors on failed read", async () => { - jest - .spyOn(GitHubClient.prototype, "getContentsAsync") - .mockReturnValue(Promise.resolve({ status: 888, data: undefined })); - - const response = await gitHubContentProvider.save(undefined, sampleGitHubUri, undefined).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toHaveBeenCalled(); - }); - - it("errors on failed update", async () => { - jest - .spyOn(GitHubClient.prototype, "getContentsAsync") - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })); - jest - .spyOn(GitHubClient.prototype, "createOrUpdateFileAsync") - .mockReturnValue(Promise.resolve({ status: 888, data: undefined })); - - const response = await gitHubContentProvider.save(undefined, sampleGitHubUri, sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(888); - expect(gitHubClient.getContentsAsync).toHaveBeenCalled(); - expect(gitHubClient.createOrUpdateFileAsync).toHaveBeenCalled(); - }); - - it("saves notebook", async () => { - jest - .spyOn(GitHubClient.prototype, "getContentsAsync") - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })); - jest - .spyOn(GitHubClient.prototype, "createOrUpdateFileAsync") - .mockReturnValue(Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })); - - const response = await gitHubContentProvider.save(undefined, sampleGitHubUri, sampleNotebookModel).toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(HttpStatusCodes.OK); - expect(gitHubClient.getContentsAsync).toHaveBeenCalled(); - expect(gitHubClient.createOrUpdateFileAsync).toHaveBeenCalled(); - expect(response.response.type).toEqual(sampleNotebookModel.type); - expect(response.response.name).toEqual(sampleNotebookModel.name); - expect(response.response.path).toEqual(sampleNotebookModel.path); - expect(response.response.content).toBeUndefined(); - }); -}); - -describe("GitHubContentProvider listCheckpoints", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("errors for everything", async () => { - const response = await gitHubContentProvider.listCheckpoints().toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - }); -}); - -describe("GitHubContentProvider createCheckpoint", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("errors for everything", async () => { - const response = await gitHubContentProvider.createCheckpoint().toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - }); -}); - -describe("GitHubContentProvider deleteCheckpoint", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("errors for everything", async () => { - const response = await gitHubContentProvider.deleteCheckpoint().toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - }); -}); - -describe("GitHubContentProvider restoreFromCheckpoint", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it("errors for everything", async () => { - const response = await gitHubContentProvider.restoreFromCheckpoint().toPromise(); - expect(response).toBeDefined(); - expect(response.status).toBe(GitHubContentProvider.SelfErrorCode); - }); -}); diff --git a/src/GitHub/GitHubContentProvider.ts b/src/GitHub/GitHubContentProvider.ts deleted file mode 100644 index 61d49e39a..000000000 --- a/src/GitHub/GitHubContentProvider.ts +++ /dev/null @@ -1,434 +0,0 @@ -import { makeNotebookRecord, Notebook, stringifyNotebook, toJS } from "@nteract/commutable"; -import { FileType, IContent, IContentProvider, IEmptyContent, IGetParams, ServerConfig } from "@nteract/core"; -import { from, Observable, of } from "rxjs"; -import { AjaxResponse } from "rxjs/ajax"; -import { HttpStatusCodes } from "../Common/Constants"; -import { getErrorMessage } from "../Common/ErrorHandlingUtils"; -import * as Logger from "../Common/Logger"; -import * as UrlUtility from "../Common/UrlUtility"; -import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; -import * as Base64Utils from "../Utils/Base64Utils"; -import * as GitHubUtils from "../Utils/GitHubUtils"; -import { GitHubClient, IGitHubFile, IGitHubResponse } from "./GitHubClient"; - -export interface GitHubContentProviderParams { - gitHubClient: GitHubClient; - promptForCommitMsg: (title: string, primaryButtonLabel: string) => Promise; -} - -class GitHubContentProviderError extends Error { - constructor( - error: string, - public errno: number = GitHubContentProvider.SelfErrorCode, - ) { - super(error); - } -} - -// Provides 'contents' API for GitHub -// http://jupyter-api.surge.sh/#!/contents -export class GitHubContentProvider implements IContentProvider { - public static readonly SelfErrorCode = 555; - - constructor(private params: GitHubContentProviderParams) {} - - public remove(_: ServerConfig, uri: string): Observable { - return from( - this.getContent(uri).then(async (content: IGitHubResponse) => { - try { - const commitMsg = await this.validateContentAndGetCommitMsg(content, "Delete", "Delete"); - const response = await this.params.gitHubClient.deleteFileAsync(content.data as IGitHubFile, commitMsg); - if (response.status !== HttpStatusCodes.OK) { - throw new GitHubContentProviderError("Failed to delete", response.status); - } - - return this.createSuccessAjaxResponse(HttpStatusCodes.NoContent, undefined); - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubContentProvider/remove", error.errno); - return this.createErrorAjaxResponse(error); - } - }), - ); - } - - public get(_: ServerConfig, uri: string, params: Partial): Observable { - return from( - this.getContent(uri).then(async (content: IGitHubResponse) => { - try { - if (content.status !== HttpStatusCodes.OK) { - throw new GitHubContentProviderError("Failed to get content", content.status); - } - - if (!Array.isArray(content.data) && !content.data.content && params.content !== 0) { - const file = content.data; - file.content = ( - await this.params.gitHubClient.getBlobAsync(file.repo.owner, file.repo.name, file.sha) - ).data; - } - - return this.createSuccessAjaxResponse(HttpStatusCodes.OK, this.createContentModel(uri, content.data, params)); - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubContentProvider/get", error.errno); - return this.createErrorAjaxResponse(error); - } - }), - ); - } - - public update( - _: ServerConfig, - uri: string, - model: Partial>, - ): Observable { - return from( - this.getContent(uri).then(async (content: IGitHubResponse) => { - try { - const gitHubFile = content.data as IGitHubFile; - const commitMsg = await this.validateContentAndGetCommitMsg(content, "Rename", "Rename"); - const newUri = model.path; - const newPath = GitHubUtils.fromContentUri(newUri).path; - const response = await this.params.gitHubClient.renameFileAsync( - gitHubFile.repo.owner, - gitHubFile.repo.name, - gitHubFile.branch.name, - commitMsg, - gitHubFile.path, - newPath, - ); - if (response.status !== HttpStatusCodes.OK) { - throw new GitHubContentProviderError("Failed to rename", response.status); - } - - gitHubFile.commit = response.data; - gitHubFile.path = newPath; - gitHubFile.name = NotebookUtil.getName(gitHubFile.path); - - return this.createSuccessAjaxResponse( - HttpStatusCodes.OK, - this.createContentModel(newUri, gitHubFile, { content: 0 }), - ); - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno); - return this.createErrorAjaxResponse(error); - } - }), - ); - } - - public create( - _: ServerConfig, - uri: string, - model: Partial> & { type: FT }, - ): Observable { - return from( - this.params.promptForCommitMsg("Create New Notebook", "Create").then(async (commitMsg: string) => { - try { - if (!commitMsg) { - throw new GitHubContentProviderError("Couldn't get a commit message"); - } - - if (model.type !== "notebook") { - throw new GitHubContentProviderError("Unsupported content type"); - } - - const contentInfo = GitHubUtils.fromContentUri(uri); - if (!contentInfo) { - throw new GitHubContentProviderError(`Failed to parse ${uri}`); - } - - const content = Base64Utils.utf8ToB64(stringifyNotebook(toJS(makeNotebookRecord()))); - const options: Intl.DateTimeFormatOptions = { - year: "numeric", - month: "short", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - hour12: false, - }; - const name = `Untitled-${new Date().toLocaleString("default", options)}.ipynb`; - let path = name; - if (contentInfo.path) { - path = UrlUtility.createUri(contentInfo.path, name); - } - - const response = await this.params.gitHubClient.createOrUpdateFileAsync( - contentInfo.owner, - contentInfo.repo, - contentInfo.branch, - path, - commitMsg, - content, - ); - if (response.status !== HttpStatusCodes.Created) { - throw new GitHubContentProviderError("Failed to create", response.status); - } - - const newUri = GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path); - const newGitHubFile: IGitHubFile = { - type: "blob", - name: NotebookUtil.getName(newUri), - path, - repo: { - owner: contentInfo.owner, - name: contentInfo.repo, - private: undefined, - }, - branch: { - name: contentInfo.branch, - }, - commit: response.data, - }; - - return this.createSuccessAjaxResponse( - HttpStatusCodes.Created, - this.createContentModel(newUri, newGitHubFile, { content: 0 }), - ); - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubContentProvider/create", error.errno); - return this.createErrorAjaxResponse(error); - } - }), - ); - } - - public save( - _: ServerConfig, - uri: string, - model: Partial>, - ): Observable { - return from( - this.getContent(uri).then(async (content: IGitHubResponse) => { - try { - 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; - if (model.type === "notebook") { - updatedContent = Base64Utils.utf8ToB64(stringifyNotebook(model.content as Notebook)); - } else if (model.type === "file") { - updatedContent = model.content as string; - if (model.format !== "base64") { - updatedContent = Base64Utils.utf8ToB64(updatedContent); - } - } else { - throw new GitHubContentProviderError("Unsupported content type"); - } - - const contentInfo = GitHubUtils.fromContentUri(uri); - let gitHubFile: IGitHubFile; - if (content.data) { - gitHubFile = content.data as IGitHubFile; - } - - 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; - } 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( - HttpStatusCodes.OK, - this.createContentModel(uri, gitHubFile, { content: 0 }), - ); - } catch (error) { - Logger.logError(getErrorMessage(error), "GitHubContentProvider/update", error.errno); - return this.createErrorAjaxResponse(error); - } - }), - ); - } - - public listCheckpoints(): Observable { - const error = new GitHubContentProviderError("Not implemented"); - Logger.logError(error.message, "GitHubContentProvider/listCheckpoints", error.errno); - return of(this.createErrorAjaxResponse(error)); - } - - public createCheckpoint(): Observable { - const error = new GitHubContentProviderError("Not implemented"); - Logger.logError(error.message, "GitHubContentProvider/createCheckpoint", error.errno); - return of(this.createErrorAjaxResponse(error)); - } - - public deleteCheckpoint(): Observable { - const error = new GitHubContentProviderError("Not implemented"); - Logger.logError(error.message, "GitHubContentProvider/deleteCheckpoint", error.errno); - return of(this.createErrorAjaxResponse(error)); - } - - public restoreFromCheckpoint(): Observable { - const error = new GitHubContentProviderError("Not implemented"); - Logger.logError(error.message, "GitHubContentProvider/restoreFromCheckpoint", error.errno); - return of(this.createErrorAjaxResponse(error)); - } - - private async validateContentAndGetCommitMsg( - content: IGitHubResponse, - promptTitle: string, - promptPrimaryButtonLabel: string, - ): Promise { - if (content.status !== HttpStatusCodes.OK) { - throw new GitHubContentProviderError("Failed to get content", content.status); - } - - if (Array.isArray(content.data)) { - throw new GitHubContentProviderError("Operation not supported for collections"); - } - - const commitMsg = await this.params.promptForCommitMsg(promptTitle, promptPrimaryButtonLabel); - if (!commitMsg) { - throw new GitHubContentProviderError("Couldn't get a commit message"); - } - - return commitMsg; - } - - private async getContent(uri: string): Promise> { - const contentInfo = GitHubUtils.fromContentUri(uri); - if (contentInfo) { - const { owner, repo, branch, path } = contentInfo; - return this.params.gitHubClient.getContentsAsync(owner, repo, branch, path); - } - - return Promise.resolve({ status: GitHubContentProvider.SelfErrorCode, data: undefined }); - } - - private createContentModel( - uri: string, - content: IGitHubFile | IGitHubFile[], - params: Partial, - ): IContent { - if (Array.isArray(content)) { - return this.createDirectoryModel(uri, content); - } - - if (content.type === "tree") { - return this.createDirectoryModel(uri, undefined); - } - - if (NotebookUtil.isNotebookFile(uri)) { - return this.createNotebookModel(content, params); - } - - return this.createFileModel(content, params); - } - - private createDirectoryModel(uri: string, gitHubFiles: IGitHubFile[] | undefined): IContent<"directory"> { - return { - name: NotebookUtil.getName(uri), - path: uri, - type: "directory", - writable: true, // TODO: tamitta: we don't know this info here - created: "", // TODO: tamitta: we don't know this info here - last_modified: "", // TODO: tamitta: we don't know this info here - mimetype: undefined, - content: gitHubFiles?.map( - (file: IGitHubFile) => - this.createContentModel( - GitHubUtils.toContentUri(file.repo.owner, file.repo.name, file.branch.name, file.path), - file, - { - content: 0, - }, - ) as IEmptyContent, - ), - format: "json", - }; - } - - private createNotebookModel(gitHubFile: IGitHubFile, params: Partial): IContent<"notebook"> { - const content: Notebook = gitHubFile.content && params.content !== 0 ? JSON.parse(gitHubFile.content) : undefined; - return { - name: gitHubFile.name, - path: GitHubUtils.toContentUri( - gitHubFile.repo.owner, - gitHubFile.repo.name, - gitHubFile.branch.name, - gitHubFile.path, - ), - type: "notebook", - writable: true, // TODO: tamitta: we don't know this info here - created: "", // TODO: tamitta: we don't know this info here - last_modified: gitHubFile.commit.commitDate, - mimetype: content ? "application/x-ipynb+json" : undefined, - content, - format: content ? "json" : undefined, - }; - } - - private createFileModel(gitHubFile: IGitHubFile, params: Partial): IContent<"file"> { - const content: string = gitHubFile.content && params.content !== 0 ? gitHubFile.content : undefined; - return { - name: gitHubFile.name, - path: GitHubUtils.toContentUri( - gitHubFile.repo.owner, - gitHubFile.repo.name, - gitHubFile.branch.name, - gitHubFile.path, - ), - type: "file", - writable: true, // TODO: tamitta: we don't know this info here - created: "", // TODO: tamitta: we don't know this info here - last_modified: gitHubFile.commit.commitDate, - mimetype: content ? "text/plain" : undefined, - content, - format: content ? "text" : undefined, - }; - } - - private createSuccessAjaxResponse(status: number, content: IContent): AjaxResponse { - return { - originalEvent: new Event("no-op"), - xhr: new XMLHttpRequest(), - request: {}, - status, - response: content ? content : undefined, - responseText: content ? JSON.stringify(content) : undefined, - responseType: "json", - }; - } - - private createErrorAjaxResponse(error: GitHubContentProviderError): AjaxResponse { - return { - originalEvent: new Event("no-op"), - xhr: new XMLHttpRequest(), - request: {}, - status: error.errno, - response: error, - responseText: getErrorMessage(error), - responseType: "json", - }; - } -} diff --git a/src/GitHub/GitHubOAuthService.test.ts b/src/GitHub/GitHubOAuthService.test.ts deleted file mode 100644 index b9819696f..000000000 --- a/src/GitHub/GitHubOAuthService.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { HttpStatusCodes } from "../Common/Constants"; -import Explorer from "../Explorer/Explorer"; -import NotebookManager from "../Explorer/Notebook/NotebookManager"; -import { JunoClient } from "../Juno/JunoClient"; -import { IGitHubConnectorParams } from "./GitHubConnector"; -import { GitHubOAuthService } from "./GitHubOAuthService"; - -describe("GitHubOAuthService", () => { - let junoClient: JunoClient; - let gitHubOAuthService: GitHubOAuthService; - let originalDataExplorer: Explorer; - - beforeEach(() => { - junoClient = new JunoClient(); - gitHubOAuthService = new GitHubOAuthService(junoClient); - originalDataExplorer = window.dataExplorer; - window.dataExplorer = { - ...originalDataExplorer, - } as Explorer; - window.dataExplorer.notebookManager = new NotebookManager(); - window.dataExplorer.notebookManager.junoClient = junoClient; - window.dataExplorer.notebookManager.gitHubOAuthService = gitHubOAuthService; - }); - - afterEach(() => { - jest.resetAllMocks(); - window.dataExplorer = originalDataExplorer; - originalDataExplorer = undefined; - gitHubOAuthService = undefined; - junoClient = undefined; - }); - - it("logout deletes app authorization and resets token", async () => { - const deleteAppAuthorizationCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NoContent }); - junoClient.deleteAppAuthorization = deleteAppAuthorizationCallback; - - await gitHubOAuthService.logout(); - expect(deleteAppAuthorizationCallback).toHaveBeenCalled(); - expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined(); - }); - - it("resetToken resets token", () => { - gitHubOAuthService.resetToken(); - expect(gitHubOAuthService.getTokenObservable()()).toBeUndefined(); - }); - - it("startOAuth resets OAuth state", async () => { - let url: string; - const windowOpenCallback = jest.fn().mockImplementation((value: string) => { - url = value; - }); - window.open = windowOpenCallback; - - await gitHubOAuthService.startOAuth("scope"); - expect(windowOpenCallback).toHaveBeenCalled(); - - const initialParams = new URLSearchParams(new URL(url).search); - expect(initialParams.get("state")).toBeDefined(); - - await gitHubOAuthService.startOAuth("another scope"); - expect(windowOpenCallback).toHaveBeenCalled(); - - const newParams = new URLSearchParams(new URL(url).search); - expect(newParams.get("state")).toBeDefined(); - expect(newParams.get("state")).not.toEqual(initialParams.get("state")); - }); - - it("finishOAuth updates token", async () => { - const data = { key: "value" }; - const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.OK, data }); - junoClient.getGitHubToken = getGitHubTokenCallback; - - const initialToken = gitHubOAuthService.getTokenObservable()(); - const state = await gitHubOAuthService.startOAuth("scope"); - - const params: IGitHubConnectorParams = { - state, - code: "code", - }; - await gitHubOAuthService.finishOAuth(params); - const updatedToken = gitHubOAuthService.getTokenObservable()(); - - expect(getGitHubTokenCallback).toHaveBeenCalledWith("code"); - expect(initialToken).not.toEqual(updatedToken); - }); - - it("finishOAuth updates token to error if state doesn't match", async () => { - await gitHubOAuthService.startOAuth("scope"); - - const params: IGitHubConnectorParams = { - state: "state", - code: "code", - }; - await gitHubOAuthService.finishOAuth(params); - - expect(gitHubOAuthService.getTokenObservable()().error).toBeDefined(); - }); - - it("finishOAuth updates token to error if unable to fetch token", async () => { - const getGitHubTokenCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NotFound }); - junoClient.getGitHubToken = getGitHubTokenCallback; - - const state = await gitHubOAuthService.startOAuth("scope"); - - const params: IGitHubConnectorParams = { - state, - code: "code", - }; - await gitHubOAuthService.finishOAuth(params); - - expect(getGitHubTokenCallback).toHaveBeenCalledWith("code"); - expect(gitHubOAuthService.getTokenObservable()().error).toBeDefined(); - }); - - it("isLoggedIn returns false if resetToken is called", () => { - gitHubOAuthService.resetToken(); - expect(gitHubOAuthService.isLoggedIn()).toBeFalsy(); - }); - - it("isLoggedIn returns false if logout is called", async () => { - const deleteAppAuthorizationCallback = jest.fn().mockReturnValue({ status: HttpStatusCodes.NoContent }); - junoClient.deleteAppAuthorization = deleteAppAuthorizationCallback; - - await gitHubOAuthService.logout(); - expect(gitHubOAuthService.isLoggedIn()).toBeFalsy(); - }); -}); diff --git a/src/GitHub/GitHubOAuthService.ts b/src/GitHub/GitHubOAuthService.ts deleted file mode 100644 index 913bc3f3b..000000000 --- a/src/GitHub/GitHubOAuthService.ts +++ /dev/null @@ -1,125 +0,0 @@ -import ko from "knockout"; -import postRobot from "post-robot"; -import { GetGithubClientId } from "Utils/GitHubUtils"; -import { HttpStatusCodes } from "../Common/Constants"; -import { handleError } from "../Common/ErrorHandlingUtils"; -import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent"; -import { JunoClient } from "../Juno/JunoClient"; -import { logConsoleInfo } from "../Utils/NotificationConsoleUtils"; -import { GitHubConnectorMsgType, IGitHubConnectorParams } from "./GitHubConnector"; - -postRobot.on( - GitHubConnectorMsgType, - { - domain: window.location.origin, - }, - (event) => { - // Typescript definition for event is wrong. So read params by casting to - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const params = (event as any).data as IGitHubConnectorParams; - window.dataExplorer.notebookManager?.gitHubOAuthService.finishOAuth(params); - }, -); - -export interface IGitHubOAuthToken { - // API properties - access_token?: string; - scope?: string; - token_type?: string; - error?: string; - error_description?: string; -} - -export class GitHubOAuthService { - private static readonly OAuthEndpoint = "https://github.com/login/oauth/authorize"; - - private state: string; - private token: ko.Observable; - - constructor(private junoClient: JunoClient) { - this.token = ko.observable(); - } - - public async startOAuth(scope: string): Promise { - // If attempting to change scope from "Public & private repos" to "Public only" we need to delete app authorization. - // Otherwise OAuth app still retains the "public & private repos" permissions. - if ( - this.token()?.scope === AuthorizeAccessComponent.Scopes.PublicAndPrivate.key && - scope === AuthorizeAccessComponent.Scopes.Public.key - ) { - const logoutSuccessful = await this.logout(); - if (!logoutSuccessful) { - return undefined; - } - } - - const params = { - scope, - client_id: GetGithubClientId(), - redirect_uri: new URL("./connectToGitHub.html", window.location.href).href, - state: this.resetState(), - }; - - window.open(`${GitHubOAuthService.OAuthEndpoint}?${new URLSearchParams(params).toString()}`); - return params.state; - } - - public async finishOAuth(params: IGitHubConnectorParams): Promise { - try { - this.validateState(params.state); - const response = await this.junoClient.getGitHubToken(params.code); - - if (response.status === HttpStatusCodes.OK && !response.data.error) { - logConsoleInfo("Successfully connected to GitHub"); - this.token(response.data); - } else { - let errorMsg = response.data.error; - if (response.data.error_description) { - errorMsg = `${errorMsg}: ${response.data.error_description}`; - } - throw new Error(errorMsg); - } - } catch (error) { - logConsoleInfo(`Failed to connect to GitHub: ${error}`); - this.token({ error }); - } - } - - public getTokenObservable(): ko.Observable { - return this.token; - } - - public async logout(): Promise { - try { - const response = await this.junoClient.deleteAppAuthorization(this.token()?.access_token); - if (response.status !== HttpStatusCodes.NoContent) { - throw new Error(`Received HTTP ${response.status}: ${response.data} when deleting app authorization`); - } - - this.resetToken(); - return true; - } catch (error) { - handleError(error, "GitHubOAuthService/logout", "Failed to delete app authorization"); - return false; - } - } - - public isLoggedIn(): boolean { - return !!this.token()?.access_token; - } - - private resetState(): string { - this.state = Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER)).toString(); - return this.state; - } - - public resetToken(): void { - this.token(undefined); - } - - private validateState(state: string) { - if (state !== this.state) { - throw new Error("State didn't match. Possibility of cross-site request forgery attack."); - } - } -} diff --git a/src/Juno/JunoClient.test.ts b/src/Juno/JunoClient.test.ts deleted file mode 100644 index ae7542926..000000000 --- a/src/Juno/JunoClient.test.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { HttpStatusCodes } from "../Common/Constants"; -import { IPinnedRepo, JunoClient } from "./JunoClient"; - -const samplePinnedRepos: IPinnedRepo[] = [ - { - owner: "owner", - name: "name", - private: false, - branches: [ - { - name: "name", - }, - ], - }, -]; - -describe("Pinned repos", () => { - const junoClient = new JunoClient(); - - beforeEach(() => { - window.fetch = jest.fn().mockImplementation(() => { - return { - status: HttpStatusCodes.OK, - text: () => JSON.stringify(samplePinnedRepos), - }; - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("updatePinnedRepos invokes pinned repos subscribers", async () => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - const callback = jest.fn().mockImplementation(() => {}); - - junoClient.subscribeToPinnedRepos(callback); - const response = await junoClient.updatePinnedRepos(samplePinnedRepos); - - expect(response.status).toBe(HttpStatusCodes.OK); - expect(callback).toHaveBeenCalledWith(samplePinnedRepos); - }); - - it("getPinnedRepos invokes pinned repos subscribers", async () => { - // eslint-disable-next-line @typescript-eslint/no-empty-function - const callback = jest.fn().mockImplementation(() => {}); - - junoClient.subscribeToPinnedRepos(callback); - const response = await junoClient.getPinnedRepos("scope"); - - expect(response.status).toBe(HttpStatusCodes.OK); - expect(callback).toHaveBeenCalledWith(samplePinnedRepos); - }); -}); - -describe("GitHub", () => { - const junoClient = new JunoClient(); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("getGitHubToken", async () => { - let fetchUrl: string; - window.fetch = jest.fn().mockImplementation((url: string) => { - fetchUrl = url; - - return { - status: HttpStatusCodes.OK, - text: () => JSON.stringify({ access_token: "token" }), - }; - }); - - const response = await junoClient.getGitHubToken("code"); - - expect(response.status).toBe(HttpStatusCodes.OK); - expect(response.data.access_token).toBeDefined(); - expect(window.fetch).toHaveBeenCalled(); - - const fetchUrlParams = new URLSearchParams(new URL(fetchUrl).search); - let fetchUrlParamsCount = 0; - fetchUrlParams.forEach(() => fetchUrlParamsCount++); - - expect(fetchUrlParamsCount).toBe(2); - expect(fetchUrlParams.get("code")).toBeDefined(); - expect(fetchUrlParams.get("client_id")).toBeDefined(); - }); - - it("deleteAppauthorization", async () => { - let fetchUrl: string; - window.fetch = jest.fn().mockImplementation((url: string) => { - fetchUrl = url; - - return { - status: HttpStatusCodes.NoContent, - text: () => undefined as string, - }; - }); - - const response = await junoClient.deleteAppAuthorization("token"); - - expect(response.status).toBe(HttpStatusCodes.NoContent); - expect(window.fetch).toHaveBeenCalled(); - - const fetchUrlParams = new URLSearchParams(new URL(fetchUrl).search); - let fetchUrlParamsCount = 0; - fetchUrlParams.forEach(() => fetchUrlParamsCount++); - - expect(fetchUrlParamsCount).toBe(2); - expect(fetchUrlParams.get("access_token")).toBeDefined(); - expect(fetchUrlParams.get("client_id")).toBeDefined(); - }); -}); diff --git a/src/Juno/JunoClient.ts b/src/Juno/JunoClient.ts index 77ba61764..839ba22f9 100644 --- a/src/Juno/JunoClient.ts +++ b/src/Juno/JunoClient.ts @@ -1,12 +1,7 @@ import { allowedJunoOrigins, validateEndpoint } from "Utils/EndpointUtils"; -import { GetGithubClientId } from "Utils/GitHubUtils"; -import ko from "knockout"; import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; import { configContext } from "../ConfigContext"; import * as DataModels from "../Contracts/DataModels"; -import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent"; -import { IGitHubResponse } from "../GitHub/GitHubClient"; -import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService"; import { userContext } from "../UserContext"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; @@ -15,17 +10,6 @@ export interface IJunoResponse { data: T; } -export interface IPinnedRepo { - owner: string; - name: string; - private: boolean; - branches: IPinnedBranch[]; -} - -export interface IPinnedBranch { - name: string; -} - export interface IGalleryItem { id: string; name: string; @@ -45,113 +29,6 @@ export interface IGalleryItem { } export class JunoClient { - private cachedPinnedRepos: ko.Observable; - - constructor() { - this.cachedPinnedRepos = ko.observable([]); - } - - public subscribeToPinnedRepos(callback: ko.SubscriptionCallback): ko.Subscription { - return this.cachedPinnedRepos.subscribe(callback); - } - - public async getPinnedRepos(scope: string): Promise> { - const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github/pinnedrepos`, { - headers: JunoClient.getHeaders(), - }); - - let pinnedRepos: IPinnedRepo[]; - if (response.status === HttpStatusCodes.OK) { - pinnedRepos = JSON.parse(await response.text()); - - // In case we're restricted to public only scope, we return only public repos - if (scope === AuthorizeAccessComponent.Scopes.Public.key) { - pinnedRepos = pinnedRepos.filter((repo) => !repo.private); - } - - this.cachedPinnedRepos(pinnedRepos); - } - - return { - status: response.status, - data: pinnedRepos, - }; - } - - public async updatePinnedRepos(repos: IPinnedRepo[]): Promise> { - const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github/pinnedrepos`, { - method: "PUT", - body: JSON.stringify(repos), - headers: JunoClient.getHeaders(), - }); - - if (response.status === HttpStatusCodes.OK) { - this.cachedPinnedRepos(repos); - } - - return { - status: response.status, - data: undefined, - }; - } - - public async deleteGitHubInfo(): Promise> { - const response = await window.fetch(`${this.getNotebooksSubscriptionIdAccountUrl()}/github`, { - method: "DELETE", - headers: JunoClient.getHeaders(), - }); - - return { - status: response.status, - data: undefined, - }; - } - - public async getGitHubToken(code: string): Promise> { - const githubParams = JunoClient.getGitHubClientParams(); - githubParams.append("code", code); - - const response = await window.fetch( - `${this.getNotebooksSubscriptionIdAccountUrl()}/github/token?${githubParams.toString()}`, - { - headers: JunoClient.getHeaders(), - }, - ); - - let data: IGitHubOAuthToken; - const body = await response.text(); - if (body) { - data = JSON.parse(body); - } else { - data = { - error: response.statusText, - }; - } - - return { - status: response.status, - data, - }; - } - - public async deleteAppAuthorization(token: string): Promise> { - const githubParams = JunoClient.getGitHubClientParams(); - githubParams.append("access_token", token); - - const response = await window.fetch( - `${this.getNotebooksSubscriptionIdAccountUrl()}/github/token?${githubParams.toString()}`, - { - method: "DELETE", - headers: JunoClient.getHeaders(), - }, - ); - - return { - status: response.status, - data: await response.text(), - }; - } - public async increaseNotebookViews(id: string): Promise> { const response = await window.fetch(`${this.getNotebooksUrl()}/gallery/${id}/views`, { method: "PATCH", @@ -245,18 +122,6 @@ export class JunoClient { return `${JunoClient.getJunoEndpoint()}/api/notebooks`; } - private getAccount(): string { - return userContext?.databaseAccount?.name; - } - - private getSubscriptionId(): string { - return userContext.subscriptionId; - } - - private getNotebooksSubscriptionIdAccountUrl(): string { - return `${this.getNotebooksUrl()}/subscriptions/${this.getSubscriptionId()}/databaseAccounts/${this.getAccount()}`; - } - private getAnalyticsUrl(): string { return `${JunoClient.getJunoEndpoint()}/api/analytics`; } @@ -268,16 +133,4 @@ export class JunoClient { [HttpHeaders.contentType]: "application/json", }; } - - private static getGitHubClientParams(): URLSearchParams { - const githubParams = new URLSearchParams({ - client_id: GetGithubClientId(), - }); - - if (configContext.GITHUB_CLIENT_SECRET) { - githubParams.append("client_secret", configContext.GITHUB_CLIENT_SECRET); - } - - return githubParams; - } } diff --git a/src/Utils/GitHubUtils.test.ts b/src/Utils/GitHubUtils.test.ts deleted file mode 100644 index 33578bd6b..000000000 --- a/src/Utils/GitHubUtils.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as GitHubUtils from "./GitHubUtils"; - -const owner = "owner-1"; -const repo = "repo-1"; -const branch = "branch/name.1-2"; -const path = "folder name/file name1:2.ipynb"; - -describe("GitHubUtils", () => { - it("fromRepoUri parses github repo url correctly", () => { - const repoInfo = GitHubUtils.fromRepoUri(`https://github.com/${owner}/${repo}/tree/${branch}`); - expect(repoInfo).toEqual({ - owner, - repo, - branch, - }); - }); - - it("toContentUri generates github uris correctly", () => { - const uri = GitHubUtils.toContentUri(owner, repo, branch, path); - expect(uri).toBe(`github://${owner}/${repo}/${path}?ref=${branch}`); - }); - - it("fromContentUri parses the github uris correctly", () => { - const contentInfo = GitHubUtils.fromContentUri(`github://${owner}/${repo}/${path}?ref=${branch}`); - expect(contentInfo).toEqual({ - owner, - repo, - branch, - path, - }); - }); -}); diff --git a/src/Utils/GitHubUtils.ts b/src/Utils/GitHubUtils.ts deleted file mode 100644 index 9d8f55818..000000000 --- a/src/Utils/GitHubUtils.ts +++ /dev/null @@ -1,79 +0,0 @@ -// https://github.com///tree/ - -import { JunoEndpoints } from "Common/Constants"; -import { configContext } from "ConfigContext"; -import { userContext } from "UserContext"; - -// The url when users visit a repo/branch on github.com -export const RepoUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/tree\/([^?]*)/; - -// github:////?ref= -// Custom scheme for github content -export const ContentUriPattern = /github:\/\/([^/]*)\/([^/]*)\/([^?]*)\?ref=(.*)/; - -// https://github.com///blob// -// We need to support this until we move to newer scheme for quickstarts -export const LegacyContentUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/blob\/([^/]*)\/([^?]*)/; - -export function toRepoFullName(owner: string, repo: string): string { - return `${owner}/${repo}`; -} - -export function fromRepoUri(repoUri: string): undefined | { owner: string; repo: string; branch: string } { - const matches = repoUri.match(RepoUriPattern); - if (matches && matches.length > 3) { - return { - owner: matches[1], - repo: matches[2], - branch: matches[3], - }; - } - - return undefined; -} - -export function fromContentUri( - contentUri: string, -): undefined | { owner: string; repo: string; branch: string; path: string } { - let matches = contentUri.match(ContentUriPattern); - if (matches && matches.length > 4) { - return { - owner: matches[1], - repo: matches[2], - branch: matches[4], - path: matches[3], - }; - } - - matches = contentUri.match(LegacyContentUriPattern); - if (matches && matches.length > 4) { - return { - owner: matches[1], - repo: matches[2], - branch: matches[3], - path: matches[4], - }; - } - - return undefined; -} - -export function toContentUri(owner: string, repo: string, branch: string, path: string): string { - return `github://${owner}/${repo}/${path}?ref=${branch}`; -} - -export function toRawContentUri(owner: string, repo: string, branch: string, path: string): string { - return `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`; -} - -export function GetGithubClientId(): string { - const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT; - if ( - junoEndpoint === JunoEndpoints.Test || - junoEndpoint === JunoEndpoints.Test2 || - junoEndpoint === JunoEndpoints.Test3 - ) { - return configContext.GITHUB_TEST_ENV_CLIENT_ID; - } - return configContext.GITHUB_CLIENT_ID; -} diff --git a/src/Utils/JunoUtils.test.ts b/src/Utils/JunoUtils.test.ts deleted file mode 100644 index b08e0b168..000000000 --- a/src/Utils/JunoUtils.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { RepoListItem } from "../Explorer/Controls/GitHub/GitHubReposComponent"; -import { IPinnedRepo } from "../Juno/JunoClient"; -import * as JunoUtils from "./JunoUtils"; -import { IGitHubRepo } from "../GitHub/GitHubClient"; - -const gitHubRepo: IGitHubRepo = { - name: "repo-name", - owner: "owner", - private: false, -}; - -const repoListItem: RepoListItem = { - key: "key", - repo: { - name: "repo-name", - owner: "owner", - private: false, - }, - branches: [ - { - name: "branch-name", - }, - ], -}; - -const pinnedRepo: IPinnedRepo = { - name: "repo-name", - owner: "owner", - private: false, - branches: [ - { - name: "branch-name", - }, - ], -}; - -describe("JunoUtils", () => { - it("toPinnedRepo converts RepoListItem to IPinnedRepo", () => { - expect(JunoUtils.toPinnedRepo(repoListItem)).toEqual(pinnedRepo); - }); - - it("toGitHubRepo converts IPinnedRepo to IGitHubRepo", () => { - expect(JunoUtils.toGitHubRepo(pinnedRepo)).toEqual(gitHubRepo); - }); -}); diff --git a/src/Utils/JunoUtils.ts b/src/Utils/JunoUtils.ts deleted file mode 100644 index cd6006a0f..000000000 --- a/src/Utils/JunoUtils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { RepoListItem } from "../Explorer/Controls/GitHub/GitHubReposComponent"; -import { IGitHubRepo } from "../GitHub/GitHubClient"; -import { IPinnedRepo } from "../Juno/JunoClient"; - -export function toPinnedRepo(item: RepoListItem): IPinnedRepo { - return { - owner: item.repo.owner, - name: item.repo.name, - private: item.repo.private, - branches: item.branches.map((element) => ({ name: element.name })), - }; -} - -export function toGitHubRepo(pinnedRepo: IPinnedRepo): IGitHubRepo { - return { - owner: pinnedRepo.owner, - name: pinnedRepo.name, - private: pinnedRepo.private, - }; -} diff --git a/src/connectToGitHub.html b/src/connectToGitHub.html deleted file mode 100644 index 50b1832d7..000000000 --- a/src/connectToGitHub.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - Connect to GitHub - - - - Connecting to GitHub... - - diff --git a/tsconfig.strict.json b/tsconfig.strict.json index 35eecb951..e1021f162 100644 --- a/tsconfig.strict.json +++ b/tsconfig.strict.json @@ -36,7 +36,6 @@ "./src/Contracts/SelfServeContracts.ts", "./src/Contracts/SubscriptionType.ts", "./src/Contracts/Versions.ts", - "./src/Explorer/Controls/GitHub/GitHubStyleConstants.ts", "./src/Explorer/Controls/SmartUi/InputUtils.ts", "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.test.ts", "./src/Explorer/Graph/GraphExplorerComponent/ArraysByKeyCache.ts", @@ -63,7 +62,6 @@ "./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.test.ts", "./src/Explorer/Tables/QueryBuilder/DateTimeUtilities.ts", "./src/Explorer/Tree/AccessibleVerticalList.ts", - "./src/GitHub/GitHubConnector.ts", "./src/HostedExplorerChildFrame.ts", "./src/Platform/Hosted/Components/MeControl.test.tsx", "./src/Platform/Hosted/Components/MeControl.tsx", @@ -96,8 +94,6 @@ "./src/Utils/CapabilityUtils.ts", "./src/Utils/CloudUtils.ts", "./src/Utils/EndpointUtils.ts", - "./src/Utils/GitHubUtils.test.ts", - "./src/Utils/GitHubUtils.ts", "./src/Utils/MessageValidation.test.ts", "./src/Utils/MessageValidation.ts", "./src/Utils/NotificationConsoleUtils.ts", diff --git a/webpack.config.js b/webpack.config.js index dfe25a315..8da4a5a5e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -110,7 +110,6 @@ module.exports = function (_env = {}, argv = {}) { quickstart: "./src/quickstart.ts", hostedExplorer: "./src/HostedExplorer.tsx", selfServe: "./src/SelfServe/SelfServe.tsx", - connectToGitHub: "./src/GitHub/GitHubConnector.ts", redirectBridge: "./src/redirectBridge.ts", ...(mode !== "production" && { testExplorer: "./test/testExplorer/TestExplorer.ts" }), ...(mode !== "production" && { @@ -139,11 +138,6 @@ module.exports = function (_env = {}, argv = {}) { template: "src/hostedExplorer.html", chunks: ["hostedExplorer"], }), - new HtmlWebpackPlugin({ - filename: "connectToGitHub.html", - template: "src/connectToGitHub.html", - chunks: ["connectToGitHub"], - }), new HtmlWebpackPlugin({ filename: "selfServe.html", template: "src/SelfServe/selfServe.html",