Skip to content
Open
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
21 changes: 21 additions & 0 deletions services/merrymaker-go/internal/http/list_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ func WrapListFetcher[T any](
}
}

// WrapFilteredFetcher adapts a filtered fetch function to FilteredFetcher and centralizes logging.
func WrapFilteredFetcher[T, F any](
fetchFunc func(ctx context.Context, filters F, limit, offset int) ([]T, error),
logger *slog.Logger,
msg string,
attrs func(filters F, pg pageOpts) []any,
) FilteredFetcher[T, F] {
return func(ctx context.Context, filters F, pg pageOpts) ([]T, error) {
limit, offset := pg.LimitAndOffset()
items, err := fetchFunc(ctx, filters, limit, offset)
if err != nil {
args := []any{"error", err, "page", pg.Page, "page_size", pg.PageSize}
if attrs != nil {
args = append(args, attrs(filters, pg)...)
}
logger.ErrorContext(ctx, msg, args...)
}
return items, err
}
}

// FilterParser is a function type for parsing URL query parameters into filter data.
// It takes url.Values and returns the parsed filter of type F, or an error if parsing fails.
// The error allows the handler to show meaningful validation errors for invalid filter params.
Expand Down
20 changes: 7 additions & 13 deletions services/merrymaker-go/internal/http/ui_allowlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,13 @@ func (h *UIHandlers) Allowlist(w http.ResponseWriter, r *http.Request) {
Handler: h,
W: w,
R: r,
Fetcher: func(ctx context.Context, pg pageOpts) ([]*model.DomainAllowlist, error) {
// Fetch pageSize+1 to detect hasNext
limit, offset := pg.LimitAndOffset()
listOpts := model.DomainAllowlistListOptions{Limit: limit, Offset: offset}
items, err := h.AllowlistSvc.List(ctx, listOpts)
if err != nil {
h.logLoadError(ctx, "failed to load allowlist for UI", err,
"page", pg.Page,
"page_size", pg.PageSize,
)
}
return items, err
},
Fetcher: WrapListFetcher(
func(ctx context.Context, limit, offset int) ([]*model.DomainAllowlist, error) {
return h.AllowlistSvc.List(ctx, model.DomainAllowlistListOptions{Limit: limit, Offset: offset})
},
h.logger(),
"failed to load allowlist for UI",
),
BasePath: "/allowlist",
PageMeta: PageMeta{Title: "Merrymaker - Allow List", PageTitle: "Allow List", CurrentPage: PageAllowlist},
ItemsKey: "Allowlist",
Expand Down
54 changes: 23 additions & 31 deletions services/merrymaker-go/internal/http/ui_iocs.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,29 @@ func (h *UIHandlers) IOCs(w http.ResponseWriter, r *http.Request) {

// Use generic list handler with filtering
HandleList(ListHandlerOpts[*model.IOC, iocsFilter]{
Handler: h,
W: w,
R: r,
FilteredFetcher: h.fetchIOCsWithFilters,
FilterParser: parseIOCsFilter,
EnrichData: h.enrichIOCsData(),
BasePath: "/iocs",
Handler: h,
W: w,
R: r,
FilteredFetcher: WrapFilteredFetcher(
func(ctx context.Context, filters iocsFilter, limit, offset int) ([]*model.IOC, error) {
opts := model.IOCListOptions{
Limit: limit,
Offset: offset,
Type: filters.Type,
Enabled: filters.Enabled,
Search: filters.Search,
}
return h.IOCSvc.List(ctx, opts)
},
h.logger(),
"failed to load IOCs for UI",
func(filters iocsFilter, _ pageOpts) []any {
return []any{"filters", filters}
},
),
FilterParser: parseIOCsFilter,
EnrichData: h.enrichIOCsData(),
BasePath: "/iocs",
PageMeta: PageMeta{
Title: "Merrymaker - IOCs",
PageTitle: "IOCs",
Expand All @@ -59,30 +75,6 @@ func (h *UIHandlers) IOCs(w http.ResponseWriter, r *http.Request) {
})
}

// fetchIOCsWithFilters fetches IOCs with applied filters and pagination.
func (h *UIHandlers) fetchIOCsWithFilters(ctx context.Context, f iocsFilter, pg pageOpts) ([]*model.IOC, error) {
limit, offset := pg.LimitAndOffset()

opts := model.IOCListOptions{
Limit: limit,
Offset: offset,
Type: f.Type,
Enabled: f.Enabled,
Search: f.Search,
}

iocs, err := h.IOCSvc.List(ctx, opts)
if err != nil {
h.logger().ErrorContext(ctx, "failed to load IOCs for UI",
"error", err,
"page", pg.Page,
"page_size", pg.PageSize,
"filters", f,
)
}
return iocs, err
}

// parseIOCsFilter parses filter parameters from query string.
func parseIOCsFilter(q url.Values) (iocsFilter, error) {
f := iocsFilter{BaseFilter: ParseBaseFilter(q)}
Expand Down
53 changes: 21 additions & 32 deletions services/merrymaker-go/internal/http/ui_jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1244,31 +1244,6 @@ func buildJobListOptions(filters jobsFilter, limit, offset int) *model.JobListOp
return opts
}

// fetchJobsWithFilters fetches jobs with optional filtering.
func (h *UIHandlers) fetchJobsWithFilters(
ctx context.Context,
filters jobsFilter,
pg pageOpts,
) ([]*model.JobWithEventCount, error) {
limit, offset := pg.LimitAndOffset()

opts := buildJobListOptions(filters, limit, offset)

jobs, err := h.Jobs.List(ctx, opts)
if err != nil {
h.logger().ErrorContext(ctx, "failed to load jobs for UI",
"error", err,
"status", filters.Status,
"type", filters.Type,
"site_id", filters.SiteID,
"is_test", filters.IsTest,
"page", pg.Page,
"page_size", pg.PageSize,
)
}
return jobs, err
}

// enrichJobsData returns a data enricher that adds filter values and sites to template.
func (h *UIHandlers) enrichJobsData() DataEnricher[*model.JobWithEventCount, jobsFilter] {
return func(builder *TemplateDataBuilder, _ []*model.JobWithEventCount, filters jobsFilter) {
Expand Down Expand Up @@ -1299,13 +1274,27 @@ func (h *UIHandlers) enrichJobsData() DataEnricher[*model.JobWithEventCount, job
func (h *UIHandlers) JobsList(w http.ResponseWriter, r *http.Request) {
// Use generic list handler with filtering
HandleList(ListHandlerOpts[*model.JobWithEventCount, jobsFilter]{
Handler: h,
W: w,
R: r,
FilteredFetcher: h.fetchJobsWithFilters,
FilterParser: parseJobsFilter,
EnrichData: h.enrichJobsData(),
BasePath: "/jobs",
Handler: h,
W: w,
R: r,
FilteredFetcher: WrapFilteredFetcher(
func(ctx context.Context, filters jobsFilter, limit, offset int) ([]*model.JobWithEventCount, error) {
return h.Jobs.List(ctx, buildJobListOptions(filters, limit, offset))
},
h.logger(),
"failed to load jobs for UI",
func(filters jobsFilter, _ pageOpts) []any {
return []any{
"status", filters.Status,
"type", filters.Type,
"site_id", filters.SiteID,
"is_test", filters.IsTest,
}
},
),
FilterParser: parseJobsFilter,
EnrichData: h.enrichJobsData(),
BasePath: "/jobs",
PageMeta: PageMeta{
Title: "Merrymaker - Jobs",
PageTitle: "Jobs",
Expand Down
26 changes: 11 additions & 15 deletions services/merrymaker-go/internal/http/ui_sites.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,26 +127,22 @@ func (h *UIHandlers) Sites(w http.ResponseWriter, r *http.Request) {
Handler: h,
W: w,
R: r,
FilteredFetcher: func(ctx context.Context, filters sitesFilter, pg pageOpts) ([]siteRow, error) {
// Fetch pageSize+1 to detect hasNext
limit, offset := pg.LimitAndOffset()

rows, err := h.listSiteRows(ctx, filters, pageBounds{Limit: limit, Offset: offset})
if err != nil {
// Log the error for operational visibility
h.logLoadError(ctx, "failed to load sites for UI", err,
FilteredFetcher: WrapFilteredFetcher(
func(ctx context.Context, filters sitesFilter, limit, offset int) ([]siteRow, error) {
return h.listSiteRows(ctx, filters, pageBounds{Limit: limit, Offset: offset})
},
h.logger(),
"failed to load sites for UI",
func(filters sitesFilter, _ pageOpts) []any {
return []any{
"query", filters.Q,
"enabled", filters.Enabled,
"scope", filters.Scope,
"sort", filters.Sort,
"dir", filters.Dir,
"page", pg.Page,
"page_size", pg.PageSize,
)
return nil, err
}
return rows, nil
},
}
},
),
FilterParser: parseSitesFilter,
EnrichData: func(builder *TemplateDataBuilder, _ []siteRow, filters sitesFilter) {
// Add filter values to template
Expand Down
57 changes: 22 additions & 35 deletions services/merrymaker-go/internal/http/ui_sources.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,28 @@ func parseSourcesFilter(q url.Values) (sourcesFilters, error) {
func (h *UIHandlers) Sources(w http.ResponseWriter, r *http.Request) {
// Use generic list handler with filtering
HandleList(ListHandlerOpts[*model.Source, sourcesFilters]{
Handler: h,
W: w,
R: r,
FilteredFetcher: h.fetchSourcesWithFilters,
FilterParser: parseSourcesFilter,
EnrichData: h.enrichSourcesData(r),
BasePath: "/sources",
PageMeta: PageMeta{Title: "Merrymaker - Sources", PageTitle: "Sources", CurrentPage: PageSources},
ItemsKey: "Sources",
ErrorMessage: errMsgUnableLoadSources,
Handler: h,
W: w,
R: r,
FilteredFetcher: WrapFilteredFetcher(
func(ctx context.Context, filters sourcesFilters, limit, offset int) ([]*model.Source, error) {
if filters.Q != "" {
return h.listSourcesByQuery(ctx, filters.Q, limit, offset)
}
return h.SourceSvc.List(ctx, limit, offset)
},
h.logger(),
"failed to load sources for UI",
func(filters sourcesFilters, _ pageOpts) []any {
return []any{"query", filters.Q, "include_tests", filters.IncludeTests}
},
),
FilterParser: parseSourcesFilter,
EnrichData: h.enrichSourcesData(r),
BasePath: "/sources",
PageMeta: PageMeta{Title: "Merrymaker - Sources", PageTitle: "Sources", CurrentPage: PageSources},
ItemsKey: "Sources",
ErrorMessage: errMsgUnableLoadSources,
ServiceAvailable: func() bool {
return h.SourceSvc != nil
},
Expand All @@ -135,31 +147,6 @@ func (h *UIHandlers) Sources(w http.ResponseWriter, r *http.Request) {
})
}

// fetchSourcesWithFilters fetches sources with optional filtering.
func (h *UIHandlers) fetchSourcesWithFilters(
ctx context.Context,
filters sourcesFilters,
pg pageOpts,
) ([]*model.Source, error) {
limit, offset := pg.LimitAndOffset()

var sources []*model.Source
var err error
if filters.Q != "" {
sources, err = h.listSourcesByQuery(ctx, filters.Q, limit, offset)
} else {
sources, err = h.SourceSvc.List(ctx, limit, offset)
}

if err != nil {
h.logger().ErrorContext(ctx, "failed to load sources for UI",
"error", err, "query", filters.Q, "include_tests", filters.IncludeTests,
"page", pg.Page, "page_size", pg.PageSize,
)
}
return sources, err
}

// enrichSourcesData returns a data enricher that adds filter values and scan counts.
func (h *UIHandlers) enrichSourcesData(r *http.Request) DataEnricher[*model.Source, sourcesFilters] {
return func(builder *TemplateDataBuilder, items []*model.Source, filters sourcesFilters) {
Expand Down
Loading