385 lines
9.4 KiB
Go
385 lines
9.4 KiB
Go
package web
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
cairnapi "github.com/mattnite/cairn/internal/api"
|
|
"github.com/mattnite/cairn/internal/models"
|
|
"github.com/mattnite/cairn/internal/regression"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type PageHandler struct {
|
|
DB *gorm.DB
|
|
Templates *Templates
|
|
ForgejoURL string
|
|
}
|
|
|
|
type PageData struct {
|
|
Title string
|
|
Content any
|
|
}
|
|
|
|
func (h *PageHandler) Index(c *gin.Context) {
|
|
ctx := c.Request.Context()
|
|
|
|
artifacts, total, err := models.ListArtifacts(ctx, h.DB, models.ListArtifactsParams{Limit: 10})
|
|
if err != nil {
|
|
c.String(http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
repos, err := models.ListRepositories(ctx, h.DB)
|
|
if err != nil {
|
|
c.String(http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
var totalCG, openCG int64
|
|
_ = h.DB.WithContext(ctx).Model(&models.CrashGroup{}).Count(&totalCG).Error
|
|
_ = h.DB.WithContext(ctx).Model(&models.CrashGroup{}).Where("status = ?", "open").Count(&openCG).Error
|
|
|
|
// Top crashers.
|
|
type topCrasher struct {
|
|
CrashGroupID uint
|
|
Title string
|
|
OccurrenceCount uint
|
|
RepoName string
|
|
}
|
|
var topCrashers []topCrasher
|
|
groups, _, err := models.ListCrashGroups(ctx, h.DB, nil, "open", 5, 0)
|
|
if err == nil {
|
|
for _, group := range groups {
|
|
topCrashers = append(topCrashers, topCrasher{
|
|
CrashGroupID: group.ID,
|
|
Title: group.Title,
|
|
OccurrenceCount: group.OccurrenceCount,
|
|
RepoName: group.RepoName,
|
|
})
|
|
}
|
|
}
|
|
|
|
data := PageData{
|
|
Title: "Dashboard",
|
|
Content: map[string]any{
|
|
"Artifacts": artifacts,
|
|
"TotalArtifacts": int(total),
|
|
"Repositories": repos,
|
|
"TotalCrashGroups": int(totalCG),
|
|
"OpenCrashGroups": int(openCG),
|
|
"TopCrashers": topCrashers,
|
|
},
|
|
}
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
_ = h.Templates.Render(c.Writer, "index", data)
|
|
}
|
|
|
|
func (h *PageHandler) Artifacts(c *gin.Context) {
|
|
limit, _ := strconv.Atoi(c.Query("limit"))
|
|
offset, _ := strconv.Atoi(c.Query("offset"))
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
|
|
repoID, err := parseOptionalUintID(c.Query("repository_id"), "repository_id")
|
|
if err != nil {
|
|
c.String(http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
artifacts, total, err := models.ListArtifacts(c.Request.Context(), h.DB, models.ListArtifactsParams{
|
|
RepositoryID: repoID,
|
|
Type: c.Query("type"),
|
|
Limit: limit,
|
|
Offset: offset,
|
|
})
|
|
if err != nil {
|
|
c.String(http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
data := PageData{
|
|
Title: "Artifacts",
|
|
Content: map[string]any{
|
|
"Artifacts": artifacts,
|
|
"Total": int(total),
|
|
"Limit": limit,
|
|
"Offset": offset,
|
|
},
|
|
}
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
_ = h.Templates.Render(c.Writer, "artifacts", data)
|
|
}
|
|
|
|
func (h *PageHandler) ArtifactDetail(c *gin.Context) {
|
|
id, err := parseUintID(c.Param("id"), "artifact id")
|
|
if err != nil {
|
|
c.String(http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
artifact, err := models.GetArtifact(c.Request.Context(), h.DB, id)
|
|
if err != nil {
|
|
c.String(http.StatusNotFound, "artifact not found")
|
|
return
|
|
}
|
|
|
|
data := PageData{
|
|
Title: fmt.Sprintf("Artifact %d", artifact.ID),
|
|
Content: artifact,
|
|
}
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
_ = h.Templates.Render(c.Writer, "artifact_detail", data)
|
|
}
|
|
|
|
func (h *PageHandler) Repos(c *gin.Context) {
|
|
repos, err := models.ListRepositories(c.Request.Context(), h.DB)
|
|
if err != nil {
|
|
c.String(http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
data := PageData{
|
|
Title: "Repositories",
|
|
Content: map[string]any{
|
|
"Repositories": repos,
|
|
},
|
|
}
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
_ = h.Templates.Render(c.Writer, "repos", data)
|
|
}
|
|
|
|
func (h *PageHandler) CrashGroups(c *gin.Context) {
|
|
limit, _ := strconv.Atoi(c.Query("limit"))
|
|
offset, _ := strconv.Atoi(c.Query("offset"))
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
|
|
repoID, err := parseOptionalUintID(c.Query("repository_id"), "repository_id")
|
|
if err != nil {
|
|
c.String(http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
groups, total, err := models.ListCrashGroups(c.Request.Context(), h.DB, repoID, c.Query("status"), limit, offset)
|
|
if err != nil {
|
|
c.String(http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
data := PageData{
|
|
Title: "Crash Groups",
|
|
Content: map[string]any{
|
|
"CrashGroups": groups,
|
|
"Total": int(total),
|
|
"Limit": limit,
|
|
"Offset": offset,
|
|
},
|
|
}
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
_ = h.Templates.Render(c.Writer, "crashgroups", data)
|
|
}
|
|
|
|
func (h *PageHandler) CrashGroupDetail(c *gin.Context) {
|
|
id, err := parseUintID(c.Param("id"), "crash group id")
|
|
if err != nil {
|
|
c.String(http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
group, err := models.GetCrashGroup(c.Request.Context(), h.DB, id)
|
|
if err != nil {
|
|
c.String(http.StatusNotFound, "crash group not found")
|
|
return
|
|
}
|
|
if group.ForgejoIssueID != nil && h.ForgejoURL != "" {
|
|
repo, repoErr := models.GetRepositoryByID(c.Request.Context(), h.DB, group.RepositoryID)
|
|
if repoErr == nil {
|
|
issueURL := fmt.Sprintf("%s/%s/%s/issues/%d", strings.TrimRight(h.ForgejoURL, "/"), repo.Owner, repo.Name, *group.ForgejoIssueID)
|
|
group.ForgejoIssueURL = &issueURL
|
|
}
|
|
}
|
|
|
|
// Get artifacts linked to this crash group's signature.
|
|
signatureID := group.CrashSignatureID
|
|
artifacts, _, _ := models.ListArtifacts(c.Request.Context(), h.DB, models.ListArtifactsParams{
|
|
SignatureID: &signatureID,
|
|
Limit: 50,
|
|
})
|
|
|
|
data := PageData{
|
|
Title: "Crash Group: " + group.Title,
|
|
Content: map[string]any{
|
|
"Group": group,
|
|
"Artifacts": artifacts,
|
|
},
|
|
}
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
_ = h.Templates.Render(c.Writer, "crashgroup_detail", data)
|
|
}
|
|
|
|
func (h *PageHandler) Search(c *gin.Context) {
|
|
q := c.Query("q")
|
|
|
|
var artifacts []cairnapi.Artifact
|
|
var total int64
|
|
if q != "" {
|
|
artifacts, total, _ = models.SearchArtifacts(c.Request.Context(), h.DB, q, 50, 0)
|
|
}
|
|
|
|
data := PageData{
|
|
Title: "Search",
|
|
Content: map[string]any{
|
|
"Query": q,
|
|
"Artifacts": artifacts,
|
|
"Total": int(total),
|
|
},
|
|
}
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
_ = h.Templates.Render(c.Writer, "search", data)
|
|
}
|
|
|
|
func (h *PageHandler) Regression(c *gin.Context) {
|
|
repo := c.Query("repo")
|
|
base := c.Query("base")
|
|
head := c.Query("head")
|
|
|
|
content := map[string]any{
|
|
"Repo": repo,
|
|
"Base": base,
|
|
"Head": head,
|
|
}
|
|
|
|
if repo != "" && base != "" && head != "" {
|
|
r, err := models.GetRepositoryByName(c.Request.Context(), h.DB, repo)
|
|
if err == nil {
|
|
result, err := regression.Compare(c.Request.Context(), h.DB, r.ID, base, head)
|
|
if err == nil {
|
|
result.RepoName = repo
|
|
content["Result"] = result
|
|
}
|
|
}
|
|
}
|
|
|
|
data := PageData{
|
|
Title: "Regression Check",
|
|
Content: content,
|
|
}
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
_ = h.Templates.Render(c.Writer, "regression", data)
|
|
}
|
|
|
|
func (h *PageHandler) Targets(c *gin.Context) {
|
|
limit, _ := strconv.Atoi(c.Query("limit"))
|
|
offset, _ := strconv.Atoi(c.Query("offset"))
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
|
|
repoID, err := parseOptionalUintID(c.Query("repository_id"), "repository_id")
|
|
if err != nil {
|
|
c.String(http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
targets, total, err := models.ListTargets(c.Request.Context(), h.DB, repoID, limit, offset)
|
|
if err != nil {
|
|
c.String(http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
data := PageData{
|
|
Title: "Targets",
|
|
Content: map[string]any{
|
|
"Targets": targets,
|
|
"Total": int(total),
|
|
},
|
|
}
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
_ = h.Templates.Render(c.Writer, "targets", data)
|
|
}
|
|
|
|
func (h *PageHandler) TargetDetail(c *gin.Context) {
|
|
id, err := parseUintID(c.Param("id"), "target id")
|
|
if err != nil {
|
|
c.String(http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
target, err := models.GetTarget(c.Request.Context(), h.DB, id)
|
|
if err != nil {
|
|
c.String(http.StatusNotFound, "target not found")
|
|
return
|
|
}
|
|
|
|
targetID := target.ID
|
|
runs, _, _ := models.ListRuns(c.Request.Context(), h.DB, &targetID, 50, 0)
|
|
corpus, corpusTotal, _ := models.ListCorpusEntries(c.Request.Context(), h.DB, targetID, 50, 0)
|
|
|
|
data := PageData{
|
|
Title: "Target: " + target.Name,
|
|
Content: map[string]any{
|
|
"Target": target,
|
|
"Runs": runs,
|
|
"Corpus": corpus,
|
|
"CorpusTotal": corpusTotal,
|
|
},
|
|
}
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
_ = h.Templates.Render(c.Writer, "target_detail", data)
|
|
}
|
|
|
|
func (h *PageHandler) RunDetail(c *gin.Context) {
|
|
id, err := parseUintID(c.Param("id"), "run id")
|
|
if err != nil {
|
|
c.String(http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
run, err := models.GetRun(c.Request.Context(), h.DB, id)
|
|
if err != nil {
|
|
c.String(http.StatusNotFound, "run not found")
|
|
return
|
|
}
|
|
|
|
runID := run.ID
|
|
artifacts, _, _ := models.ListArtifacts(c.Request.Context(), h.DB, models.ListArtifactsParams{
|
|
RunID: &runID,
|
|
Limit: 50,
|
|
})
|
|
|
|
data := PageData{
|
|
Title: fmt.Sprintf("Run #%d", run.ID),
|
|
Content: map[string]any{
|
|
"Run": run,
|
|
"Artifacts": artifacts,
|
|
},
|
|
}
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
_ = h.Templates.Render(c.Writer, "run_detail", data)
|
|
}
|
|
|
|
func parseUintID(raw string, field string) (uint, error) {
|
|
id, err := strconv.ParseUint(raw, 10, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid %s", field)
|
|
}
|
|
return uint(id), nil
|
|
}
|
|
|
|
func parseOptionalUintID(raw string, field string) (*uint, error) {
|
|
if raw == "" {
|
|
return nil, nil
|
|
}
|
|
id, err := parseUintID(raw, field)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &id, nil
|
|
}
|