Parcourir la source

add delete integrations from dashboard, search across github repos, and remove duplicates

Alexander Belanger il y a 5 ans
Parent
commit
55c0ca94d9

+ 122 - 48
dashboard/src/components/repo-selector/RepoList.tsx

@@ -20,13 +20,15 @@ type StateType = {
   repos: RepoType[];
   loading: boolean;
   error: boolean;
+  searchFilter: string;
 };
 
-export default class ActionConfEditor extends Component<PropsType, StateType> {
+export default class RepoList extends Component<PropsType, StateType> {
   state = {
     repos: [] as RepoType[],
     loading: true,
     error: false,
+    searchFilter: "",
   };
 
   // TODO: Try to unhook before unmount
@@ -37,49 +39,75 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
     if (!this.props.userId && this.props.userId !== 0) {
       api
         .getGitRepos("<token>", {}, { project_id: currentProject.id })
-        .then((res) => {
+        .then(async (res) => {
+          if (res.data.length == 0) {
+            this.setState({ loading: false, error: false });
+            return
+          }
+
           var allRepos: any = [];
-          // TODO: make into promise.all
-          for (let i = 0; i < res.data.length; i++) {
-            var grid = res.data[i].id;
-            api
-              .getGitRepoList(
-                "<token>",
-                {},
-                { project_id: currentProject.id, git_repo_id: grid }
-              )
-              .then((res) => {
-                res.data.forEach((repo: any, id: number) => {
-                  repo.GHRepoID = grid;
-                });
-                allRepos = allRepos.concat(res.data);
-                allRepos.sort((a: any, b: any) => {
-                  if (a.FullName < b.FullName) {
-                    return -1;
-                  } else if (a.FullName > b.FullName) {
-                    return 1;
-                  } else {
-                    return 0;
-                  }
-                });
-                this.setState({
-                  repos: allRepos,
-                  loading: false,
-                  error: false,
+          var errors : any = [];
+
+          var promises = res.data.map((gitrepo: any, id: number) => {
+            return new Promise((resolve, reject) => {
+              api
+                .getGitRepoList(
+                  "<token>",
+                  {},
+                  { project_id: currentProject.id, git_repo_id: gitrepo.id }
+                )
+                .then((res) => {
+                  res.data.forEach((repo: any, id: number) => {
+                    repo.GHRepoID = gitrepo.id;
+                  });
+
+                  resolve(res.data)
+                })
+                .catch((err) => {
+                  errors.push(err)
+                  resolve([])
                 });
               })
-              .catch((err) => {
-                console.log(err);
-                this.setState({ loading: false, error: true });
-              });
-          }
-          if (res.data.length < 1) {
-            this.setState({ loading: false, error: false });
+            })  
+
+          var sepRepos = await Promise.all(promises);
+
+          allRepos = [].concat.apply([], sepRepos);
+
+          // remove duplicates based on name
+          allRepos = allRepos.filter((repo : any, index : number, self : any) => {
+            var keep = index === self.findIndex((_repo : any) => {
+              return repo.FullName === _repo.FullName
+            })
+
+            return keep
+          })
+
+          // sort repos based on name
+          allRepos.sort((a: any, b: any) => {
+            if (a.FullName < b.FullName) {
+              return -1;
+            } else if (a.FullName > b.FullName) {
+              return 1;
+            } else {
+              return 0;
+            }
+          });
+
+          if (allRepos.length == 0 && errors.length > 0) {
+            this.setState({ loading: false, error: true });
+          } else {
+            this.setState({
+              repos: allRepos,
+              loading: false,
+              error: false,
+            });
           }
         })
         .catch((_) => this.setState({ loading: false, error: true }));
     } else {
       let grid = this.props.userId;
+
       api
         .getGitRepoList(
           "<token>",
@@ -87,14 +115,25 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
           { project_id: currentProject.id, git_repo_id: grid }
         )
         .then((res) => {
-          res.data.forEach((repo: any, id: number) => {
+          var repos : any = res.data
+
+          repos.forEach((repo: any, id: number) => {
             repo.GHRepoID = grid;
           });
-          // TODO: sort repos alphabetically
-          this.setState({ repos: res.data, loading: false, error: false });
+
+          repos.sort((a: any, b: any) => {
+            if (a.FullName < b.FullName) {
+              return -1;
+            } else if (a.FullName > b.FullName) {
+              return 1;
+            } else {
+              return 0;
+            }
+          });
+
+          this.setState({ repos: repos, loading: false, error: false });
         })
         .catch((err) => {
-          console.log(err);
           this.setState({ loading: false, error: true });
         });
     }
@@ -132,7 +171,9 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
       );
     }
 
-    return repos.map((repo: RepoType, i: number) => {
+    return repos.filter((repo: RepoType, i: number) => {
+      return repo.FullName.includes(this.state.searchFilter || "")
+    }).map((repo: RepoType, i: number) => {
       return (
         <RepoName
           key={i}
@@ -150,7 +191,7 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
 
   renderExpanded = () => {
     if (this.props.readOnly) {
-      return <ExpandedWrapperAlt>{this.renderRepoList()}</ExpandedWrapperAlt>;
+      return <ExpandedWrapperAlt>{this.renderRepoList()}</ExpandedWrapperAlt>
     } else {
       return (
         <ExpandedWrapper>
@@ -159,10 +200,18 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
             lastItem={false}
             readOnly={this.props.readOnly}
           >
-            <img src={info} />
-            Select Repo
+            <i className="material-icons">search</i>
+            <SearchInput 
+              value={this.state.searchFilter}
+              onChange={(e: any) => {
+                this.setState({ searchFilter: e.target.value });
+              }}
+              placeholder="Search repos..."
+            />
           </InfoRow>
-          {this.renderRepoList()}
+          <ExpandedWrapper>
+            {this.renderRepoList()}
+          </ExpandedWrapper>
         </ExpandedWrapper>
       );
     }
@@ -173,7 +222,7 @@ export default class ActionConfEditor extends Component<PropsType, StateType> {
   }
 }
 
-ActionConfEditor.contextType = Context;
+RepoList.contextType = Context;
 
 const RepoName = styled.div`
   display: flex;
@@ -209,11 +258,12 @@ const RepoName = styled.div`
     }
   }
 
-  > img {
+  > img,i {
     width: 18px;
     height: 18px;
     margin-left: 12px;
     margin-right: 12px;
+    font-size: 20px;
   }
 `;
 
@@ -222,6 +272,10 @@ const InfoRow = styled(RepoName)`
   color: #ffffff55;
   :hover {
     background: #ffffff11;
+
+    > i {
+      background: none;
+    }
   }
 `;
 
@@ -239,7 +293,16 @@ const ExpandedWrapper = styled.div`
   width: 100%;
   border-radius: 3px;
   border: 0px solid #ffffff44;
-  max-height: 275px;
+  max-height: 235px;
+  top: 40px; 
+
+  > i {
+    font-size: 18px;
+    display: block;
+    position: absolute; 
+    left: 10px; 
+    top: 10px; 
+  }
 `;
 
 const ExpandedWrapperAlt = styled(ExpandedWrapper)`
@@ -254,3 +317,14 @@ const A = styled.a`
   margin-left: 5px;
   cursor: pointer;
 `;
+
+const SearchInput = styled.input`
+  outline: none;
+  border: none;
+  font-size: 13px;
+  background: none;
+  width: 100%;
+  color: white;
+  padding: 0;
+  height: 20px; 
+`;

+ 2 - 0
dashboard/src/main/home/integrations/IntegrationCategories.tsx

@@ -159,6 +159,7 @@ class IntegrationCategories extends Component<PropsType, StateType> {
             integrations={this.state.currentOptions}
             titles={this.state.currentTitles}
             itemIdentifier={this.state.currentIntegrationData}
+            updateIntegrationList={() => this.getIntegrationsForCategory(this.props.category)}
           />
         </div>
       );
@@ -195,6 +196,7 @@ class IntegrationCategories extends Component<PropsType, StateType> {
             integrations={this.state.currentOptions}
             titles={this.state.currentTitles}
             itemIdentifier={this.state.currentIds}
+            updateIntegrationList={() => this.getIntegrationsForCategory(this.props.category)}
           />
         </div>
       );

+ 61 - 0
dashboard/src/main/home/integrations/IntegrationList.tsx

@@ -4,6 +4,8 @@ import styled from "styled-components";
 import { Context } from "shared/Context";
 import { integrationList } from "shared/common";
 import IntegrationRow from "./IntegrationRow";
+import ConfirmOverlay from "components/ConfirmOverlay";
+import api from "shared/api";
 
 type PropsType = {
   setCurrent?: (x: string) => void;
@@ -12,15 +14,22 @@ type PropsType = {
   itemIdentifier?: any[];
   titles?: string[];
   isCategory?: boolean;
+  updateIntegrationList: () => void;
 };
 
 type StateType = {
   displayExpanded: boolean[];
+  isDelete: boolean;
+  deleteName: string;
+  deleteID: number;
 };
 
 export default class IntegrationList extends Component<PropsType, StateType> {
   state = {
     displayExpanded: this.props.integrations.map(() => false),
+    isDelete: false,
+    deleteName: "",
+    deleteID: 0,
   };
 
   allCollapsed = () =>
@@ -51,14 +60,59 @@ export default class IntegrationList extends Component<PropsType, StateType> {
     this.setState({ displayExpanded: x });
   };
 
+  triggerDelete = (event: MouseEvent, i: number, id: number) => {
+    if (event) {
+      event.stopPropagation();
+    }
+
+    this.setState({ isDelete: true, deleteName: this.props.titles[i], deleteID: id })
+  }
+
+  handleDeleteIntegration = () => {
+    let { currentProject } = this.context;
+
+    if (this.props.currentCategory === "registry") {
+      api.deleteRegistryIntegration(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          registry_id: this.state.deleteID,
+        }
+      ).then(() => {
+        this.setState({ isDelete: false })
+        this.props.updateIntegrationList()
+      }).catch((err) => {
+        this.context.setCurrentError(err)
+      })
+    } else if (this.props.currentCategory === "repo") {
+      api.deleteGitRepoIntegration(
+        "<token>",
+        {},
+        {
+          project_id: currentProject.id,
+          git_repo_id: this.state.deleteID,
+        }
+      ).then(() => {
+        this.setState({ isDelete: false })
+        this.props.updateIntegrationList()
+      }).catch((err) => {
+        this.context.setCurrentError(err)
+      })
+    }
+  }
+
   handleParent = (event: any, integration: string) =>
     this.props.setCurrent && this.props.setCurrent(integration);
 
   renderContents = () => {
     let { integrations, titles, setCurrent, isCategory } = this.props;
+
     if (titles && titles.length > 0) {
       return integrations.map((integration: string, i: number) => {
         let label = titles[i];
+        let item_id = this.props.itemIdentifier[i].id || this.props.itemIdentifier[i]
+
         return (
           <IntegrationRow
             category={this.props.currentCategory}
@@ -68,6 +122,7 @@ export default class IntegrationList extends Component<PropsType, StateType> {
             itemId={this.props.itemIdentifier[i]}
             label={label}
             toggleCollapse={(e: MouseEvent) => this.toggleDisplay(e, i)}
+            triggerDelete={(e: MouseEvent) => this.triggerDelete(e, i, item_id)}
           ></IntegrationRow>
         );
       });
@@ -123,6 +178,12 @@ export default class IntegrationList extends Component<PropsType, StateType> {
   render() {
     return (
       <StyledIntegrationList>
+        <ConfirmOverlay
+          show={this.state.isDelete}
+          message={`Are you sure you want to delete the ${this.props.currentCategory === "registry" ? "Docker registry integration" : "Github integration"} with name ${this.state.deleteName}?`}
+          onYes={this.handleDeleteIntegration}
+          onNo={() => this.setState({ isDelete: false })}
+        />
         {this.props.titles && this.props.titles.length > 0 && (
           <ControlRow>{this.collapseAllButton()}</ControlRow>
         )}

+ 9 - 2
dashboard/src/main/home/integrations/IntegrationRow.tsx

@@ -10,6 +10,7 @@ import CreateIntegrationForm from "./create-integration/CreateIntegrationForm";
 
 type PropsType = {
   toggleCollapse: MouseEventHandler;
+  triggerDelete: MouseEventHandler;
   label: string;
   integration: string;
   expanded: boolean;
@@ -63,6 +64,12 @@ export default class IntegrationRow extends Component<PropsType, StateType> {
           <MaterialIconTray disabled={false}>
             {/* <i className="material-icons"
             onClick={this.editButtonOnClick}>mode_edit</i> */}
+            <i
+              className="material-icons"
+              onClick={this.props.triggerDelete}
+            >
+              delete
+            </i>
             <I
               className="material-icons"
               showList={this.props.expanded}
@@ -168,8 +175,7 @@ const MainRow = styled.div`
 `;
 
 const MaterialIconTray = styled.div`
-  width: 32px;
-  margin-right: -7px;
+  max-width: 60px;
   display: flex;
   align-items: center;
   justify-content: space-between;
@@ -178,6 +184,7 @@ const MaterialIconTray = styled.div`
     border-radius: 20px;
     font-size: 18px;
     padding: 5px;
+    margin: 0 5px; 
     color: #ffffff44;
     :hover {
       background: ${(props: { disabled: boolean }) =>

+ 1 - 0
dashboard/src/main/home/integrations/Integrations.tsx

@@ -85,6 +85,7 @@ class Integrations extends Component<PropsType, StateType> {
               integrations={["kubernetes", "registry", "repo"]}
               setCurrent={(x) => this.props.history.push(`/integrations/${x}`)}
               isCategory={true}
+              updateIntegrationList={() => {}}
             />
           </div>
         </Route>

+ 22 - 0
dashboard/src/shared/api.tsx

@@ -203,6 +203,16 @@ const deleteCluster = baseApi<
   return `/api/projects/${pathParams.project_id}/clusters/${pathParams.cluster_id}`;
 });
 
+const deleteGitRepoIntegration = baseApi<
+  {},
+  {
+    project_id: number;
+    git_repo_id: number;
+  }
+>("DELETE", (pathParams) => {
+  return `/api/projects/${pathParams.project_id}/gitrepos/${pathParams.git_repo_id}`;
+});
+
 const deleteInvite = baseApi<{}, { id: number; invId: number }>(
   "DELETE",
   (pathParams) => {
@@ -214,6 +224,16 @@ const deleteProject = baseApi<{}, { id: number }>("DELETE", (pathParams) => {
   return `/api/projects/${pathParams.id}`;
 });
 
+const deleteRegistryIntegration = baseApi<
+  {},
+  {
+    project_id: number;
+    registry_id: number;
+  }
+>("DELETE", (pathParams) => {
+  return `/api/projects/${pathParams.project_id}/registries/${pathParams.registry_id}`;
+});
+
 const deployTemplate = baseApi<
   {
     templateName: string;
@@ -672,8 +692,10 @@ export default {
   createPasswordResetFinalize,
   createProject,
   deleteCluster,
+  deleteGitRepoIntegration,
   deleteInvite,
   deleteProject,
+  deleteRegistryIntegration,
   createSubdomain,
   deployTemplate,
   destroyEKS,

+ 20 - 8
server/api/git_repo_handler.go

@@ -70,21 +70,33 @@ func (app *App) HandleListRepos(w http.ResponseWriter, r *http.Request) {
 
 	client := github.NewClient(app.GithubProjectConf.Client(oauth2.NoContext, tok))
 
-	// list all repositories for specified user
-	repos, _, err := client.Repositories.List(context.Background(), "", &github.RepositoryListOptions{
+	allRepos := make([]*github.Repository, 0)
+
+	opt := &github.RepositoryListOptions{
 		ListOptions: github.ListOptions{
 			PerPage: 100,
 		},
 		Sort: "updated",
-	})
+	}
 
-	if err != nil {
-		app.handleErrorInternal(err, w)
-		return
+	for {
+		repos, resp, err := client.Repositories.List(context.Background(), "", opt)
+
+		if err != nil {
+			app.handleErrorInternal(err, w)
+			return
+		}
+
+		allRepos = append(allRepos, repos...)
+
+		if resp.NextPage == 0 {
+			break
+		}
+
+		opt.Page = resp.NextPage
 	}
 
-	// TODO -- check if repo has already been appended -- there may be duplicates
-	for _, repo := range repos {
+	for _, repo := range allRepos {
 		res = append(res, Repo{
 			FullName: repo.GetFullName(),
 			Kind:     "github",