فهرست منبع

[POR-1503] Refactor project retrieval (#3504)

jose-fully-ported 2 سال پیش
والد
کامیت
52b9547953

+ 2 - 2
api/server/handlers/project/list.go

@@ -35,10 +35,10 @@ func (p *ProjectListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	res := make([]*types.Project, len(projects))
+	res := make([]*types.ProjectList, len(projects))
 
 	for i, proj := range projects {
-		res[i] = proj.ToProjectType()
+		res[i] = proj.ToProjectListType()
 	}
 
 	p.WriteResult(w, r, res)

+ 4 - 4
api/server/handlers/project/list_test.go

@@ -40,11 +40,11 @@ func TestListProjectsSuccessful(t *testing.T) {
 
 	handler.ServeHTTP(rr, req)
 
-	expProjects := make([]*types.Project, 0)
+	expProjects := make([]*types.ProjectList, 0)
 
-	expProjects = append(expProjects, proj1.ToProjectType())
-	expProjects = append(expProjects, proj2.ToProjectType())
-	gotProjects := []*types.Project{}
+	expProjects = append(expProjects, proj1.ToProjectListType())
+	expProjects = append(expProjects, proj2.ToProjectListType())
+	gotProjects := []*types.ProjectList{}
 
 	apitest.AssertResponseExpected(t, rr, &expProjects, &gotProjects)
 }

+ 23 - 0
api/types/project.go

@@ -1,5 +1,28 @@
 package types
 
+// ProjectList type for entries in the api response on GET /projects
+type ProjectList struct {
+	ID   uint   `json:"id"`
+	Name string `json:"name"`
+
+	// note: all of these fields should be considered deprecated
+	// in an api response
+	Roles                  []Role `json:"roles"`
+	PreviewEnvsEnabled     bool   `json:"preview_envs_enabled"`
+	RDSDatabasesEnabled    bool   `json:"enable_rds_databases"`
+	ManagedInfraEnabled    bool   `json:"managed_infra_enabled"`
+	APITokensEnabled       bool   `json:"api_tokens_enabled"`
+	StacksEnabled          bool   `json:"stacks_enabled"`
+	CapiProvisionerEnabled bool   `json:"capi_provisioner_enabled"`
+	SimplifiedViewEnabled  bool   `json:"simplified_view_enabled"`
+	AzureEnabled           bool   `json:"azure_enabled"`
+	HelmValuesEnabled      bool   `json:"helm_values_enabled"`
+	MultiCluster           bool   `json:"multi_cluster"`
+	FullAddOns             bool   `json:"full_add_ons"`
+	EnableReprovision      bool   `json:"enable_reprovision"`
+	ValidateApplyV2        bool   `json:"validate_apply_v2"`
+}
+
 type Project struct {
 	ID                     uint    `json:"id"`
 	Name                   string  `json:"name"`

+ 2 - 1
api/types/user.go

@@ -66,7 +66,8 @@ type FinalizeResetUserPasswordRequest struct {
 	NewPassword string `json:"new_password" form:"required,max=255"`
 }
 
-type ListUserProjectsResponse []*Project
+// ListUserProjectsResponse type for api responses to GET /projects
+type ListUserProjectsResponse []*ProjectList
 
 type WelcomeWebhookRequest struct {
 	Email     string `json:"email" schema:"email"`

+ 37 - 41
dashboard/src/main/home/Home.tsx

@@ -8,7 +8,7 @@ import midnight from "shared/themes/midnight";
 import standard from "shared/themes/standard";
 import { Context } from "shared/Context";
 import { PorterUrl, pushFiltered, pushQueryParams } from "shared/routing";
-import { ClusterType, ProjectType } from "shared/types";
+import { ClusterType, ProjectType, ProjectListType } from "shared/types";
 
 import ConfirmOverlay from "components/ConfirmOverlay";
 import Loading from "components/Loading";
@@ -39,7 +39,6 @@ import Spacer from "components/porter/Spacer";
 import Button from "components/porter/Button";
 import NewAppFlow from "./app-dashboard/new-app-flow/NewAppFlow";
 import ExpandedApp from "./app-dashboard/expanded-app/ExpandedApp";
-import ExpandedJob from "./app-dashboard/expanded-app/expanded-job/ExpandedJob";
 import CreateApp from "./app-dashboard/create-app/CreateApp";
 import AppView from "./app-dashboard/app-view/AppView";
 
@@ -116,7 +115,7 @@ const Home: React.FC<Props> = (props) => {
       });
   };
 
-  const getProjects = (id?: number) => {
+  const getProjects = async (id?: number) => {
     let { currentProject } = props;
     let queryString = window.location.search;
     let urlParams = new URLSearchParams(queryString);
@@ -125,39 +124,29 @@ const Home: React.FC<Props> = (props) => {
       pushQueryParams(props, { project_id: currentProject.id.toString() });
     }
 
-    api
-      .getProjects("<token>", {}, { id: user.userId })
-      .then((res) => {
-        if (res.data) {
-          if (res.data.length === 0) {
-            redirectToNewProject();
-          } else if (res.data.length > 0 && !currentProject) {
-            setProjects(res.data);
-
-            let foundProject = null;
-            if (id) {
-              res.data.forEach((project: ProjectType, i: number) => {
-                if (project.id === id) {
-                  foundProject = project;
-                }
-              });
-              setCurrentProject(foundProject || res.data[0]);
-            }
-            if (!foundProject) {
-              res.data.forEach((project: ProjectType, i: number) => {
-                if (
-                  project.id.toString() ===
-                  localStorage.getItem("currentProject")
-                ) {
-                  foundProject = project;
-                }
-              });
-              setCurrentProject(foundProject || res.data[0]);
-            }
-          }
-        }
-      })
-      .catch(console.log);
+    try {
+      const projectList = await api
+        .getProjects("<token>", {}, { id: user.userId })
+        .then((res) => res.data as ProjectListType[]);
+
+      if (projectList.length === 0) {
+        redirectToNewProject();
+      } else if (projectList.length > 0 && !currentProject) {
+        setProjects(projectList);
+
+        id = id ?? Number(localStorage.getItem("currentProject"));
+        const foundProjectListEntry = projectList.find(
+          (item: ProjectListType) => item.id === id
+        );
+
+        const project = await api
+          .getProject("<token>", {}, { id: foundProjectListEntry?.id || projectList[0].id })
+          .then((res) => res.data as ProjectType);
+        setCurrentProject(project);
+      }
+    } catch (error) {
+      console.log(error);
+    }
   };
 
   const checkIfCanCreateProject = () => {
@@ -311,17 +300,24 @@ const Home: React.FC<Props> = (props) => {
 
   const projectOverlayCall = async () => {
     try {
-      const res = await api.getProjects("<token>", {}, { id: user.userId });
-      if (!res.data) {
+      const projectList = await api
+        .getProjects("<token>", {}, { id: user.userId })
+        .then((res) => res.data as ProjectListType[]);
+
+      if (!projectList) {
         setCurrentModal(null, null);
         return;
       }
 
-      setProjects(res.data);
-      if (!res.data.length) {
+      setProjects(projectList);
+      if (!projectList.length) {
         setCurrentProject(null, () => redirectToNewProject());
       } else {
-        setCurrentProject(res.data[0]);
+        const project = await api
+          .getProject("<token>", {}, { id: projectList[0].id })
+          .then((res) => res.data as ProjectType);
+
+        setCurrentProject(project);
       }
       setCurrentModal(null, null);
     } catch (error) {

+ 2 - 2
dashboard/src/main/home/new-project/NewProject.tsx

@@ -17,6 +17,7 @@ import Helper from "components/form-components/Helper";
 import TitleSection from "components/TitleSection";
 import WelcomeForm from "./WelcomeForm";
 import { trackCreateNewProject } from "shared/anayltics";
+import { ProjectListType } from "shared/types";
 
 type ValidationError = {
   hasError: boolean;
@@ -30,7 +31,6 @@ export const NewProjectFC = () => {
     setCurrentProject,
     canCreateProject,
     projects,
-    capabilities,
   } = useContext(Context);
   const { pushFiltered } = useRouting();
   const [buttonStatus, setButtonStatus] = useState("");
@@ -96,7 +96,7 @@ export const NewProjectFC = () => {
             id: user.userId,
           }
         )
-        .then((res) => res.data);
+        .then((res) => res.data as ProjectListType[]);
       setProjects(projectList);
       setCurrentProject(project);
       setButtonStatus("successful");

+ 14 - 8
dashboard/src/main/home/sidebar/ProjectSection.tsx

@@ -2,14 +2,15 @@ import React, { Component } from "react";
 import styled from "styled-components";
 import gradient from "assets/gradient.png";
 
+import api from "shared/api"; 
 import { Context } from "shared/Context";
-import { ProjectType } from "shared/types";
+import { ProjectListType, ProjectType } from "shared/types";
 import { pushFiltered } from "shared/routing";
 import { RouteComponentProps, withRouter } from "react-router";
 
 type PropsType = RouteComponentProps & {
   currentProject: ProjectType;
-  projects: ProjectType[];
+  projects: ProjectListType[];
 };
 
 type StateType = {
@@ -46,16 +47,21 @@ class ProjectSection extends Component<PropsType, StateType> {
 
   renderOptionList = () => {
     let { setCurrentProject, setCurrentCluster, currentProject } = this.context;
-    return this.props.projects.map((project: ProjectType, i: number) => {
+    return this.props.projects.map((projectListEntry: ProjectListType, i: number) => {
       return (
         <Option
           key={i}
-          selected={project.name === this.props.currentProject.name}
-          onClick={() => {
+          selected={projectListEntry.name === this.props.currentProject.name}
+          onClick={async () => {
             this.setState({ expanded: false });
-            if (project.id !== currentProject.id) {
+            if (projectListEntry.id !== currentProject.id) {
               setCurrentCluster(null);
             }
+
+            const project = await api
+              .getProject("<token>", {}, { id: projectListEntry.id })
+              .then((res) => res.data as ProjectType);
+
             setCurrentProject(project, () => {
               pushFiltered(this.props, "/dashboard", ["project_id"]);
             });
@@ -63,9 +69,9 @@ class ProjectSection extends Component<PropsType, StateType> {
         >
           <ProjectIcon>
             <ProjectImage src={gradient} />
-            <Letter>{project.name[0].toUpperCase()}</Letter>
+            <Letter>{projectListEntry.name[0].toUpperCase()}</Letter>
           </ProjectIcon>
-          <ProjectLabel>{project.name}</ProjectLabel>
+          <ProjectLabel>{projectListEntry.name}</ProjectLabel>
         </Option>
       );
     });

+ 7 - 6
dashboard/src/main/home/sidebar/ProjectSelectionModal.tsx

@@ -7,10 +7,8 @@ import Text from "components/porter/Text";
 import Spacer from "components/porter/Spacer";
 import { Context } from "shared/Context";
 import { DetailedClusterType, ProjectType } from "shared/types";
-import gradient from "assets/gradient.png";
 import { pushFiltered } from "shared/routing";
 import SearchBar from "components/porter/SearchBar";
-import { search } from "shared/search";
 import _ from 'lodash';
 import { useMemo } from 'react';
 import api from "shared/api";
@@ -90,12 +88,15 @@ const ProjectSelectionModal: React.FC<Props> = ({
     }
   };
   const renderBlockList = () => {
-    return filteredProjects.map((project: ProjectType, i: number) => {
+    return filteredProjects.map((projectListEntry: ProjectListType, i: number) => {
       return (
         <IdContainer
           key={i}
-          selected={project.id === currentProject.id}
+          selected={projectListEntry.id === currentProject.id}
           onClick={async () => {
+            const project = await api
+              .getProject("<token>", {}, { id: projectListEntry.id })
+              .then((res) => res.data as ProjectType);
 
             setCurrentProject(project);
 
@@ -115,11 +116,11 @@ const ProjectSelectionModal: React.FC<Props> = ({
           }}
         >
           {/* <BlockIcon src={gradient} /> */}
-          <BlockTitle>{project.name}</BlockTitle>
+          <BlockTitle>{projectListEntry.name}</BlockTitle>
 
 
           <BlockDescription>
-            Project ID: {project.id}
+            Project ID: {projectListEntry.id}
           </BlockDescription>
         </IdContainer>
       );

+ 4 - 3
dashboard/src/shared/Context.tsx

@@ -4,6 +4,7 @@ import {
   CapabilityType,
   ClusterType,
   ContextProps,
+  ProjectListType,
   ProjectType,
   UsageData,
 } from "shared/types";
@@ -42,8 +43,8 @@ export interface GlobalContextType {
     currentProject: ProjectType,
     callback?: () => void
   ) => void;
-  projects: ProjectType[];
-  setProjects: (projects: ProjectType[]) => void;
+  projects: ProjectListType[];
+  setProjects: (projects: ProjectListType[]) => void;
   user: any;
   setUser: (userId: number, email: string) => void;
   devOpsMode: boolean;
@@ -127,7 +128,7 @@ class ContextProvider extends Component<PropsType, StateType> {
       });
     },
     projects: [],
-    setProjects: (projects: ProjectType[]) => {
+    setProjects: (projects: ProjectListType[]) => {
       projects.sort((a: any, b: any) => (a.name > b.name ? 1 : -1));
       this.setState({ projects });
     },

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

@@ -1522,6 +1522,10 @@ const getProjectRepos = baseApi<{}, { id: number }>("GET", (pathParams) => {
 
 const getProjects = baseApi("GET", "/api/projects");
 
+const getProject = baseApi<{}, { id: number }>("GET", (pathParams) => {
+  return `/api/projects/${pathParams.id}`;
+});
+
 const getPrometheusIsInstalled = baseApi<
   {},
   {
@@ -3022,6 +3026,7 @@ export default {
   getProjectRegistries,
   getProjectRepos,
   getProjects,
+  getProject,
   getPrometheusIsInstalled,
   getRegistryIntegrations,
   getReleaseToken,

+ 6 - 2
dashboard/src/shared/types.tsx

@@ -259,6 +259,10 @@ export interface FileType {
   path: string;
   type: string;
 }
+export interface ProjectListType {
+  id: number;
+  name: string;
+}
 
 export interface ProjectType {
   id: number;
@@ -369,8 +373,8 @@ export interface ContextProps {
   setCurrentCluster: (currentCluster: ClusterType, callback?: any) => void;
   currentProject?: ProjectType;
   setCurrentProject: (currentProject: ProjectType, callback?: any) => void;
-  projects: ProjectType[];
-  setProjects: (projects: ProjectType[]) => void;
+  projects: ProjectListType[];
+  setProjects: (projects: ProjectListType[]) => void;
   user: any;
   setUser: (userId: number, email: string) => void;
   devOpsMode: boolean;

+ 33 - 0
internal/models/project.go

@@ -100,3 +100,36 @@ func (p *Project) ToProjectType() *types.Project {
 		FullAddOns:             p.FullAddOns,
 	}
 }
+
+// ToProjectListType returns a "minified" version of a Project
+// suitable for api responses to GET /projects
+// TODO: update this in the future to use default values for all
+// the feature flags instead of trying to retrieve them from the database
+func (p *Project) ToProjectListType() *types.ProjectList {
+	var roles []types.Role
+	for _, role := range p.Roles {
+		roles = append(roles, *role.ToRoleType())
+	}
+
+	return &types.ProjectList{
+		ID:   p.ID,
+		Name: p.Name,
+
+		// note: all of these fields should be considered deprecated
+		// in an api response
+		Roles:                  roles,
+		PreviewEnvsEnabled:     p.PreviewEnvsEnabled,
+		RDSDatabasesEnabled:    p.RDSDatabasesEnabled,
+		ManagedInfraEnabled:    p.ManagedInfraEnabled,
+		StacksEnabled:          p.StacksEnabled,
+		APITokensEnabled:       p.APITokensEnabled,
+		CapiProvisionerEnabled: p.CapiProvisionerEnabled,
+		SimplifiedViewEnabled:  p.SimplifiedViewEnabled,
+		AzureEnabled:           p.AzureEnabled,
+		HelmValuesEnabled:      p.HelmValuesEnabled,
+		MultiCluster:           p.MultiCluster,
+		EnableReprovision:      p.EnableReprovision,
+		ValidateApplyV2:        p.ValidateApplyV2,
+		FullAddOns:             p.FullAddOns,
+	}
+}