فهرست منبع

Merge pull request #881 from porter-dev/0.6.0-branch-list-fixes

[0.6.0] [API] Branch list fixes
jusrhee 4 سال پیش
والد
کامیت
5fa5ac7572

+ 93 - 0
dashboard/src/components/SearchBar.tsx

@@ -0,0 +1,93 @@
+import React, { useState } from "react";
+import Button from "./Button";
+import styled from "styled-components";
+
+interface Props {
+  setSearchFilter: (x: string) => void;
+  disabled: boolean;
+  prompt?: string;
+}
+
+const SearchBar: React.FC<Props> = ({ setSearchFilter, disabled, prompt }) => {
+  const [searchInput, setSearchInput] = useState("");
+
+  return (
+    <SearchRowWrapper>
+      <SearchBarWrapper>
+        <i className="material-icons">search</i>
+        <SearchInput
+          value={searchInput}
+          onChange={(e: any) => {
+            setSearchInput(e.target.value);
+          }}
+          onKeyPress={({ key }) => {
+            if (key === "Enter") {
+              setSearchFilter(searchInput);
+            }
+          }}
+          placeholder={prompt}
+        />
+      </SearchBarWrapper>
+      <ButtonWrapper disabled={disabled}>
+        <Button
+          onClick={() => setSearchFilter(searchInput)}
+          disabled={disabled}
+        >
+          Search
+        </Button>
+      </ButtonWrapper>
+    </SearchRowWrapper>
+  );
+};
+
+export default SearchBar;
+
+const SearchRow = styled.div`
+  display: flex;
+  align-items: center;
+  height: 40px;
+  background: #ffffff11;
+  border-bottom: 1px solid #606166;
+  margin-bottom: 10px;
+`;
+
+const SearchRowWrapper = styled(SearchRow)`
+  border-bottom: 0;
+  border: 1px solid #ffffff55;
+  border-radius: 3px;
+`;
+
+const ButtonWrapper = styled.div`
+  background: ${(props: { disabled?: boolean }) =>
+    props.disabled ? "#aaaabbee" : "#616FEEcc"};
+  :hover {
+    background: ${(props: { disabled?: boolean }) =>
+      props.disabled ? "" : "#505edddd"};
+  }
+  height: 40px;
+  display: flex;
+  align-items: center;
+`;
+
+const SearchBarWrapper = styled.div`
+  display: flex;
+  flex: 1;
+
+  > i {
+    color: #aaaabb;
+    padding-top: 1px;
+    margin-left: 13px;
+    font-size: 18px;
+    margin-right: 8px;
+  }
+`;
+
+const SearchInput = styled.input`
+  outline: none;
+  border: none;
+  font-size: 13px;
+  background: none;
+  width: 100%;
+  color: white;
+  height: 20px;
+`;

+ 2 - 2
dashboard/src/components/repo-selector/ActionConfEditor.tsx

@@ -49,12 +49,12 @@ const ActionConfEditor: React.FC<Props> = (props) => {
   } else if (!branch) {
     return (
       <>
-        <ExpandedWrapper>
+        <ExpandedWrapperAlt>
           <BranchList
             actionConfig={actionConfig}
             setBranch={(branch: string) => setBranch(branch)}
           />
-        </ExpandedWrapper>
+        </ExpandedWrapperAlt>
         <Br />
         <BackButton
           width="135px"

+ 71 - 50
dashboard/src/components/repo-selector/BranchList.tsx

@@ -1,36 +1,28 @@
-import React, { Component } from "react";
+import React, { useEffect, useState, useContext } from "react";
 import styled from "styled-components";
 import branch_icon from "assets/branch.png";
-import info from "assets/info.svg";
 
 import api from "../../shared/api";
 import { Context } from "../../shared/Context";
-import { ActionConfigType } from "../..//shared/types";
+import { ActionConfigType } from "../../shared/types";
 
 import Loading from "../Loading";
+import SearchBar from "../SearchBar";
 
-type PropsType = {
+type Props = {
   actionConfig: ActionConfigType;
   setBranch: (x: string) => void;
 };
 
-type StateType = {
-  loading: boolean;
-  error: boolean;
-  branches: string[];
-};
-
-export default class BranchList extends Component<PropsType, StateType> {
-  state = {
-    loading: true,
-    error: false,
-    branches: [] as string[],
-  };
+const BranchList: React.FC<Props> = ({ setBranch, actionConfig }) => {
+  const [loading, setLoading] = useState(true);
+  const [error, setError] = useState(false);
+  const [branches, setBranches] = useState<string[]>([]);
+  const [searchFilter, setSearchFilter] = useState(null);
 
-  componentDidMount() {
-    let { actionConfig } = this.props;
-    let { currentProject } = this.context;
+  const { currentProject } = useContext(Context);
 
+  useEffect(() => {
     // Get branches
     api
       .getBranches(
@@ -44,17 +36,19 @@ export default class BranchList extends Component<PropsType, StateType> {
           name: actionConfig.git_repo.split("/")[1],
         }
       )
-      .then((res) =>
-        this.setState({ branches: res.data, loading: false, error: false })
-      )
+      .then((res) => {
+        setBranches(res.data);
+        setLoading(false);
+        setError(false);
+      })
       .catch((err) => {
         console.log(err);
-        this.setState({ loading: false, error: true });
+        setLoading(false);
+        setError(true);
       });
-  }
+  }, []);
 
-  renderBranchList = () => {
-    let { branches, loading, error } = this.state;
+  const renderBranchList = () => {
     if (loading) {
       return (
         <LoadingWrapper>
@@ -65,34 +59,47 @@ export default class BranchList extends Component<PropsType, StateType> {
       return <LoadingWrapper>Error loading branches</LoadingWrapper>;
     }
 
-    return branches.map((branch: string, i: number) => {
+    let results =
+      searchFilter != null
+        ? branches.filter((branch) => {
+            return branch
+              .toLowerCase()
+              .includes(searchFilter.toLowerCase() || "");
+          })
+        : branches.slice(0, 10);
+
+    if (results.length == 0) {
+      return <LoadingWrapper>No matching Branches found.</LoadingWrapper>;
+    }
+    return results.map((branch: string, i: number) => {
       return (
         <BranchName
           key={i}
           lastItem={i === branches.length - 1}
-          onClick={() => this.props.setBranch(branch)}
+          onClick={() => setBranch(branch)}
         >
-          <img src={branch_icon} />
+          <img src={branch_icon} alt={"branch icon"} />
           {branch}
         </BranchName>
       );
     });
   };
 
-  render() {
-    return (
-      <>
-        <InfoRow lastItem={false}>
-          <img src={info} />
-          Select Branch
-        </InfoRow>
-        {this.renderBranchList()}
-      </>
-    );
-  }
-}
+  return (
+    <>
+      <SearchBar
+        setSearchFilter={setSearchFilter}
+        disabled={error || loading}
+        prompt={"Search branches..."}
+      />
+      <BranchListWrapper>
+        <ExpandedWrapper>{renderBranchList()}</ExpandedWrapper>
+      </BranchListWrapper>
+    </>
+  );
+};
 
-BranchList.contextType = Context;
+export default BranchList;
 
 const BranchName = styled.div`
   display: flex;
@@ -123,14 +130,6 @@ const BranchName = styled.div`
   }
 `;
 
-const InfoRow = styled(BranchName)`
-  cursor: default;
-  color: #ffffff55;
-  :hover {
-    background: #ffffff11;
-  }
-`;
-
 const LoadingWrapper = styled.div`
   padding: 30px 0px;
   background: #ffffff11;
@@ -140,3 +139,25 @@ const LoadingWrapper = styled.div`
   font-size: 13px;
   color: #ffffff44;
 `;
+
+const BranchListWrapper = styled.div`
+  border: 1px solid #ffffff55;
+  border-radius: 3px;
+  overflow-y: auto;
+`;
+
+const ExpandedWrapper = styled.div`
+  width: 100%;
+  border-radius: 3px;
+  border: 0px solid #ffffff44;
+  max-height: 221px;
+  top: 40px;
+
+  > i {
+    font-size: 18px;
+    display: block;
+    position: absolute;
+    left: 10px;
+    top: 10px;
+  }
+`;

+ 11 - 93
dashboard/src/components/repo-selector/RepoList.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useContext, useEffect, useRef } from "react";
+import React, { useState, useContext, useEffect } from "react";
 import styled from "styled-components";
 import github from "assets/github.png";
 
@@ -7,8 +7,7 @@ import { RepoType, ActionConfigType } from "shared/types";
 import { Context } from "shared/Context";
 
 import Loading from "../Loading";
-import Button from "../Button";
-import { AxiosResponse } from "axios";
+import SearchBar from "../SearchBar";
 
 type Props = {
   actionConfig: ActionConfigType | null;
@@ -27,7 +26,6 @@ const RepoList: React.FC<Props> = ({
   const [loading, setLoading] = useState(true);
   const [error, setError] = useState(false);
   const [searchFilter, setSearchFilter] = useState(null);
-  const [searchInput, setSearchInput] = useState("");
   const { currentProject } = useContext(Context);
 
   // TODO: Try to unhook before unmount
@@ -135,7 +133,9 @@ const RepoList: React.FC<Props> = ({
     let results =
       searchFilter != null
         ? repos.filter((repo: RepoType) => {
-            return repo.FullName.includes(searchFilter || "");
+            return repo.FullName.toLowerCase().includes(
+              searchFilter.toLowerCase() || ""
+            );
           })
         : repos.slice(0, 10);
 
@@ -151,7 +151,7 @@ const RepoList: React.FC<Props> = ({
             onClick={() => setRepo(repo)}
             readOnly={readOnly}
           >
-            <img src={github} />
+            <img src={github} alt={"github icon"} />
             {repo.FullName}
           </RepoName>
         );
@@ -165,31 +165,11 @@ const RepoList: React.FC<Props> = ({
     } else {
       return (
         <>
-          <SearchRowTop>
-            <SearchBar>
-              <i className="material-icons">search</i>
-              <SearchInput
-                value={searchInput}
-                onChange={(e: any) => {
-                  setSearchInput(e.target.value);
-                }}
-                onKeyPress={({ key }) => {
-                  if (key === "Enter") {
-                    setSearchFilter(searchInput);
-                  }
-                }}
-                placeholder="Search repos..."
-              />
-            </SearchBar>
-            <ButtonWrapper disabled={loading || error}>
-              <Button
-                onClick={() => setSearchFilter(searchInput)}
-                disabled={loading || error}
-              >
-                Search
-              </Button>
-            </ButtonWrapper>
-          </SearchRowTop>
+          <SearchBar
+            setSearchFilter={setSearchFilter}
+            disabled={error || loading}
+            prompt={"Search repos..."}
+          />
           <RepoListWrapper>
             <ExpandedWrapper>{renderRepoList()}</ExpandedWrapper>
           </RepoListWrapper>
@@ -203,39 +183,12 @@ const RepoList: React.FC<Props> = ({
 
 export default RepoList;
 
-const ButtonWrapper = styled.div`
-  background: ${(props: { disabled?: boolean }) =>
-    props.disabled ? "#aaaabbee" : "#616FEEcc"};
-  :hover {
-    background: ${(props: { disabled?: boolean }) =>
-      props.disabled ? "" : "#505edddd"};
-  }
-  height: 40px;
-  display: flex;
-  align-items: center;
-`;
-
 const RepoListWrapper = styled.div`
   border: 1px solid #ffffff55;
   border-radius: 3px;
   overflow-y: auto;
 `;
 
-const SearchRow = styled.div`
-  display: flex;
-  align-items: center;
-  height: 40px;
-  background: #ffffff11;
-  border-bottom: 1px solid #606166;
-  margin-bottom: 10px;
-`;
-
-const SearchRowTop = styled(SearchRow)`
-  border-bottom: 0;
-  border: 1px solid #ffffff55;
-  border-radius: 3px;
-`;
-
 const RepoName = styled.div`
   display: flex;
   width: 100%;
@@ -280,18 +233,6 @@ const RepoName = styled.div`
   }
 `;
 
-const InfoRow = styled(RepoName)`
-  cursor: default;
-  color: #ffffff55;
-  :hover {
-    background: #ffffff11;
-
-    > i {
-      background: none;
-    }
-  }
-`;
-
 const LoadingWrapper = styled.div`
   padding: 30px 0px;
   background: #ffffff11;
@@ -330,26 +271,3 @@ const A = styled.a`
   margin-left: 5px;
   cursor: pointer;
 `;
-
-const SearchBar = styled.div`
-  display: flex;
-  flex: 1;
-
-  > i {
-    color: #aaaabb;
-    padding-top: 1px;
-    margin-left: 13px;
-    font-size: 18px;
-    margin-right: 8px;
-  }
-`;
-
-const SearchInput = styled.input`
-  outline: none;
-  border: none;
-  font-size: 13px;
-  background: none;
-  width: 100%;
-  color: white;
-  height: 20px;
-`;

+ 58 - 3
server/api/git_repo_handler.go

@@ -201,16 +201,71 @@ func (app *App) HandleGetBranches(w http.ResponseWriter, r *http.Request) {
 	client := github.NewClient(app.GithubProjectConf.Client(oauth2.NoContext, tok))
 
 	// List all branches for a specified repo
-	branches, _, err := client.Repositories.ListBranches(context.Background(), owner, name, &github.ListOptions{
+	allBranches, resp, err := client.Repositories.ListBranches(context.Background(), owner, name, &github.ListOptions{
 		PerPage: 100,
 	})
 
 	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	// make workers to get branches concurrently
+	const WCOUNT = 5
+	numPages := resp.LastPage + 1
+	var workerErr error
+	var mu sync.Mutex
+	var wg sync.WaitGroup
+
+	worker := func(cp int) {
+		defer wg.Done()
+
+		for cp < numPages {
+			opts := &github.ListOptions{
+				Page:    cp,
+				PerPage: 100,
+			}
+
+			branches, _, err := client.Repositories.ListBranches(context.Background(), owner, name, opts)
+
+			if err != nil {
+				mu.Lock()
+				workerErr = err
+				mu.Unlock()
+				return
+			}
+
+			mu.Lock()
+			allBranches = append(allBranches, branches...)
+			mu.Unlock()
+
+			cp += WCOUNT
+		}
+	}
+
+	var numJobs int
+	if numPages > WCOUNT {
+		numJobs = WCOUNT
+	} else {
+		numJobs = numPages
+	}
+
+	wg.Add(numJobs)
+
+	// page 1 is already loaded so we start with 2
+	for i := 1; i <= numJobs; i++ {
+		go worker(i + 1)
+	}
+
+	wg.Wait()
+
+	if workerErr != nil {
+		app.handleErrorInternal(workerErr, w)
 		return
 	}
 
-	res := []string{}
-	for _, b := range branches {
+	res := make([]string, 0)
+	for _, b := range allBranches {
 		res = append(res, b.GetName())
 	}