Skip to content

Commit

Permalink
fix(frontend): performence issue on plans fetching (#212)
Browse files Browse the repository at this point in the history
feat(frontend): add loading spinner during plan fetching
  • Loading branch information
hbollon committed Sep 1, 2021
1 parent a5d6050 commit 4e6d2b6
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 17 deletions.
50 changes: 49 additions & 1 deletion api/api.go
Expand Up @@ -274,6 +274,50 @@ func SubmitPlan(w http.ResponseWriter, r *http.Request, db *db.Database) {
}
}

// GetPlansSummary provides summary of all Plan by lineage (only metadata added by the wrapper).
// Optional "&limit=X" parameter to limit requested quantity of plans.
// Optional "&page=X" parameter to add an offset to the query and enable pagination.
// Sorted by most recent to oldest.
// /api/plans/summary GET endpoint callback
// Also return pagination informations (current page ans total items count in database)
func GetPlansSummary(w http.ResponseWriter, r *http.Request, db *db.Database) {
lineage := r.URL.Query().Get("lineage")
limit := r.URL.Query().Get("limit")
page := r.URL.Query().Get("page")
plans, currentPage, total := db.GetPlansSummary(lineage, limit, page)

response := make(map[string]interface{})
response["plans"] = plans
response["page"] = currentPage
response["total"] = total
j, err := json.Marshal(response)
if err != nil {
log.Errorf("Failed to marshal plans: %v", err)
JSONError(w, "Failed to marshal plans", err)
return
}
if _, err := io.WriteString(w, string(j)); err != nil {
log.Error(err.Error())
}
}

// GetPlan provides a specific Plan of a lineage using ID.
// /api/plans GET endpoint callback on request with ?plan_id=X parameter
func GetPlan(w http.ResponseWriter, r *http.Request, db *db.Database) {
id := r.URL.Query().Get("planid")
plan := db.GetPlan(id)

j, err := json.Marshal(plan)
if err != nil {
log.Errorf("Failed to marshal plan: %v", err)
JSONError(w, "Failed to marshal plan", err)
return
}
if _, err := io.WriteString(w, string(j)); err != nil {
log.Error(err.Error())
}
}

// GetPlans provides all Plan by lineage.
// Optional "&limit=X" parameter to limit requested quantity of plans.
// Optional "&page=X" parameter to add an offset to the query and enable pagination.
Expand Down Expand Up @@ -305,7 +349,11 @@ func GetPlans(w http.ResponseWriter, r *http.Request, db *db.Database) {
// on /api/plans request
func ManagePlans(w http.ResponseWriter, r *http.Request, db *db.Database) {
if r.Method == "GET" {
GetPlans(w, r, db)
if r.URL.Query().Get("planid") != "" {
GetPlan(w, r, db)
} else {
GetPlans(w, r, db)
}
} else if r.Method == "POST" {
SubmitPlan(w, r, db)
} else {
Expand Down
77 changes: 77 additions & 0 deletions db/db.go
Expand Up @@ -619,6 +619,83 @@ func (db *Database) InsertPlan(plan []byte) error {
return db.Create(&p).Error
}

// GetPlansSummary retrieves a summary of all Plans of a lineage from the database
func (db *Database) GetPlansSummary(lineage, limitStr, pageStr string) (plans []types.Plan, page int, total int) {
var whereClause []interface{}
var whereClauseTotal string
if lineage != "" {
whereClause = append(whereClause, `"Lineage"."value" = ?`, lineage)
whereClauseTotal = ` JOIN lineages on lineages.id=t.lineage_id WHERE lineages.value = ?`
}

row := db.Raw("SELECT count(*) FROM plans AS t"+whereClauseTotal, lineage).Row()
if err := row.Scan(&total); err != nil {
log.Error(err.Error())
}

var limit int
if limitStr == "" {
limit = -1
} else {
var err error
limit, err = strconv.Atoi(limitStr)
if err != nil {
log.Warnf("GetPlans limit ignored: %v", err)
limit = -1
}
}

var offset int
if pageStr == "" {
offset = -1
} else {
var err error
page, err = strconv.Atoi(pageStr)
if err != nil {
log.Warnf("GetPlans offset ignored: %v", err)
} else {
offset = (page - 1) * pageSize
}
}

db.Select(`"plans"."id"`, `"plans"."created_at"`, `"plans"."updated_at"`, `"plans"."tf_version"`,
`"plans"."git_remote"`, `"plans"."git_commit"`, `"plans"."ci_url"`, `"plans"."source"`).
Joins("Lineage").
Order("created_at desc").
Limit(limit).
Offset(offset).
Find(&plans, whereClause...)

return
}

// GetPlan retrieves a specific Plan by his ID from the database
func (db *Database) GetPlan(id string) (plans types.Plan) {
db.Joins("Lineage").
Preload("ParsedPlan").
Preload("ParsedPlan.PlanStateValue").
Preload("ParsedPlan.PlanStateValue.PlanStateOutputs").
Preload("ParsedPlan.PlanStateValue.PlanStateModule").
Preload("ParsedPlan.PlanStateValue.PlanStateModule.PlanStateResources").
Preload("ParsedPlan.PlanStateValue.PlanStateModule.PlanStateResources.PlanStateResourceAttributes").
Preload("ParsedPlan.PlanStateValue.PlanStateModule.PlanStateModules").
Preload("ParsedPlan.Variables").
Preload("ParsedPlan.PlanResourceChanges").
Preload("ParsedPlan.PlanResourceChanges.Change").
Preload("ParsedPlan.PlanOutputs").
Preload("ParsedPlan.PlanOutputs.Change").
Preload("ParsedPlan.PlanState").
Preload("ParsedPlan.PlanState.PlanStateValue").
Preload("ParsedPlan.PlanState.PlanStateValue.PlanStateOutputs").
Preload("ParsedPlan.PlanState.PlanStateValue.PlanStateModule").
Preload("ParsedPlan.PlanState.PlanStateValue.PlanStateModule.PlanStateResources").
Preload("ParsedPlan.PlanState.PlanStateValue.PlanStateModule.PlanStateResources.PlanStateResourceAttributes").
Preload("ParsedPlan.PlanState.PlanStateValue.PlanStateModule.PlanStateModules").
Find(&plans, `"plans"."id" = ?`, id)

return
}

// GetPlans retrieves all Plan of a lineage from the database
func (db *Database) GetPlans(lineage, limitStr, pageStr string) (plans []types.Plan, page int, total int) {
var whereClause []interface{}
Expand Down
1 change: 1 addition & 0 deletions main.go
Expand Up @@ -189,6 +189,7 @@ func main() {
apiRouter.HandleFunc(util.GetFullPath("attribute/keys"), handleWithDB(api.ListAttributeKeys, database))
apiRouter.HandleFunc(util.GetFullPath("tf_versions"), handleWithDB(api.ListTfVersions, database))
apiRouter.HandleFunc(util.GetFullPath("plans"), handleWithDB(api.ManagePlans, database))
apiRouter.HandleFunc(util.GetFullPath("plans/summary"), handleWithDB(api.GetPlansSummary, database))

// Serve static files (CSS, JS, images) from dir
spa := spaHandler{staticPath: "static", indexPath: "index.html"}
Expand Down
2 changes: 1 addition & 1 deletion static/terraboard-vuejs/src/views/Lineage.vue
Expand Up @@ -109,7 +109,7 @@ class ObjWrapper {
this.data.push(entry)
});
const url = `/api/plans?lineage=`+this.lineage;
const url = `/api/plans/summary?lineage=`+this.lineage;
axios.get(url)
.then((response) => {
// handle success
Expand Down
49 changes: 35 additions & 14 deletions static/terraboard-vuejs/src/views/Plan.vue
Expand Up @@ -9,7 +9,7 @@
<li
v-for="plan in plans"
v-bind:key="plan"
v-bind:class="{ selected: plan == selectedPlan }"
v-bind:class="{ selected: selectedPlan !== undefined && plan.ID == selectedPlan.ID }"
@click="setPlanSelected(plan)"
class="list-group-item plan"
>
Expand All @@ -19,12 +19,15 @@
</div>
</div>
</div>
<div id="node" class="col-xl-8 col-xxl-9">
<div id="node h-100" :key="selectedPlan" class="col-xl-8 col-xxl-9">
<PlanContent
v-if="selectedPlan.parsed_plan !== undefined"
v-if="selectedPlan !== undefined && selectedPlan.parsed_plan !== undefined"
v-bind:plan="selectedPlan"
v-bind:key="selectedPlan"
/>
<div v-else class="h-100 w-100 text-center">
<i class="fas fa-spinner fa-spin fa-5x"></i>
</div>
</div>
</div>
</template>
Expand Down Expand Up @@ -62,8 +65,8 @@ import PlanContent from "../components/PlanContent.vue";
formatDate(date: string): string {
return new Date(date).toUTCString();
},
fetchLatestPlans(limit: number): void {
const url = `/api/plans?limit=`+limit+`&lineage=`+this.url.lineage;
fetchLatestPlansSummary(limit: number): void {
const url = `/api/plans/summary?limit=`+limit+`&lineage=`+this.url.lineage;
axios
.get(url)
.then((response) => {
Expand All @@ -79,7 +82,7 @@ import PlanContent from "../components/PlanContent.vue";
}
});
if (planFinded === false) {
const url = `/api/plans?lineage=`+this.url.lineage;
const url = `/api/plans/summary?lineage=`+this.url.lineage;
axios
.get(url)
.then((response) => {
Expand Down Expand Up @@ -121,20 +124,38 @@ import PlanContent from "../components/PlanContent.vue";
});
},
setPlanSelected(plan: any): void {
this.selectedPlan = plan;
router.replace({
path: `/lineage/${this.url.lineage}/plans`,
query: {
planid: plan.ID,
},
});
this.selectedPlan = undefined;
const url = `/api/plans?planid=`+plan.ID;
axios
.get(url)
.then((response) => {
this.selectedPlan = response.data;
router.replace({
path: `/lineage/${this.url.lineage}/plans`,
query: {
planid: this.selectedPlan.ID,
},
});
})
.catch(function(err) {
if (err.response) {
console.log("Server Error:", err);
} else if (err.request) {
console.log("Network Error:", err);
} else {
console.log("Client Error:", err);
}
})
.then(function() {
// always executed
});
},
},
created() {
this.updateTitle();
this.url.lineage = this.$route.params.lineage;
this.url.planid = router.currentRoute.value.query.planid;
this.fetchLatestPlans(10);
this.fetchLatestPlansSummary(10);
},
updated() {
hljs.highlightAll();
Expand Down
2 changes: 1 addition & 1 deletion static/terraboard-vuejs/src/views/PlansExplorer.vue
Expand Up @@ -161,7 +161,7 @@ import router from "../router";
.join("&");
router.push({ name: "PlansExplorer", query: params });
const url = `/api/plans?` + query;
const url = `/api/plans/summary?` + query;
axios
.get(url)
.then((response) => {
Expand Down

0 comments on commit 4e6d2b6

Please sign in to comment.