NOISSUE - Update reports to use chromedp (#249)

* initial implementation

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* switch to gotenberg

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix top bar

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update env variable

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* address comments

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update changes

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update query

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update method

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* address commants

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

---------

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
This commit is contained in:
Steve Munene
2025-07-14 11:33:22 +03:00
committed by GitHub
parent ef6417a15d
commit b4eb6fd1aa
13 changed files with 78 additions and 66 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ type config struct {
TraceRatio float64 `env:"SMQ_JAEGER_TRACE_RATIO" envDefault:"1.0"`
BrokerURL string `env:"SMQ_MESSAGE_BROKER_URL" envDefault:"nats://localhost:4222"`
DefaultTemplatePath string `env:"MG_REPORTS_DEFAULT_TEMPLATE" envDefault:""`
ConverterURL string `env:"MG_REPORT_BROWSERLESS_URL" envDefault:"http://localhost:4000/pdf"`
ConverterURL string `env:"MG_PDF_CONVERTER_URL" envDefault:"http://localhost:4000/pdf"`
}
func main() {
@@ -61,7 +61,7 @@
.header-top-bar {
height: 8px;
background-color: var(--primary-color);
margin: 0 -10mm 15px -10mm;
margin: 5px 0 10px 0;
flex-shrink: 0;
}
+1 -1
View File
@@ -154,7 +154,7 @@ MG_REPORTS_DB_SSL_ROOT_CERT=
MG_REPORTS_INSTANCE_ID=
MG_REPORTS_EMAIL_TEMPLATE=reports.tmpl
MG_REPORTS_DEFAULT_TEMPLATE=
MG_REPORT_BROWSERLESS_URL=http://browserless:3000/pdf
MG_PDF_CONVERTER_URL=http://pdf-generator:3000/forms/chromium/convert/html
### Certs
SMQ_ADDONS_CERTS_PATH_PREFIX=./
+5 -5
View File
@@ -395,8 +395,8 @@ services:
MG_REPORTS_DB_SSL_CERT: ${MG_REPORTS_DB_SSL_CERT}
MG_REPORTS_DB_SSL_KEY: ${MG_REPORTS_DB_SSL_KEY}
MG_REPORTS_DB_SSL_ROOT_CERT: ${MG_REPORTS_DB_SSL_ROOT_CERT}
MG_REPORT_TEMPLATE_PATH: ${MG_REPORT_TEMPLATE_PATH}
MG_REPORT_BROWSERLESS_URL: ${MG_REPORT_BROWSERLESS_URL}
MG_REPORTS_DEFAULT_TEMPLATE: ${MG_REPORTS_DEFAULT_TEMPLATE}
MG_PDF_CONVERTER_URL: ${MG_PDF_CONVERTER_URL}
SMQ_MESSAGE_BROKER_URL: ${SMQ_MESSAGE_BROKER_URL}
SMQ_JAEGER_URL: ${SMQ_JAEGER_URL}
SMQ_JAEGER_TRACE_RATIO: ${SMQ_JAEGER_TRACE_RATIO}
@@ -450,9 +450,9 @@ services:
bind:
create_host_path: true
browserless:
image: browserless/chrome
container_name: magistrala-browserless
pdf-generator:
image: gotenberg/gotenberg:${MG_RELEASE_TAG}
container_name: magistrala-pdf
ports:
- "4000:3000"
networks:
+1 -1
View File
@@ -167,7 +167,7 @@ func (res emailReportResp) Empty() bool {
}
type viewReportTemplateRes struct {
Template string `json:"html_template"`
Template reports.ReportTemplate `json:"html_template"`
}
func (res viewReportTemplateRes) Code() int {
+38 -19
View File
@@ -7,10 +7,10 @@ import (
"bytes"
"context"
"encoding/csv"
"encoding/json"
"fmt"
"html/template"
"io"
"mime/multipart"
"net/http"
"sort"
"time"
@@ -77,29 +77,48 @@ func (r *report) generate(ctx context.Context, templateContent string, data Repo
}
func (r *report) htmlToPDF(ctx context.Context, htmlContent string) ([]byte, error) {
payload := map[string]interface{}{
"html": htmlContent,
"options": map[string]interface{}{
"printBackground": true,
"margin": map[string]string{
"top": "0",
"bottom": "0",
"left": "0",
"right": "0",
},
"preferCSSPageSize": true,
},
var requestBody bytes.Buffer
writer := multipart.NewWriter(&requestBody)
htmlPart, err := writer.CreateFormFile("files", "index.html")
if err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
jsonPayload, err := json.Marshal(payload)
if _, err := htmlPart.Write([]byte(htmlContent)); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("marginTop", "0"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("marginBottom", "0"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("marginLeft", "0."); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("marginRight", "0"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("printBackground", "true"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.WriteField("waitForSelector", "body"); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Close(); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.converterURL, &requestBody)
if err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, r.converterURL, bytes.NewReader(jsonPayload))
if err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
+1 -1
View File
@@ -198,7 +198,7 @@ func (am *authorizationMiddleware) UpdateReportTemplate(ctx context.Context, ses
return am.svc.UpdateReportTemplate(ctx, session, cfg)
}
func (am *authorizationMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (string, error) {
func (am *authorizationMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) {
if err := am.authorize(ctx, smqauthz.PolicyReq{
Domain: session.DomainID,
SubjectType: policies.UserType,
+1 -1
View File
@@ -227,7 +227,7 @@ func (lm *loggingMiddleware) UpdateReportTemplate(ctx context.Context, session a
return lm.svc.UpdateReportTemplate(ctx, session, cfg)
}
func (lm *loggingMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (t string, err error) {
func (lm *loggingMiddleware) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (t reports.ReportTemplate, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
+8 -8
View File
@@ -570,22 +570,22 @@ func (_c *Repository_ViewReportConfig_Call) RunAndReturn(run func(ctx context.Co
}
// ViewReportTemplate provides a mock function for the type Repository
func (_mock *Repository) ViewReportTemplate(ctx context.Context, domainID string, reportID string) (string, error) {
func (_mock *Repository) ViewReportTemplate(ctx context.Context, domainID string, reportID string) (reports.ReportTemplate, error) {
ret := _mock.Called(ctx, domainID, reportID)
if len(ret) == 0 {
panic("no return value specified for ViewReportTemplate")
}
var r0 string
var r0 reports.ReportTemplate
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (string, error)); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (reports.ReportTemplate, error)); ok {
return returnFunc(ctx, domainID, reportID)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) string); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) reports.ReportTemplate); ok {
r0 = returnFunc(ctx, domainID, reportID)
} else {
r0 = ret.Get(0).(string)
r0 = ret.Get(0).(reports.ReportTemplate)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
r1 = returnFunc(ctx, domainID, reportID)
@@ -615,12 +615,12 @@ func (_c *Repository_ViewReportTemplate_Call) Run(run func(ctx context.Context,
return _c
}
func (_c *Repository_ViewReportTemplate_Call) Return(s string, err error) *Repository_ViewReportTemplate_Call {
_c.Call.Return(s, err)
func (_c *Repository_ViewReportTemplate_Call) Return(reportTemplate reports.ReportTemplate, err error) *Repository_ViewReportTemplate_Call {
_c.Call.Return(reportTemplate, err)
return _c
}
func (_c *Repository_ViewReportTemplate_Call) RunAndReturn(run func(ctx context.Context, domainID string, reportID string) (string, error)) *Repository_ViewReportTemplate_Call {
func (_c *Repository_ViewReportTemplate_Call) RunAndReturn(run func(ctx context.Context, domainID string, reportID string) (reports.ReportTemplate, error)) *Repository_ViewReportTemplate_Call {
_c.Call.Return(run)
return _c
}
+8 -8
View File
@@ -678,22 +678,22 @@ func (_c *Service_ViewReportConfig_Call) RunAndReturn(run func(ctx context.Conte
}
// ViewReportTemplate provides a mock function for the type Service
func (_mock *Service) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (string, error) {
func (_mock *Service) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error) {
ret := _mock.Called(ctx, session, id)
if len(ret) == 0 {
panic("no return value specified for ViewReportTemplate")
}
var r0 string
var r0 reports.ReportTemplate
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (string, error)); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (reports.ReportTemplate, error)); ok {
return returnFunc(ctx, session, id)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) string); ok {
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) reports.ReportTemplate); ok {
r0 = returnFunc(ctx, session, id)
} else {
r0 = ret.Get(0).(string)
r0 = ret.Get(0).(reports.ReportTemplate)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok {
r1 = returnFunc(ctx, session, id)
@@ -723,12 +723,12 @@ func (_c *Service_ViewReportTemplate_Call) Run(run func(ctx context.Context, ses
return _c
}
func (_c *Service_ViewReportTemplate_Call) Return(s string, err error) *Service_ViewReportTemplate_Call {
_c.Call.Return(s, err)
func (_c *Service_ViewReportTemplate_Call) Return(reportTemplate reports.ReportTemplate, err error) *Service_ViewReportTemplate_Call {
_c.Call.Return(reportTemplate, err)
return _c
}
func (_c *Service_ViewReportTemplate_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (string, error)) *Service_ViewReportTemplate_Call {
func (_c *Service_ViewReportTemplate_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (reports.ReportTemplate, error)) *Service_ViewReportTemplate_Call {
_c.Call.Return(run)
return _c
}
+10 -17
View File
@@ -315,7 +315,7 @@ func (repo *PostgresRepository) UpdateReportDue(ctx context.Context, id string,
func (repo *PostgresRepository) UpdateReportTemplate(ctx context.Context, domainID, reportID string, template reports.ReportTemplate) error {
q := `
UPDATE report_configs
UPDATE report_config
SET report_template = :report_template, updated_at = :updated_at
WHERE id = :id AND domain_id = :domain_id`
@@ -335,13 +335,13 @@ func (repo *PostgresRepository) UpdateReportTemplate(ctx context.Context, domain
return nil
}
func (repo *PostgresRepository) ViewReportTemplate(ctx context.Context, domainID, reportID string) (string, error) {
func (repo *PostgresRepository) ViewReportTemplate(ctx context.Context, domainID, reportID string) (reports.ReportTemplate, error) {
q := `
SELECT COALESCE(report_template, '') as report_template
FROM report_configs
FROM report_config
WHERE id = $1 AND domain_id = $2`
var template string
var template reports.ReportTemplate
err := repo.DB.QueryRowxContext(ctx, q, reportID, domainID).Scan(&template)
if err != nil {
if err == sql.ErrNoRows {
@@ -355,28 +355,21 @@ func (repo *PostgresRepository) ViewReportTemplate(ctx context.Context, domainID
func (repo *PostgresRepository) DeleteReportTemplate(ctx context.Context, domainID, reportID string) error {
q := `
UPDATE report_configs
SET custom_template = NULL, updated_at = :updated_at
WHERE id = :id AND domain_id = :domain_id`
UPDATE report_config
SET report_template = '', updated_at = :updated_at
WHERE id = :id AND domain_id = :domain_id`
dbr := dbReport{
ID: reportID,
DomainID: domainID,
UpdatedAt: time.Now().UTC(),
}
result, err := repo.DB.ExecContext(ctx, q, dbr)
row, err := repo.DB.NamedQueryContext(ctx, q, dbr)
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
return errors.Wrap(repoerr.ErrRemoveEntity, err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return errors.Wrap(repoerr.ErrUpdateEntity, err)
}
if rowsAffected == 0 {
return repoerr.ErrNotFound
}
defer row.Close()
return nil
}
+2 -2
View File
@@ -397,7 +397,7 @@ type Repository interface {
UpdateReportDue(ctx context.Context, id string, due time.Time) (ReportConfig, error)
UpdateReportTemplate(ctx context.Context, domainID, reportID string, template ReportTemplate) error
ViewReportTemplate(ctx context.Context, domainID, reportID string) (string, error)
ViewReportTemplate(ctx context.Context, domainID, reportID string) (ReportTemplate, error)
DeleteReportTemplate(ctx context.Context, domainID, reportID string) error
}
@@ -412,7 +412,7 @@ type Service interface {
DisableReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error)
UpdateReportTemplate(ctx context.Context, session authn.Session, cfg ReportConfig) error
ViewReportTemplate(ctx context.Context, session authn.Session, id string) (string, error)
ViewReportTemplate(ctx context.Context, session authn.Session, id string) (ReportTemplate, error)
DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error
GenerateReport(ctx context.Context, session authn.Session, config ReportConfig, action ReportAction) (ReportPage, error)
+1 -1
View File
@@ -434,7 +434,7 @@ func (r *report) UpdateReportTemplate(ctx context.Context, session authn.Session
return nil
}
func (r *report) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (string, error) {
func (r *report) ViewReportTemplate(ctx context.Context, session authn.Session, id string) (ReportTemplate, error) {
template, err := r.repo.ViewReportTemplate(ctx, session.DomainID, id)
if err != nil {
return "", errors.Wrap(svcerr.ErrCreateEntity, err)