diff --git a/api/api.go b/api/api.go index 56901b1d..9d09632c 100644 --- a/api/api.go +++ b/api/api.go @@ -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. @@ -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 { diff --git a/db/db.go b/db/db.go index 5cf0af3b..315493bc 100644 --- a/db/db.go +++ b/db/db.go @@ -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{} diff --git a/main.go b/main.go index 41465768..71a78b4d 100644 --- a/main.go +++ b/main.go @@ -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"} diff --git a/static/terraboard-vuejs/src/views/Lineage.vue b/static/terraboard-vuejs/src/views/Lineage.vue index cb000bf4..505690ef 100644 --- a/static/terraboard-vuejs/src/views/Lineage.vue +++ b/static/terraboard-vuejs/src/views/Lineage.vue @@ -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 diff --git a/static/terraboard-vuejs/src/views/Plan.vue b/static/terraboard-vuejs/src/views/Plan.vue index aa669cdf..16aeab10 100644 --- a/static/terraboard-vuejs/src/views/Plan.vue +++ b/static/terraboard-vuejs/src/views/Plan.vue @@ -9,7 +9,7 @@
  • @@ -19,12 +19,15 @@ -
    +
    +
    + +
    @@ -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) => { @@ -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) => { @@ -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(); diff --git a/static/terraboard-vuejs/src/views/PlansExplorer.vue b/static/terraboard-vuejs/src/views/PlansExplorer.vue index 025c44c1..3179da2b 100644 --- a/static/terraboard-vuejs/src/views/PlansExplorer.vue +++ b/static/terraboard-vuejs/src/views/PlansExplorer.vue @@ -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) => {