Skip to content

Commit

Permalink
merged #669
Browse files Browse the repository at this point in the history
  • Loading branch information
cyrusjc committed Apr 9, 2024
2 parents 6cc3429 + 3c5d9cd commit 6c12ed0
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 17 deletions.
122 changes: 118 additions & 4 deletions server/plugin/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ func (p *Plugin) initializeAPI() {
apiRouter.HandleFunc("/labels", p.checkAuth(p.attachUserContext(p.getLabels), ResponseTypePlain)).Methods(http.MethodGet)
apiRouter.HandleFunc("/milestones", p.checkAuth(p.attachUserContext(p.getMilestones), ResponseTypePlain)).Methods(http.MethodGet)
apiRouter.HandleFunc("/assignees", p.checkAuth(p.attachUserContext(p.getAssignees), ResponseTypePlain)).Methods(http.MethodGet)
apiRouter.HandleFunc("/organizations", p.checkAuth(p.attachUserContext(p.getOrganizations), ResponseTypePlain)).Methods(http.MethodGet)
apiRouter.HandleFunc("/repos_by_org", p.checkAuth(p.attachUserContext(p.getReposByOrg), ResponseTypePlain)).Methods(http.MethodGet)
apiRouter.HandleFunc("/repositories", p.checkAuth(p.attachUserContext(p.getRepositories), ResponseTypePlain)).Methods(http.MethodGet)
apiRouter.HandleFunc("/settings", p.checkAuth(p.attachUserContext(p.updateSettings), ResponseTypePlain)).Methods(http.MethodPost)
apiRouter.HandleFunc("/issue", p.checkAuth(p.attachUserContext(p.getIssueByNumber), ResponseTypePlain)).Methods(http.MethodGet)
Expand Down Expand Up @@ -1186,10 +1188,29 @@ func (p *Plugin) getMilestones(c *UserContext, w http.ResponseWriter, r *http.Re
p.writeJSON(w, allMilestones)
}

func getRepositoryList(c context.Context, userName string, githubClient *github.Client, opt github.ListOptions) ([]*github.Repository, error) {
func getOrganizationList(c context.Context, userName string, githubClient *github.Client, opt github.ListOptions) ([]*github.Organization, error) {
var allOrgs []*github.Organization
for {
orgs, resp, err := githubClient.Organizations.List(c, userName, &opt)
if err != nil {
return nil, err
}

allOrgs = append(allOrgs, orgs...)
if resp.NextPage == 0 {
break
}

opt.Page = resp.NextPage
}

return allOrgs, nil
}

func getRepositoryList(c context.Context, userName string, githubClient *github.Client, opt github.RepositoryListOptions) ([]*github.Repository, error) {
var allRepos []*github.Repository
for {
repos, resp, err := githubClient.Repositories.List(c, userName, &github.RepositoryListOptions{ListOptions: opt})
repos, resp, err := githubClient.Repositories.List(c, userName, &opt)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1223,6 +1244,99 @@ func getRepositoryListByOrg(c context.Context, org string, githubClient *github.
return allRepos, http.StatusOK, nil
}

func (p *Plugin) getOrganizations(c *UserContext, w http.ResponseWriter, r *http.Request) {
var allOrgs []*github.Organization
org := p.getConfiguration().GitHubOrg

if org == "" {
includeLoggedInUser := r.URL.Query().Get("includeLoggedInUser")
if includeLoggedInUser == "true" {
allOrgs = append(allOrgs, &github.Organization{Login: &c.GHInfo.GitHubUsername})
}
githubClient := p.githubConnectUser(c.Context.Ctx, c.GHInfo)
orgList, err := getOrganizationList(c.Ctx, "", githubClient, github.ListOptions{PerPage: 50})
if err != nil {
c.Log.WithError(err).Warnf("Failed to list organizations")
p.writeAPIError(w, &APIErrorResponse{Message: "Failed to fetch organizations", StatusCode: http.StatusInternalServerError})
return
}
allOrgs = append(allOrgs, orgList...)
} else {
allOrgs = append(allOrgs, &github.Organization{Login: &org})
}
// Only send down fields to client that are needed
type OrganizationResponse struct {
Login string `json:"login,omitempty"`
}

resp := make([]OrganizationResponse, len(allOrgs))
for i, r := range allOrgs {
resp[i].Login = r.GetLogin()
}

p.writeJSON(w, resp)
}

func (p *Plugin) getReposByOrg(c *UserContext, w http.ResponseWriter, r *http.Request) {
githubClient := p.githubConnectUser(c.Context.Ctx, c.GHInfo)

opt := github.ListOptions{PerPage: 50}

org := r.URL.Query().Get("organization")

if org == "" {
err := errors.New("organization query param is empty")
c.Log.WithError(err).Warnf("Failed to list repositories")
p.writeAPIError(w, &APIErrorResponse{Message: "Failed to fetch repositories", StatusCode: http.StatusInternalServerError})
return
}

var allRepos []*github.Repository
var err error
var statusCode int

// if organization is username of authenticated user then return repos where authenticated user is owner
if org == c.GHInfo.GitHubUsername {
allRepos, err = getRepositoryList(c.Ctx, "", githubClient, github.RepositoryListOptions{ListOptions: opt, Affiliation: "owner"})
if err != nil {
c.Log.WithError(err).Warnf("Failed to list repositories")
p.writeAPIError(w, &APIErrorResponse{Message: "Failed to fetch repositories", StatusCode: http.StatusInternalServerError})
return
}
} else {
allRepos, statusCode, err = getRepositoryListByOrg(c.Ctx, org, githubClient, opt)
if err != nil {
if statusCode == http.StatusNotFound {
allRepos, err = getRepositoryList(c.Ctx, org, githubClient, github.RepositoryListOptions{ListOptions: opt})
if err != nil {
c.Log.WithError(err).Warnf("Failed to list repositories")
p.writeAPIError(w, &APIErrorResponse{Message: "Failed to fetch repositories", StatusCode: http.StatusInternalServerError})
return
}
} else {
c.Log.WithError(err).Warnf("Failed to list repositories")
p.writeAPIError(w, &APIErrorResponse{Message: "Failed to fetch repositories", StatusCode: statusCode})
return
}
}
}
// Only send down fields to client that are needed
type RepositoryResponse struct {
Name string `json:"name,omitempty"`
FullName string `json:"full_name,omitempty"`
Permissions map[string]bool `json:"permissions,omitempty"`
}

resp := make([]RepositoryResponse, len(allRepos))
for i, r := range allRepos {
resp[i].Name = r.GetName()
resp[i].FullName = r.GetFullName()
resp[i].Permissions = r.GetPermissions()
}

p.writeJSON(w, resp)
}

func (p *Plugin) getRepositories(c *UserContext, w http.ResponseWriter, r *http.Request) {
githubClient := p.githubConnectUser(c.Context.Ctx, c.GHInfo)

Expand All @@ -1234,7 +1348,7 @@ func (p *Plugin) getRepositories(c *UserContext, w http.ResponseWriter, r *http.
opt := github.ListOptions{PerPage: 50}

if org == "" {
allRepos, err = getRepositoryList(c.Ctx, "", githubClient, opt)
allRepos, err = getRepositoryList(c.Ctx, "", githubClient, github.RepositoryListOptions{ListOptions: opt})
if err != nil {
c.Log.WithError(err).Warnf("Failed to list repositories")
p.writeAPIError(w, &APIErrorResponse{Message: "Failed to fetch repositories", StatusCode: http.StatusInternalServerError})
Expand All @@ -1244,7 +1358,7 @@ func (p *Plugin) getRepositories(c *UserContext, w http.ResponseWriter, r *http.
allRepos, statusCode, err = getRepositoryListByOrg(c.Ctx, org, githubClient, opt)
if err != nil {
if statusCode == http.StatusNotFound {
allRepos, err = getRepositoryList(c.Ctx, org, githubClient, opt)
allRepos, err = getRepositoryList(c.Ctx, org, githubClient, github.RepositoryListOptions{ListOptions: opt})
if err != nil {
c.Log.WithError(err).Warnf("Failed to list repositories")
p.writeAPIError(w, &APIErrorResponse{Message: "Failed to fetch repositories", StatusCode: http.StatusInternalServerError})
Expand Down
3 changes: 3 additions & 0 deletions webapp/src/action_types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const {id: pluginId} = manifest;

export default {
RECEIVED_REPOSITORIES: pluginId + '_received_repositories',
RECEIVED_ORGANIZATIONS: pluginId + '_received_organizations',
RECEIVED_REPOSITORIES_BY_ORGANIZATION: pluginId + '_received_repositories_by_organization',
RECEIVED_REVIEWS: pluginId + '_received_reviews',
RECEIVED_REVIEWS_DETAILS: pluginId + '_received_reviews_details',
RECEIVED_YOUR_PRS_DETAILS: pluginId + '_received_your_prs_details',
RECEIVED_SIDEBAR_CONTENT: pluginId + '_received_sidebar_content',
Expand Down
46 changes: 46 additions & 0 deletions webapp/src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,52 @@ export function getReviewsDetails(prList) {
};
}

export function getOrgs() {
return async (dispatch, getState) => {
let data;
try {
data = await Client.getOrganizations();
} catch (error) {
return {error: data};
}

const connected = await checkAndHandleNotConnected(data)(dispatch, getState);
if (!connected) {
return {error: data};
}

dispatch({
type: ActionTypes.RECEIVED_ORGANIZATIONS,
data,
});

return {data};
};
}

export function getReposByOrg(organization) {
return async (dispatch, getState) => {
let data;
try {
data = await Client.getRepositoriesByOrganization(organization);
} catch (error) {
return {error: data};
}

const connected = await checkAndHandleNotConnected(data)(dispatch, getState);
if (!connected) {
return {error: data};
}

dispatch({
type: ActionTypes.RECEIVED_REPOSITORIES_BY_ORGANIZATION,
data,
});

return {data};
};
}

export function getRepos() {
return async (dispatch, getState) => {
let data;
Expand Down
8 changes: 8 additions & 0 deletions webapp/src/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,18 @@ export default class Client {
return this.doPost(`${this.url}/user`, {user_id: userID});
}

getOrganizations = async () => {
return this.doGet(`${this.url}/organizations?includeLoggedInUser=true`);
}

getRepositories = async () => {
return this.doGet(`${this.url}/repositories`);
}

getRepositoriesByOrganization = async (organization) => {
return this.doGet(`${this.url}/repos_by_org?organization=${organization}`);
}

getLabels = async (repo) => {
return this.doGet(`${this.url}/labels?repo=${repo}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ import ReactSelectSetting from 'components/react_select_setting';
const initialState = {
invalid: false,
error: null,
org: '',
};

export default class GithubRepoSelector extends PureComponent {
static propTypes = {
yourRepos: PropTypes.array.isRequired,
yourOrgs: PropTypes.array.isRequired,
yourReposByOrg: PropTypes.array,
theme: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
value: PropTypes.string,
addValidate: PropTypes.func,
removeValidate: PropTypes.func,
actions: PropTypes.shape({
getRepos: PropTypes.func.isRequired,
getOrgs: PropTypes.func.isRequired,
getReposByOrg: PropTypes.func.isRequired,
}).isRequired,
};

Expand All @@ -30,25 +33,73 @@ export default class GithubRepoSelector extends PureComponent {
}

componentDidMount() {
this.props.actions.getRepos();
this.props.actions.getOrgs();
}

onChange = (_, name) => {
const repo = this.props.yourRepos.find((r) => r.full_name === name);
componentDidUpdate(prevProps) {
if (prevProps.yourOrgs !== this.props.yourOrgs) {
if (this.props.yourOrgs.length >= 1) {
this.onChangeForOrg(0, this.props.yourOrgs[0].login);
}
}
}

onChangeForOrg = (_, org) => {
if (this.state.org !== org) {
this.setState({org});
this.props.actions.getReposByOrg(org);
this.props.onChange(null);
}
}

onChangeForRepo = (_, name) => {
const repo = this.props.yourReposByOrg.find((r) => r.full_name === name);
this.props.onChange({name, permissions: repo.permissions});
}

render() {
const repoOptions = this.props.yourRepos.map((item) => ({value: item.full_name, label: item.full_name}));
const orgOptions = this.props.yourOrgs.map((item) => ({value: item.login, label: item.login}));
const repoOptions = this.props.yourReposByOrg.map((item) => ({value: item.full_name, label: item.name}));

let orgSelector = null;
let helperTextForRepoSelector = 'Returns GitHub repositories connected to the user account';

// If there are no organanizations for authenticated user, then don't show organization selector
if (orgOptions.length > 1) {
orgSelector = (
<div>
<ReactSelectSetting
name={'org'}
label={'Organization'}
limitOptions={true}
required={false}
onChange={this.onChangeForOrg}
options={orgOptions}
isMulti={false}
key={'org'}
theme={this.props.theme}
addValidate={this.props.addValidate}
formatGroupLabel='user repositories'
removeValidate={this.props.removeValidate}
value={orgOptions.find((option) => option.value === this.state.org)}
/>
<div className={'help-text'}>
{'Returns GitHub organizations connected to the user account'} <br/><br/>
</div>
</div>
);
helperTextForRepoSelector = 'Returns GitHub repositories under selected organizations';
}

return (
<div className={'form-group margin-bottom x3'}>
{ orgSelector }
<ReactSelectSetting
name={'repo'}
label={'Repository'}
limitOptions={true}
required={true}
onChange={this.onChange}
onChange={this.onChangeForRepo}
options={repoOptions}
isMulti={false}
key={'repo'}
Expand All @@ -58,7 +109,7 @@ export default class GithubRepoSelector extends PureComponent {
value={repoOptions.find((option) => option.value === this.props.value)}
/>
<div className={'help-text'}>
{'Returns GitHub repositories connected to the user account'} <br/>
{helperTextForRepoSelector} <br/>
</div>
</div>
);
Expand Down
16 changes: 11 additions & 5 deletions webapp/src/components/github_repo_selector/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,27 @@ import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import manifest from 'manifest';
import {getRepos} from '../../actions';

import {getReposByOrg, getOrgs} from '../../actions';

import GithubRepoSelector from './github_repo_selector.jsx';

function mapStateToProps(state) {
return {
yourRepos: state[`plugins-${manifest.id}`].yourRepos,
yourOrgs: state[`plugins-${pluginId}`].yourOrgs,
yourReposByOrg: state[`plugins-${pluginId}`].yourReposByOrg,
};
}

function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
getRepos,
}, dispatch),
actions: bindActionCreators(
{
getOrgs,
getReposByOrg,
},
dispatch,
),
};
}

Expand Down
Loading

0 comments on commit 6c12ed0

Please sign in to comment.