cairn/internal/web/web.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
}