Initial Move from Azure DevOps to GitHub

This commit is contained in:
Steve Faulkner
2020-05-25 21:30:55 -05:00
commit 36581fb6d9
986 changed files with 195242 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
import { DefaultButton, IButtonProps, ITextFieldProps, TextField } from "office-ui-fabric-react";
import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels";
import * as Constants from "../../../Common/Constants";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../../Common/Constants";
import { RepoListItem } from "./GitHubReposComponent";
import { ChildrenMargin } from "./GitHubStyleConstants";
import { GitHubUtils } from "../../../Utils/GitHubUtils";
import { IGitHubRepo } from "../../../GitHub/GitHubClient";
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
export interface AddRepoComponentProps {
container: ViewModels.Explorer;
getRepo: (owner: string, repo: string) => Promise<IGitHubRepo>;
pinRepo: (item: RepoListItem) => void;
}
interface AddRepoComponentState {
textFieldValue: string;
textFieldErrorMessage: string;
}
export class AddRepoComponent extends React.Component<AddRepoComponentProps, AddRepoComponentState> {
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";
private static readonly DefaultBranchName = "master";
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 (
<>
<p style={{ marginBottom: ChildrenMargin }}>{AddRepoComponent.DescriptionText}</p>
<TextField {...textFieldProps} />
<DefaultButton style={{ marginTop: ChildrenMargin }} {...buttonProps} />
</>
);
}
private onTextFieldChange = (
event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newValue?: string
): void => {
this.setState({
textFieldValue: newValue || "",
textFieldErrorMessage: undefined
});
};
private onAddRepoButtonClick = async (): Promise<void> => {
const startKey: number = TelemetryProcessor.traceStart(Action.NotebooksGitHubManualRepoAdd, {
databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name,
defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Notebook
});
let enteredUrl = this.state.textFieldValue;
if (enteredUrl.indexOf("/tree/") === -1) {
enteredUrl = `${enteredUrl}/tree/${AddRepoComponent.DefaultBranchName}`;
}
const gitHubInfo = GitHubUtils.fromGitHubUri(enteredUrl);
if (gitHubInfo) {
this.setState({
textFieldValue: "",
textFieldErrorMessage: undefined
});
const repo = await this.props.getRepo(gitHubInfo.owner, gitHubInfo.repo);
if (repo) {
const item: RepoListItem = {
key: GitHubUtils.toRepoFullName(repo.owner.login, repo.name),
repo,
branches: [
{
name: gitHubInfo.branch
}
]
};
TelemetryProcessor.traceSuccess(
Action.NotebooksGitHubManualRepoAdd,
{
databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name,
defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Notebook
},
startKey
);
return this.props.pinRepo(item);
}
}
this.setState({
textFieldErrorMessage: AddRepoComponent.TextFieldErrorMessage
});
TelemetryProcessor.traceFailure(
Action.NotebooksGitHubManualRepoAdd,
{
databaseAccountName: this.props.container.databaseAccount() && this.props.container.databaseAccount().name,
defaultExperience: this.props.container.defaultExperience && this.props.container.defaultExperience(),
dataExplorerArea: Constants.Areas.Notebook,
error: AddRepoComponent.TextFieldErrorMessage
},
startKey
);
};
}

View File

@@ -0,0 +1,91 @@
import {
ChoiceGroup,
IButtonProps,
IChoiceGroupProps,
PrimaryButton,
IChoiceGroupOption
} from "office-ui-fabric-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<HTMLElement>, 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 (
<>
<p>{AuthorizeAccessComponent.DescriptionPara1}</p>
<p style={{ marginTop: ChildrenMargin }}>{AuthorizeAccessComponent.DescriptionPara2}</p>
<ChoiceGroup style={{ marginTop: ChildrenMargin }} {...choiceGroupProps} />
<PrimaryButton style={{ marginTop: ChildrenMargin }} {...buttonProps} />
</>
);
}
}

View File

@@ -0,0 +1,82 @@
import { DefaultButton, IButtonProps, Link, PrimaryButton } from "office-ui-fabric-react";
import * as React from "react";
import { IGitHubBranch, IGitHubRepo } from "../../../GitHub/GitHubClient";
import { AddRepoComponent, AddRepoComponentProps } from "./AddRepoComponent";
import { AuthorizeAccessComponent, AuthorizeAccessComponentProps } from "./AuthorizeAccessComponent";
import { ChildrenMargin, ButtonsFooterStyle, 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<GitHubReposComponentProps> {
public static readonly ConnectToGitHubTitle = "Connect to GitHub";
public static readonly ManageGitHubRepoTitle = "Manage GitHub settings";
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 header: JSX.Element = (
<p>
{this.props.showAuthorizeAccess
? GitHubReposComponent.ConnectToGitHubTitle
: GitHubReposComponent.ManageGitHubRepoTitle}
</p>
);
const content: JSX.Element = this.props.showAuthorizeAccess ? (
<AuthorizeAccessComponent {...this.props.authorizeAccessProps} />
) : (
<>
<p>{GitHubReposComponent.ManageGitHubRepoDescription}</p>
<ReposListComponent {...this.props.reposListProps} />
</>
);
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 (
<>
<div className={"firstdivbg headerline"}>{header}</div>
<div className={"paneMainContent"}>{content}</div>
{!this.props.showAuthorizeAccess && (
<>
<div className={"paneFooter"} style={ContentFooterStyle}>
<AddRepoComponent {...this.props.addRepoProps} />
</div>
<div className={"paneFooter"} style={ButtonsFooterStyle}>
<PrimaryButton {...okProps} />
<DefaultButton style={{ marginLeft: ChildrenMargin }} {...cancelProps} />
</div>
</>
)}
</>
);
}
}

View File

@@ -0,0 +1,20 @@
import ko from "knockout";
import * as React from "react";
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
import { GitHubReposComponent, GitHubReposComponentProps } from "./GitHubReposComponent";
export class GitHubReposComponentAdapter implements ReactAdapter {
public parameters: ko.Observable<number>;
constructor(private props: GitHubReposComponentProps) {
this.parameters = ko.observable<number>(Date.now());
}
public renderComponent(): JSX.Element {
return <GitHubReposComponent {...this.props} />;
}
public triggerRender(): void {
window.requestAnimationFrame(() => this.parameters(Date.now()));
}
}

View File

@@ -0,0 +1,58 @@
import {
IStyleFunctionOrObject,
ICheckboxStyleProps,
ICheckboxStyles,
IDropdownStyles,
IDropdownStyleProps
} from "office-ui-fabric-react";
export const ButtonsFooterStyle: React.CSSProperties = {
padding: 14,
height: "auto"
};
export const ContentFooterStyle: React.CSSProperties = {
padding: "10px 24px 10px 24px",
height: "auto"
};
export const ChildrenMargin = 10;
export const FontSize = 12;
export const ReposListCheckboxStyles: IStyleFunctionOrObject<ICheckboxStyleProps, ICheckboxStyles> = {
label: {
margin: 0,
padding: "2 0 2 0"
},
text: {
fontSize: FontSize
}
};
export const BranchesDropdownCheckboxStyles: IStyleFunctionOrObject<ICheckboxStyleProps, ICheckboxStyles> = {
label: {
margin: 0,
padding: 0,
fontSize: FontSize
},
root: {
padding: 0
},
text: {
fontSize: FontSize
}
};
export const BranchesDropdownStyles: IStyleFunctionOrObject<IDropdownStyleProps, IDropdownStyles> = {
title: {
fontSize: FontSize
}
};
export const BranchesDropdownOptionContainerStyle: React.CSSProperties = {
padding: 8
};
export const ReposListRepoColumnMinWidth = 192;
export const ReposListBranchesColumnWidth = 116;
export const BranchesDropdownWidth = 200;

View File

@@ -0,0 +1,301 @@
import {
Checkbox,
DetailsList,
DetailsRow,
Dropdown,
ICheckboxProps,
IDetailsFooterProps,
IDetailsListProps,
IDetailsRowBaseProps,
IDropdown,
IDropdownOption,
IDropdownProps,
ILinkProps,
ISelectableDroppableTextProps,
Link,
ResponsiveMode,
SelectionMode,
Text
} from "office-ui-fabric-react";
import * as React from "react";
import { IGitHubBranch } from "../../../GitHub/GitHubClient";
import { GitHubUtils } from "../../../Utils/GitHubUtils";
import { RepoListItem } from "./GitHubReposComponent";
import {
BranchesDropdownCheckboxStyles,
BranchesDropdownOptionContainerStyle,
ReposListCheckboxStyles,
ReposListRepoColumnMinWidth,
ReposListBranchesColumnWidth,
BranchesDropdownWidth,
BranchesDropdownStyles
} from "./GitHubStyleConstants";
export interface ReposListComponentProps {
branchesProps: Record<string, BranchesProps>; // key'd by repo key
pinnedReposProps: PinnedReposProps;
unpinnedReposProps: UnpinnedReposProps;
pinRepo: (repo: RepoListItem) => void;
unpinRepo: (repo: RepoListItem) => void;
}
export interface BranchesProps {
branches: IGitHubBranch[];
hasMore: boolean;
isLoading: boolean;
loadMore: () => void;
}
export interface PinnedReposProps {
repos: RepoListItem[];
}
export interface UnpinnedReposProps {
repos: RepoListItem[];
hasMore: boolean;
isLoading: boolean;
loadMore: () => void;
}
export class ReposListComponent extends React.Component<ReposListComponentProps> {
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 DefaultBranchName = "master";
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 (
<>
<DetailsList {...pinnedReposListProps} />
<DetailsList {...unpinnedReposListProps} />
</>
);
}
private onRenderPinnedReposColumnItem = (item: RepoListItem, index: number): JSX.Element => {
if (index === ReposListComponent.FooterIndex) {
return <Text>None</Text>;
}
const checkboxProps: ICheckboxProps = {
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner.login, item.repo.name)),
styles: ReposListCheckboxStyles,
defaultChecked: true,
onChange: () => this.props.unpinRepo(item)
};
return <Checkbox {...checkboxProps} />;
};
private onRenderPinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => {
if (index === ReposListComponent.FooterIndex) {
return <></>;
}
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner.login, item.repo.name)];
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 <Dropdown {...dropdownProps} />;
};
private onRenderUnpinnedReposBranchesColumnItem = (item: RepoListItem, index: number): JSX.Element => {
if (index === ReposListComponent.FooterIndex) {
return <></>;
}
const dropdownProps: IDropdownProps = {
styles: BranchesDropdownStyles,
options: [],
placeholder: ReposListComponent.DefaultBranchName,
disabled: true
};
return <Dropdown {...dropdownProps} />;
};
private onRenderBranchesDropdownList = (props: ISelectableDroppableTextProps<IDropdown, IDropdown>): JSX.Element => {
const renderedList: JSX.Element[] = [];
props.options.forEach((option: IDropdownOption) => {
const item = (
<div key={option.key} style={BranchesDropdownOptionContainerStyle}>
{this.onRenderPinnedReposBranchesDropdownOption(option)}
</div>
);
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.login, item.repo.name)];
if (option.index === ReposListComponent.FooterIndex) {
const linkProps: ILinkProps = {
disabled: branchesProps.isLoading,
onClick: branchesProps.loadMore
};
return <Link {...linkProps}>{option.text}</Link>;
}
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 <Checkbox {...checkboxProps} />;
}
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 <Link {...linkProps}>{linkText}</Link>;
}
const checkboxProps: ICheckboxProps = {
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner.login, item.repo.name)),
styles: ReposListCheckboxStyles,
onChange: () => {
const repoListItem = { ...item };
repoListItem.branches = [{ name: ReposListComponent.DefaultBranchName }];
this.props.pinRepo(repoListItem);
}
};
return <Checkbox {...checkboxProps} />;
};
private onRenderReposFooter = (detailsFooterProps: IDetailsFooterProps): JSX.Element => {
const props: IDetailsRowBaseProps = {
...detailsFooterProps,
item: {},
itemIndex: ReposListComponent.FooterIndex
};
return <DetailsRow {...props} />;
};
private static getCheckboxPropsForLabel(label: string): ICheckboxProps {
return {
label,
title: label,
ariaLabel: label
};
}
private static getKey(item: RepoListItem): string {
return item.key;
}
}