Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/database/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func Models() []any {
datasources_domain.Datasource{},
dashboards_domain.Dashboard{},
dashboards_domain.Visualization{},
dashboards_domain.DashboardFilter{},
loganalyzer_domain.SavedQuery{},
adaudit_domain.ADUser{},
}
Expand Down
6 changes: 6 additions & 0 deletions backend/modules/audit/domain/event_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ const (
VISUALIZATION_UPDATE_SUCCESS ApplicationEventType = "VISUALIZATION_UPDATE_SUCCESS"
VISUALIZATION_DELETE_ATTEMPT ApplicationEventType = "VISUALIZATION_DELETE_ATTEMPT"
VISUALIZATION_DELETE_SUCCESS ApplicationEventType = "VISUALIZATION_DELETE_SUCCESS"
DASHBOARD_FILTER_CREATE_ATTEMPT ApplicationEventType = "DASHBOARD_FILTER_CREATE_ATTEMPT"
DASHBOARD_FILTER_CREATE_SUCCESS ApplicationEventType = "DASHBOARD_FILTER_CREATE_SUCCESS"
DASHBOARD_FILTER_UPDATE_ATTEMPT ApplicationEventType = "DASHBOARD_FILTER_UPDATE_ATTEMPT"
DASHBOARD_FILTER_UPDATE_SUCCESS ApplicationEventType = "DASHBOARD_FILTER_UPDATE_SUCCESS"
DASHBOARD_FILTER_DELETE_ATTEMPT ApplicationEventType = "DASHBOARD_FILTER_DELETE_ATTEMPT"
DASHBOARD_FILTER_DELETE_SUCCESS ApplicationEventType = "DASHBOARD_FILTER_DELETE_SUCCESS"
DASHBOARD_LAYOUT_CREATE_ATTEMPT ApplicationEventType = "DASHBOARD_LAYOUT_CREATE_ATTEMPT"
DASHBOARD_LAYOUT_CREATE_SUCCESS ApplicationEventType = "DASHBOARD_LAYOUT_CREATE_SUCCESS"
DASHBOARD_LAYOUT_UPDATE_ATTEMPT ApplicationEventType = "DASHBOARD_LAYOUT_UPDATE_ATTEMPT"
Expand Down
7 changes: 7 additions & 0 deletions backend/modules/dashboards/connectors/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,10 @@ type VisualizationRepository interface {
List(ctx context.Context, f dto.VisualizationFilter) ([]domain.Visualization, int64, error)
Delete(ctx context.Context, id uuid.UUID) error
}

type DashboardFilterRepository interface {
Save(ctx context.Context, v *domain.DashboardFilter) error
FindByID(ctx context.Context, id uuid.UUID) (*domain.DashboardFilter, error)
List(ctx context.Context, f dto.DashboardFilterFilter) ([]domain.DashboardFilter, int64, error)
Delete(ctx context.Context, id uuid.UUID) error
}
9 changes: 9 additions & 0 deletions backend/modules/dashboards/connectors/usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,12 @@ type VisualizationUsecase interface {
List(ctx context.Context, f dto.VisualizationFilter) ([]domain.Visualization, int64, error)
Delete(ctx context.Context, id uuid.UUID) error
}


type DashboardFilterUsecase interface {
Create(ctx context.Context, v *domain.DashboardFilter, user string) (*domain.DashboardFilter, error)
Update(ctx context.Context, v *domain.DashboardFilter, user string) (*domain.DashboardFilter, error)
List(ctx context.Context, f dto.DashboardFilterFilter) ([]domain.DashboardFilter, int64, error)
Delete(ctx context.Context, id uuid.UUID) error
}

31 changes: 31 additions & 0 deletions backend/modules/dashboards/domain/filters.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package domain

import (
"time"

"github.com/google/uuid"
)


type DashboardFilterType = string

const (
DashboardFilterMultiple DashboardFilterType = "multiple"
DashboardFilterSearchable DashboardFilterType = "searchable"
)

type DashboardFilter struct {
ID uuid.UUID `gorm:"column:id;type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
TenantID uuid.UUID `gorm:"column:tenant_id;type:uuid;not null;index" json:"-"`
DashboardID uuid.UUID `gorm:"column:dashboard_id;type:uuid;not null;index" json:"dashboardId"`
DataSet string `gorm:"column:data_set;not null" json:"data_set"`
Field string `gorm:"column:field;not null" json:"field"`
Type DashboardFilterType `gorm:"column:type;not null" json:"type"`
Label string `gorm:"column:label" json:"label"`
PlaceHolder string `gorm:"column:place_holder" json:"place_holder"`
CreatedDate time.Time `gorm:"column:created_date" json:"createdDate"`
ModifiedDate time.Time `gorm:"column:modified_date" json:"modifiedDate"`
}

func (DashboardFilter) TableName() string { return "dashboard_filter" }

34 changes: 34 additions & 0 deletions backend/modules/dashboards/dto/dashboard_filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

package dto

import (
"github.com/google/uuid"
"github.com/utmstack/utmstack/backend/pkg/database"
)

type DashboardFilterFilter struct {
DashboardID *uuid.UUID
database.Params
}


// the wire form of the filter. gin cannot bind a
// uuid.UUID from a query string (it is a [16]byte), so the id arrives as text
// and is parsed here.
type DashboardFilterQuery struct {
DashboardID string `form:"dashboardId"`
database.Params
}

func (q DashboardFilterQuery) Filter() (DashboardFilterFilter, error) {
f := DashboardFilterFilter{Params: q.Params}
if q.DashboardID == "" {
return f, nil
}
id, err := uuid.Parse(q.DashboardID)
if err != nil {
return f, err
}
f.DashboardID = &id
return f, nil
}
142 changes: 142 additions & 0 deletions backend/modules/dashboards/handler/dashboard_filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package handler

import (
"net/http"

"github.com/gin-gonic/gin"
"github.com/utmstack/utmstack/backend/modules/audit"
audit_connectors "github.com/utmstack/utmstack/backend/modules/audit/connectors"
audit_domain "github.com/utmstack/utmstack/backend/modules/audit/domain"
"github.com/utmstack/utmstack/backend/modules/dashboards/connectors"
"github.com/utmstack/utmstack/backend/modules/dashboards/domain"
"github.com/utmstack/utmstack/backend/modules/dashboards/dto"
)

type DashboardFilterHandler struct {
uc connectors.DashboardFilterUsecase
}

func NewDashboardFilterHandler(uc connectors.DashboardFilterUsecase) *DashboardFilterHandler {
return &DashboardFilterHandler{uc: uc}
}

// Create godoc
//
// @Summary Create a dashbaord filter
// @Description Creates a custom dashboard filter.
// @Tags DashboardFilters
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param input body domain.DashboardFilter true "DashboardFilter to create"
// @Success 201 {object} domain.DashboardFilter
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /filter [post]
func (h *DashboardFilterHandler) Create(c *gin.Context) {
var v domain.DashboardFilter
if err := c.ShouldBindJSON(&v); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
res, err := h.uc.Create(c.Request.Context(), &v, currentUser(c))
resID := ""
if res != nil {
resID = res.ID.String()
}
audit.Record(c, audit_connectors.Event{Action: "dashboard_filter.create", ResourceType: "dashboard_filter", ResourceID: resID},
audit_domain.DASHBOARD_FILTER_CREATE_ATTEMPT, audit_domain.DASHBOARD_FILTER_CREATE_SUCCESS, err)
if err != nil {
writeError(c, err)
return
}
c.JSON(http.StatusCreated, res)
}

// Update godoc
//
// @Summary Update a dashbaord filter
// @Description Updates an existing dashboard filter
// @Tags DashboardFilters
// @Security BearerAuth
// @Accept json
// @Produce json
// @Param input body domain.DashboardFilter true "DashboardFilter to update"
// @Success 200 {object} domain.DashboardFilter
// @Failure 400 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /filter [put]
func (h *DashboardFilterHandler) Update(c *gin.Context) {
var v domain.DashboardFilter
if err := c.ShouldBindJSON(&v); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
res, err := h.uc.Update(c.Request.Context(), &v, currentUser(c))
audit.Record(c, audit_connectors.Event{Action: "dashboard_filter.update", ResourceType: "dashboard_filter", ResourceID: v.ID.String()},
audit_domain.DASHBOARD_FILTER_UPDATE_ATTEMPT, audit_domain.DASHBOARD_FILTER_UPDATE_SUCCESS, err)
if err != nil {
writeError(c, err)
return
}
c.JSON(http.StatusOK, res)
}

// List godoc
//
// @Summary List dashbaord filters
// @Description Lists dashboard filter.
// @Tags DashboardFilters
// @Security BearerAuth
// @Produce json
// @Param dashboardId query int false "Filter by dashboard id"
// @Param page query int false "Page (0-based)"
// @Param size query int false "Page size"
// @Success 200 {array} domain.DashboardFilter
// @Header 200 {string} X-Total-Count "Total records"
// @Failure 500 {object} map[string]string
// @Router /filters [get]
func (h *DashboardFilterHandler) List(c *gin.Context) {
var q dto.DashboardFilterQuery
if err := c.ShouldBindQuery(&q); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
f, err := q.Filter()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "dashboardId must be a uuid"})
return
}
items, total, err := h.uc.List(c.Request.Context(), f)
if err != nil {
writeError(c, err)
return
}
writeList(c, items, total)
}


// Delete godoc
//
// @Summary Delete a dashbaord filter by id
// @Tags DashboardFilters
// @Security BearerAuth
// @Param id path int true "DashboardFilter id"
// @Success 200 "Deleted"
// @Failure 500 {object} map[string]string
// @Router /filter/{id} [delete]
func (h *DashboardFilterHandler) Delete(c *gin.Context) {
id, ok := pathID(c)
if !ok {
return
}
err := h.uc.Delete(c.Request.Context(), id)
audit.Record(c, audit_connectors.Event{Action: "visualization.delete", ResourceType: "visualization", ResourceID: id.String()},
audit_domain.VISUALIZATION_DELETE_ATTEMPT, audit_domain.VISUALIZATION_DELETE_SUCCESS, err)
if err != nil {
writeError(c, err)
return
}
c.Status(http.StatusOK)
}
13 changes: 13 additions & 0 deletions backend/modules/dashboards/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,33 @@ type Module struct {
queryHandler *handler.QueryHandler
dashboardHandler *handler.DashboardHandler
visualizationHandler *handler.VisualizationHandler
dashboardFilterHandler *handler.DashboardFilterHandler
dashboardUC connectors.DashboardUsecase
visualizationUC connectors.VisualizationUsecase
dashboardFilterUC connectors.DashboardFilterUsecase
dashboardBootstrap *repository.DashboardBootstrap
}

func NewModule(db *gorm.DB, events usecase.Reader) *Module {
dashRepo := repository.NewDashboardRepository(db)
vizRepo := repository.NewVisualizationRepository(db)
filterRepo := repository.NewDashboardFilterRepository(db)

dashUC := usecase.NewDashboardUsecase(dashRepo)
vizUC := usecase.NewVisualizationUsecase(vizRepo)
filtUC := usecase.NewDashboardFilterUsecase(filterRepo)

dashboardBootstrap := repository.NewDashboardBootstrap(
env.String(repository.DashboardsSrcDirEnv, repository.DefaultDashboardsSrcDir, false), db)

m := &Module{
dashboardHandler: handler.NewDashboardHandler(dashUC),
visualizationHandler: handler.NewVisualizationHandler(vizUC),
dashboardFilterHandler: handler.NewDashboardFilterHandler(filtUC) ,
dashboardUC: dashUC,
visualizationUC: vizUC,
dashboardBootstrap: dashboardBootstrap,
dashboardFilterUC: filtUC,
}
if events != nil {
m.queryHandler = handler.NewQueryHandler(usecase.NewQueryService(events))
Expand All @@ -54,10 +60,17 @@ func (m *Module) Start(ctx context.Context) {
}

func (m *Module) GetDashboardHandler() *handler.DashboardHandler { return m.dashboardHandler }

func (m *Module) GetVisualizationHandler() *handler.VisualizationHandler {
return m.visualizationHandler
}

func (m *Module) GetDashboardFilterHandler() *handler.DashboardFilterHandler {
return m.dashboardFilterHandler
}



func (m *Module) GetDashboardUsecase() connectors.DashboardUsecase { return m.dashboardUC }
func (m *Module) GetVisualizationUsecase() connectors.VisualizationUsecase {
return m.visualizationUC
Expand Down
55 changes: 55 additions & 0 deletions backend/modules/dashboards/repository/dashboard_filters.pg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package repository

import (
"context"
"errors"

"github.com/utmstack/utmstack/backend/modules/dashboards/connectors"
"github.com/utmstack/utmstack/backend/modules/dashboards/domain"
"github.com/utmstack/utmstack/backend/modules/dashboards/dto"
"gorm.io/gorm"

"github.com/google/uuid"
)

type pgDashboardFilterRepository struct{ db *gorm.DB }

func NewDashboardFilterRepository(db *gorm.DB) connectors.DashboardFilterRepository {
return &pgDashboardFilterRepository{db: db}
}

func (r *pgDashboardFilterRepository) Save(ctx context.Context, v *domain.DashboardFilter) error {
return r.db.WithContext(ctx).Save(v).Error
}

func (r *pgDashboardFilterRepository) FindByID(ctx context.Context, id uuid.UUID) (*domain.DashboardFilter, error) {
var v domain.DashboardFilter
err := r.db.WithContext(ctx).First(&v, id).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
return &v, nil
}

func (r *pgDashboardFilterRepository) List(ctx context.Context, f dto.DashboardFilterFilter) ([]domain.DashboardFilter, int64, error) {
q := r.db.WithContext(ctx).Model(&domain.DashboardFilter{})
if f.DashboardID != nil {
q = q.Where("dashboard_id = ?", *f.DashboardID)
}
var total int64
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
var items []domain.DashboardFilter
if err := q.Order("id ASC").Offset(f.Offset()).Limit(f.Limit()).Find(&items).Error; err != nil {
return nil, 0, err
}
return items, total, nil
}

func (r *pgDashboardFilterRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.WithContext(ctx).Delete(&domain.DashboardFilter{}, id).Error
}
7 changes: 7 additions & 0 deletions backend/modules/dashboards/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
func RegisterRoutes(api *gin.RouterGroup, m *Module, userAuth gin.HandlerFunc) {
dh := m.GetDashboardHandler()
vh := m.GetVisualizationHandler()
fh := m.GetDashboardFilterHandler()

read := middleware.RequirePermission("dashboards.read")
write := middleware.RequirePermission("dashboards.write")
Expand All @@ -19,6 +20,12 @@ func RegisterRoutes(api *gin.RouterGroup, m *Module, userAuth gin.HandlerFunc) {
d.GET("/:id", read, dh.GetByID)
d.DELETE("/:id", write, dh.Delete)

f := d.Group("/filters", userAuth)
f.POST("", write, fh.Create)
f.PUT("", write, fh.Update)
f.GET("", read, fh.List)
f.DELETE("/:id", write, fh.Delete)

v := api.Group("/visualizations", userAuth)
v.POST("", write, vh.Create)
v.PUT("", write, vh.Update)
Expand Down
Loading
Loading