From adfeca4a436c50098cad160aa91945880cffc1e2 Mon Sep 17 00:00:00 2001 From: Nataly Musilah <115026536+Musilah@users.noreply.github.com> Date: Wed, 6 Mar 2024 18:24:38 +0300 Subject: [PATCH] NOISSUE - Add Fetch chart data functionality (#199) * set up linechart Signed-off-by: Musilah * update chart axes order Signed-off-by: Musilah * fix lint error Signed-off-by: Musilah * update go mod Signed-off-by: Musilah * add fetch data from reader functionality Signed-off-by: ianmuchyri * update content type to json Signed-off-by: ianmuchyri * wrap in a more suitable error Signed-off-by: ianmuchyri * resolve comments Signed-off-by: Musilah * extract page from offset and limit Signed-off-by: ianmuchyri * error renaming for sdk error Signed-off-by: ianmuchyri * fix middleware method naming Signed-off-by: ianmuchyri --------- Signed-off-by: Musilah Signed-off-by: ianmuchyri Co-authored-by: ianmuchyri --- cmd/ui/main.go | 2 +- docker/.env | 2 +- go.mod | 4 +- go.sum | 2 + ui/README.md | 4 +- ui/api/endpoint.go | 21 ++++++- ui/api/errors.go | 7 ++- ui/api/logging.go | 26 ++++++-- ui/api/metrics.go | 14 ++++- ui/api/requests.go | 140 +++++++++++++++++++++++++------------------- ui/api/transport.go | 132 +++++++++++++++++++++++++++++++++++++++-- ui/service.go | 38 ++++++++---- 12 files changed, 301 insertions(+), 91 deletions(-) diff --git a/cmd/ui/main.go b/cmd/ui/main.go index 239dff6b3..551203daa 100644 --- a/cmd/ui/main.go +++ b/cmd/ui/main.go @@ -35,7 +35,7 @@ type config struct { Port string `env:"MG_UI_PORT" envDefault:"9095"` InstanceID string `env:"MG_UI_INSTANCE_ID" envDefault:""` HTTPAdapterURL string `env:"MG_HTTP_ADAPTER_URL" envDefault:"http://localhost:8008"` - ReaderURL string `env:"MG_READER_URL" envDefault:"http://localhost:9007"` + ReaderURL string `env:"MG_READER_URL" envDefault:"http://localhost:9011"` ThingsURL string `env:"MG_THINGS_URL" envDefault:"http://localhost:9000"` UsersURL string `env:"MG_USERS_URL" envDefault:"http://localhost:9002"` HostURL string `env:"MG_UI_HOST_URL" envDefault:"http://localhost:9095"` diff --git a/docker/.env b/docker/.env index 9b92faa2e..d05ef9b47 100644 --- a/docker/.env +++ b/docker/.env @@ -7,7 +7,7 @@ MG_UI_LOG_LEVEL=debug MG_UI_PORT=9095 MG_HTTP_ADAPTER_URL=http://http-adapter:8008 -MG_READER_URL=http://mongodb-reader:9007 +MG_READER_URL=http://timescale-reader:9011 MG_THINGS_URL=http://things:9000 MG_USERS_URL=http://users:9002 MG_INVITATIONS_URL=http://invitations:9020 diff --git a/go.mod b/go.mod index a06b72cc6..532bb7690 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.21.5 require ( github.com/absmach/agent v0.0.0-20240202075640-cc619e6685c8 - github.com/absmach/magistrala v0.14.0 + github.com/absmach/magistrala v0.14.1-0.20240305111255-42d433a92f39 github.com/absmach/senml v1.0.5 github.com/caarlos0/env/v10 v10.0.0 github.com/eclipse/paho.mqtt.golang v1.4.3 @@ -13,9 +13,9 @@ require ( github.com/go-kit/kit v0.13.0 github.com/go-zoo/bone v1.3.0 github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/gorilla/securecookie v1.1.2 github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect - github.com/gorilla/securecookie v1.1.2 github.com/jackc/pgx/v5 v5.5.3 github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jmoiron/sqlx v1.3.5 diff --git a/go.sum b/go.sum index 95a6a59d9..3ae7c2317 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/absmach/agent v0.0.0-20240202075640-cc619e6685c8 h1:UpXJGOwJ1JAt93/C+ github.com/absmach/agent v0.0.0-20240202075640-cc619e6685c8/go.mod h1:yv3n0/JnLgZFShQlV+d4D14wA7IhGgQwBL5DG+hmTR0= github.com/absmach/magistrala v0.14.0 h1:ouIYOFwl0RMumBuXr4lGtfcukLGxFpzGAD4XDgrMcGg= github.com/absmach/magistrala v0.14.0/go.mod h1:7hyZSXwYbXHRBQTBUZ0lgsPw3HlPw+PBQRe+xsr542M= +github.com/absmach/magistrala v0.14.1-0.20240305111255-42d433a92f39 h1:qzE/GO9n1nfaFZ3XoubIDmddr68LQK5QTmjZGPlCKnI= +github.com/absmach/magistrala v0.14.1-0.20240305111255-42d433a92f39/go.mod h1:IiDX6bEARa1CM6rVRwck38DjCAeS2FkBzY37nlKOs6Y= github.com/absmach/mproxy v0.4.2 h1:u0ORPxSrUknqbVrC+E1MdsCv/7Q5eWNG7clIwOMV5hk= github.com/absmach/mproxy v0.4.2/go.mod h1:TeXhbHdjihXLVoohSzxvIEFzWu16WDOa91LNduks/N8= github.com/absmach/senml v1.0.5 h1:zNPRYpGr2Wsb8brAusz8DIfFqemy1a2dNbmMnegY3GE= diff --git a/ui/README.md b/ui/README.md index ec2645121..a373b5a02 100644 --- a/ui/README.md +++ b/ui/README.md @@ -11,7 +11,7 @@ The service is configured using the environment variables presented in the follo | MG_UI_LOG_LEVEL | Log level for UI (debug, info, warn, error) | debug | | MG_UI_PORT | Port where UI service is run | 9095 | | MG_HTTP_ADAPTER_URL | HTTP adapter URL | | -| MG_READER_URL | Reader URL | | +| MG_READER_URL | Reader URL | | | MG_THINGS_URL | Things URL | | | MG_USERS_URL | Users URL | | | MG_INVITATIONS_URL | Invitations URL | | @@ -60,7 +60,7 @@ make install MG_UI_LOG_LEVEL=debug \ MG_UI_PORT=9095 \ MG_HTTP_ADAPTER_URL="http://localhost:8008" \ -MG_READER_URL="http://localhost:9007" \ +MG_READER_URL="http://localhost:9011" \ MG_THINGS_URL="http://localhost:9000" \ MG_USERS_URL="http://localhost:9002" \ MG_INVITATIONS_URL="http://localhost:9020" \ diff --git a/ui/api/endpoint.go b/ui/api/endpoint.go index 747a564a6..daab9975a 100644 --- a/ui/api/endpoint.go +++ b/ui/api/endpoint.go @@ -1283,7 +1283,7 @@ func readMessagesEndpoint(svc ui.Service) endpoint.Endpoint { return nil, err } - res, err := svc.ReadMessages(req.Session, req.channelID, req.thingKey, req.page, req.limit) + res, err := svc.ReadMessages(req.Session, req.channelID, req.thingKey, req.mpgm) if err != nil { return nil, err } @@ -1295,6 +1295,25 @@ func readMessagesEndpoint(svc ui.Service) endpoint.Endpoint { } } +func FetchChartDataEndpoint(svc ui.Service) endpoint.Endpoint { + return func(_ context.Context, request interface{}) (response interface{}, err error) { + req := request.(readMessagesReq) + if err := req.validate(); err != nil { + return nil, err + } + res, err := svc.FetchChartData(req.Session.AccessToken, req.channelID, req.mpgm) + if err != nil { + return nil, err + } + + return uiRes{ + code: http.StatusOK, + html: res, + headers: map[string]string{"Content-Type": jsonContentType}, + }, nil + } +} + func createBootstrap(svc ui.Service, prefix string) endpoint.Endpoint { return func(_ context.Context, request interface{}) (response interface{}, err error) { req := request.(createBootstrapReq) diff --git a/ui/api/errors.go b/ui/api/errors.go index 78b02574e..601fa860d 100644 --- a/ui/api/errors.go +++ b/ui/api/errors.go @@ -6,8 +6,9 @@ package api import "github.com/absmach/magistrala/pkg/errors" var ( - errAuthorization = errors.New("missing or invalid credentials provided") + errInvalidCredentials = errors.New("missing or invalid credentials provided") errAuthentication = errors.New("failed to perform authentication over the entity") + errAuthorization = errors.New("failed to perform authorization over the entity") errMissingSecret = errors.New("missing secret") errMissingIdentity = errors.New("missing entity identity") errLimitSize = errors.New("invalid limit size") @@ -44,4 +45,8 @@ var ( errMissingRole = errors.New("missing role") errMissingValue = errors.New("missing value") errCookieDecryption = errors.New("failed to decrypt the cookie") + errMissingFrom = errors.New("missing from time value") + errMissingTo = errors.New("missing to time value") + errInvalidAggregation = errors.New("invalid aggregation value") + errInvalidInterval = errors.New("invalid interval value") ) diff --git a/ui/api/logging.go b/ui/api/logging.go index 4f87dc5e5..4a1991609 100644 --- a/ui/api/logging.go +++ b/ui/api/logging.go @@ -1161,13 +1161,12 @@ func (lm *loggingMiddleware) Publish(channelID, thingKey string, message ui.Mess } // ReadMessages adds logging middleware to read messages method. -func (lm *loggingMiddleware) ReadMessages(s ui.Session, channelID, thingKey string, page, limit uint64) (b []byte, err error) { +func (lm *loggingMiddleware) ReadMessages(s ui.Session, channelID, thingKey string, mpgm sdk.MessagePageMetadata) (b []byte, err error) { defer func(begin time.Time) { args := []any{ slog.String("duration", time.Since(begin).String()), slog.String("channel_id", channelID), - slog.Uint64("page", page), - slog.Uint64("limit", limit), + slog.Any("page_metadata", mpgm), } if err != nil { args = append(args, slog.Any("error", err)) @@ -1177,7 +1176,26 @@ func (lm *loggingMiddleware) ReadMessages(s ui.Session, channelID, thingKey stri lm.logger.Info("Read messages completed successfully", args...) }(time.Now()) - return lm.svc.ReadMessages(s, channelID, thingKey, page, limit) + return lm.svc.ReadMessages(s, channelID, thingKey, mpgm) +} + +// FetchChartData adds logging middleware to fetch chart data method. +func (lm *loggingMiddleware) FetchChartData(token string, channelID string, mpgm sdk.MessagePageMetadata) (b []byte, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.String("channel_id", channelID), + slog.Any("page_metadata", mpgm), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Fetch chart data failed to complete successfully", args...) + return + } + lm.logger.Info("Fetch chart data completed successfully", args...) + }(time.Now()) + + return lm.svc.FetchChartData(token, channelID, mpgm) } // CreateBootstrap adds logging middleware to create bootstrap method. diff --git a/ui/api/metrics.go b/ui/api/metrics.go index 3741ff5fb..717264fb2 100644 --- a/ui/api/metrics.go +++ b/ui/api/metrics.go @@ -670,13 +670,23 @@ func (mm *metricsMiddleware) Publish(channelID, thingKey string, message ui.Mess } // ReadMessages adds metrics middleware to read messages method. -func (mm *metricsMiddleware) ReadMessages(s ui.Session, channelID, thingKey string, page, limit uint64) ([]byte, error) { +func (mm *metricsMiddleware) ReadMessages(s ui.Session, channelID, thingKey string, mpgm sdk.MessagePageMetadata) ([]byte, error) { defer func(begin time.Time) { mm.counter.With("method", "read_messages").Add(1) mm.latency.With("method", "read_messages").Observe(time.Since(begin).Seconds()) }(time.Now()) - return mm.svc.ReadMessages(s, channelID, thingKey, page, limit) + return mm.svc.ReadMessages(s, channelID, thingKey, mpgm) +} + +// FetchChartData adds metrics middleware to fetch chart data method. +func (mm *metricsMiddleware) FetchChartData(token string, channelID string, mpgm sdk.MessagePageMetadata) ([]byte, error) { + defer func(begin time.Time) { + mm.counter.With("method", "fetch_chart_data").Add(1) + mm.latency.With("method", "fetch_chart_data").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.FetchChartData(token, channelID, mpgm) } // CreateBootstrap adds metrics middleware to create bootstrap method. diff --git a/ui/api/requests.go b/ui/api/requests.go index c8adf8c12..30db54269 100644 --- a/ui/api/requests.go +++ b/ui/api/requests.go @@ -4,11 +4,20 @@ package api import ( + "slices" + "strings" + "time" + "github.com/absmach/magistrala-ui/ui" sdk "github.com/absmach/magistrala/pkg/sdk/go" ) -const maxNameSize = 1024 +const ( + maxNameSize = 1024 + maxLimitSize = 1000 +) + +var validAggregations = []string{"MAX", "MIN", "AVG", "SUM", "COUNT"} type indexReq struct { ui.Session @@ -16,7 +25,7 @@ type indexReq struct { func (req indexReq) validate() error { if req.AccessToken == "" { - return errAuthorization + return errInvalidCredentials } return nil } @@ -58,7 +67,7 @@ type secureTokenReq struct { func (req secureTokenReq) validate() error { if req.AccessToken == "" { - return errAuthorization + return errInvalidCredentials } if req.RefreshToken == "" { return errMissingRefreshToken @@ -88,7 +97,7 @@ type createUserReq struct { func (req createUserReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.User.Credentials.Secret == "" { return errMissingSecret @@ -106,7 +115,7 @@ type createUsersReq struct { func (req createUsersReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } for _, user := range req.users { if user.Credentials.Secret == "" { @@ -129,7 +138,7 @@ type listEntityReq struct { func (req listEntityReq) validate() error { if req.AccessToken == "" { - return errAuthorization + return errInvalidCredentials } if req.page == 0 { return errPageSize @@ -150,7 +159,7 @@ type listEntityByIDReq struct { func (req listEntityByIDReq) validate() error { if req.AccessToken == "" { - return errAuthorization + return errInvalidCredentials } if req.id == "" { return errMissingUserID @@ -171,7 +180,7 @@ type viewResourceReq struct { func (req viewResourceReq) validate() error { if req.AccessToken == "" { - return errAuthorization + return errInvalidCredentials } if req.id == "" { return errMissingUserID @@ -187,7 +196,7 @@ type updateUserReq struct { func (req updateUserReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ID == "" { return errMissingUserID @@ -205,7 +214,7 @@ type updateUserTagsReq struct { func (req updateUserTagsReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ID == "" { return errMissingUserID @@ -220,7 +229,7 @@ type updateUserIdentityReq struct { func (req updateUserIdentityReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ID == "" { return errMissingUserID @@ -239,7 +248,7 @@ type updateUserStatusReq struct { func (req updateUserStatusReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.id == "" { return errMissingUserID @@ -255,7 +264,7 @@ type updateUserRoleReq struct { func (req updateUserRoleReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ID == "" { return errMissingUserID @@ -279,7 +288,7 @@ type updateUserPasswordReq struct { func (req updateUserPasswordReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.oldPass == "" { return errMissingSecret @@ -309,7 +318,7 @@ type passwordResetReq struct { func (req passwordResetReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.password == "" { return errMissingPassword @@ -330,7 +339,7 @@ type createThingReq struct { func (req createThingReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.Thing.Name == "" { return errMissingName @@ -345,7 +354,7 @@ type updateThingReq struct { func (req updateThingReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ID == "" { return errMissingThingID @@ -366,7 +375,7 @@ type updateThingTagsReq struct { func (req updateThingTagsReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ID == "" { return errMissingThingID @@ -381,7 +390,7 @@ type updateThingSecretReq struct { func (req updateThingSecretReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ID == "" { return errMissingThingID @@ -399,7 +408,7 @@ type updateThingStatusReq struct { func (req updateThingStatusReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.id == "" { return errMissingThingID @@ -415,7 +424,7 @@ type createThingsReq struct { func (req createThingsReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } return nil } @@ -427,7 +436,7 @@ type createChannelReq struct { func (req createChannelReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.Channel.Name == "" { return errMissingName @@ -442,7 +451,7 @@ type createChannelsReq struct { func (req createChannelsReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } for _, channel := range req.Channels { if channel.Name == "" { @@ -459,7 +468,7 @@ type updateChannelReq struct { func (req updateChannelReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ID == "" { return errMissingChannelID @@ -482,7 +491,7 @@ type connectThingReq struct { func (req connectThingReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.channelID == "" { return errMissingChannelID @@ -504,7 +513,7 @@ type shareThingReq struct { func (req shareThingReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if len(req.UserIDs) == 0 { return errMissingUserID @@ -525,7 +534,7 @@ type updateChannelStatusReq struct { func (req updateChannelStatusReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.id == "" { return errMissingChannelID @@ -540,7 +549,7 @@ type createGroupReq struct { func (req createGroupReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.Group.Name == "" { return errMissingName @@ -556,7 +565,7 @@ type createGroupsReq struct { func (req createGroupsReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } for _, group := range req.Groups { if group.Name == "" { @@ -574,7 +583,7 @@ type updateGroupReq struct { func (req updateGroupReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ID == "" { return errMissingGroupID @@ -596,7 +605,7 @@ type assignReq struct { func (req assignReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.groupID == "" { @@ -618,7 +627,7 @@ type updateGroupStatusReq struct { func (req updateGroupStatusReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.id == "" { return errMissingGroupID @@ -646,27 +655,38 @@ type readMessagesReq struct { ui.Session channelID string thingKey string - page uint64 - limit uint64 + mpgm sdk.MessagePageMetadata } func (req readMessagesReq) validate() error { - if req.AccessToken == "" { - return errAuthorization + if req.AccessToken == "" && req.thingKey == "" { + return errInvalidCredentials } if req.channelID == "" { return errMissingChannelID } - if req.thingKey == "" { - return errMissingThingKey - } - if req.page == 0 { - return errPageSize - } - if req.limit == 0 { + if req.mpgm.Limit < 1 || req.mpgm.Limit > maxLimitSize { return errLimitSize } + if req.mpgm.Aggregation != "" { + if req.mpgm.From == 0 { + return errMissingFrom + } + + if req.mpgm.To == 0 { + return errMissingTo + } + + if !slices.Contains(validAggregations, strings.ToUpper(req.mpgm.Aggregation)) { + return errInvalidAggregation + } + + if _, err := time.ParseDuration(req.mpgm.Interval); err != nil { + return errInvalidInterval + } + } + return nil } @@ -678,7 +698,7 @@ type bootstrapCommandReq struct { func (req bootstrapCommandReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.id == "" { return errMissingConfigID @@ -693,7 +713,7 @@ type updateBootstrapReq struct { func (req updateBootstrapReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ThingID == "" { @@ -709,7 +729,7 @@ type deleteBootstrapReq struct { func (req deleteBootstrapReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.id == "" { return errMissingConfigID @@ -724,7 +744,7 @@ type updateBootstrapStateReq struct { func (req updateBootstrapStateReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ThingID == "" { return errMissingConfigID @@ -739,7 +759,7 @@ type updateBootstrapCertReq struct { func (req updateBootstrapCertReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ThingID == "" { return errMissingThingID @@ -754,7 +774,7 @@ type updateBootstrapConnReq struct { func (req updateBootstrapConnReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ThingID == "" { return errMissingConfigID @@ -769,7 +789,7 @@ type createBootstrapReq struct { func (req createBootstrapReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ExternalID == "" { return errMissingExternalID @@ -792,7 +812,7 @@ type getEntitiesReq struct { func (req getEntitiesReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.page == 0 { return errPageSize @@ -826,7 +846,7 @@ type addUserToChannelReq struct { func (req addUserToChannelReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.ChannelID == "" { return errMissingChannelID @@ -849,7 +869,7 @@ type addUserGroupToChannelReq struct { func (req addUserGroupToChannelReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.channelID == "" { return errMissingChannelID @@ -886,7 +906,7 @@ type listDomainsReq struct { } func (req listDomainsReq) validate() error { - if req.AccessToken == "" { + if req.Token.AccessToken == "" { return errAuthentication } if req.page == 0 { @@ -953,7 +973,7 @@ type updateDomainStatusReq struct { func (req updateDomainStatusReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.id == "" { return errMissingDomainID @@ -1086,7 +1106,7 @@ type viewDashboardReq struct { func (req viewDashboardReq) validate() error { if req.AccessToken == "" { - return errAuthorization + return errInvalidCredentials } return nil } @@ -1100,7 +1120,7 @@ type createDashboardReq struct { func (req createDashboardReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } return nil } @@ -1113,7 +1133,7 @@ type listDashboardsReq struct { func (req listDashboardsReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } if req.page == 0 { return errPageSize @@ -1140,7 +1160,7 @@ type updateDashboardReq struct { func (req updateDashboardReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } return nil } @@ -1152,7 +1172,7 @@ type deleteDashboardReq struct { func (req deleteDashboardReq) validate() error { if req.token == "" { - return errAuthorization + return errInvalidCredentials } return nil } diff --git a/ui/api/transport.go b/ui/api/transport.go index 81c0fb4fc..2b802a791 100644 --- a/ui/api/transport.go +++ b/ui/api/transport.go @@ -45,6 +45,20 @@ const ( permissionKey = "permission" identityKey = "identity" statusKey = "status" + formatKey = "format" + subtopicKey = "subtopic" + publisherKey = "publisher" + protocolKey = "protocol" + valueKey = "v" + stringValueKey = "vs" + dataValueKey = "vd" + boolValueKey = "vb" + comparatorKey = "comparator" + fromKey = "from" + toKey = "to" + aggregationKey = "aggregation" + intervalKey = "interval" + defInterval = "1s" defPage = 1 defLimit = 10 defKey = "" @@ -191,6 +205,7 @@ func MakeHandler(svc ui.Service, r *chi.Mux, instanceID, prefix string, secureCo encodeResponse, opts..., ).ServeHTTP)) + r.Route("/dashboards", func(r chi.Router) { r.Get("/{id}", kithttp.NewServer( viewDashboardEndpoint(svc), @@ -652,6 +667,13 @@ func MakeHandler(svc ui.Service, r *chi.Mux, instanceID, prefix string, secureCo ).ServeHTTP) }) + r.Get("/data", kithttp.NewServer( + FetchChartDataEndpoint(svc), + decodeReadMessagesRequest, + encodeResponse, + opts..., + ).ServeHTTP) + r.Route("/bootstraps", func(r chi.Router) { r.Get("/", kithttp.NewServer( listBootstrap(svc), @@ -1835,17 +1857,98 @@ func decodeReadMessagesRequest(_ context.Context, r *http.Request) (interface{}, return nil, err } + subtopic, err := readStringQuery(r, subtopicKey, "") + if err != nil { + return nil, err + } + + publisher, err := readStringQuery(r, publisherKey, "") + if err != nil { + return nil, err + } + + protocol, err := readStringQuery(r, protocolKey, "") + if err != nil { + return nil, err + } + + name, err := readStringQuery(r, nameKey, "") + if err != nil { + return nil, err + } + + v, err := readNumQuery[float64](r, valueKey, 0) + if err != nil { + return nil, err + } + + vs, err := readStringQuery(r, stringValueKey, "") + if err != nil { + return nil, err + } + + vd, err := readStringQuery(r, dataValueKey, "") + if err != nil { + return nil, err + } + + vb, err := readBoolQuery(r, boolValueKey, false) + if err != nil { + return nil, err + } + + from, err := readNumQuery[float64](r, fromKey, 0) + if err != nil { + return nil, err + } + + to, err := readNumQuery[float64](r, toKey, 0) + if err != nil { + return nil, err + } + + aggregation, err := readStringQuery(r, aggregationKey, "") + if err != nil { + return nil, err + } + + var interval string + if aggregation != "" { + interval, err = readStringQuery(r, intervalKey, defInterval) + if err != nil { + return nil, err + } + } + session, err := sessionFromHeader(r) if err != nil { return nil, err } + offset := (page - 1) * limit + return readMessagesReq{ channelID: r.Form.Get("channel"), thingKey: r.Form.Get("thing"), - page: page, - limit: limit, Session: session, + mpgm: sdk.MessagePageMetadata{ + PageMetadata: sdk.PageMetadata{ + Limit: limit, + Offset: offset, + Name: name, + }, + Subtopic: subtopic, + Publisher: publisher, + Protocol: protocol, + Value: v, + StringValue: vs, + DataValue: vd, + BoolValue: &vb, + From: from, + To: to, + Aggregation: aggregation, + Interval: interval, + }, }, nil } @@ -2364,10 +2467,28 @@ func readNumQuery[N number](r *http.Request, key string, def N) (N, error) { } } +func readBoolQuery(r *http.Request, key string, def bool) (bool, error) { + vals := r.URL.Query()[key] + if len(vals) > 1 { + return false, errInvalidQueryParams + } + + if len(vals) == 0 { + return def, nil + } + + b, err := strconv.ParseBool(vals[0]) + if err != nil { + return false, errors.Wrap(errInvalidQueryParams, err) + } + + return b, nil +} + func tokenFromCookie(r *http.Request, cookie string) (string, error) { c, err := r.Cookie(cookie) if err != nil { - return "", errors.Wrap(err, errAuthorization) + return "", errors.Wrap(err, errInvalidCredentials) } return c.Value, nil @@ -2399,7 +2520,7 @@ func AdminAuthMiddleware(prefix string) func(http.Handler) http.Handler { } if session.User.Role != "admin" { - err = errors.ErrAuthorization + err = errAuthorization return } @@ -2573,7 +2694,7 @@ func encodeError(prefix string) kithttp.ErrorEncoder { _, displayError := errors.Unwrap(err) switch { - case errors.Contains(err, errAuthorization), + case errors.Contains(err, errInvalidCredentials), errors.Contains(err, errAuthentication), errors.Contains(err, ui.ErrTokenRefresh): w.Header().Set("Location", fmt.Sprintf("%s/login", prefix)) @@ -2611,6 +2732,7 @@ func encodeError(prefix string) kithttp.ErrorEncoder { errors.Contains(err, ui.ErrFailedDashboardSave), errors.Contains(err, ui.ErrFailedDashboardDelete), errors.Contains(err, ui.ErrFailedDashboardUpdate), + errors.Contains(err, ui.ErrJSONMarshal), errors.Contains(err, ui.ErrFailedDashboardRetrieve): w.Header().Set("Location", fmt.Sprintf("%s/%s?error=%s", prefix, errorAPIEndpoint, url.QueryEscape(displayError.Error()))) w.WriteHeader(http.StatusSeeOther) diff --git a/ui/service.go b/ui/service.go index d846e1cf8..1a343487a 100644 --- a/ui/service.go +++ b/ui/service.go @@ -219,6 +219,7 @@ var ( ErrFailedUnshare = errors.New("failed to unshare entity") ErrConflict = errors.New("entity already exists") ErrSessionType = errors.New("invalid session type") + ErrJSONMarshal = errors.New("failed to encode to json") ErrFailedViewDashboard = errors.New("failed to view dashboard") ErrFailedDashboardSave = errors.New("failed to save dashboard") @@ -371,7 +372,9 @@ type Service interface { // Publish facilitates a thing publishin messages to a channel. Publish(channelID, thingKey string, message Message) error // ReadMessages retrieves messages published in a channel. - ReadMessages(s Session, channelID, thingKey string, page, limit uint64) ([]byte, error) + ReadMessages(s Session, channelID, thingKey string, mpgm sdk.MessagePageMetadata) ([]byte, error) + // FetchChartData retrieves messages published in a channel to populate charts. + FetchChartData(token string, channelID string, mpgm sdk.MessagePageMetadata) ([]byte, error) // CreateBootstrap creates a new bootstrap config. CreateBootstrap(token string, config ...sdk.BootstrapConfig) error @@ -1784,23 +1787,20 @@ func (us *uiService) ListUserGroupChannels(s Session, id string, page, limit uin return btpl.Bytes(), nil } -func (us *uiService) ReadMessages(s Session, channelID, thingKey string, page, limit uint64) ([]byte, error) { - offset := (page - 1) * limit - pgm := sdk.PageMetadata{ - Offset: offset, - Limit: limit, - } - msg, err := us.sdk.ReadMessages(pgm, channelID, s.AccessToken) +func (us *uiService) ReadMessages(s Session, channelID, thingKey string, mpgm sdk.MessagePageMetadata) ([]byte, error) { + msg, err := us.sdk.ReadMessages(mpgm, channelID, s.AccessToken) if err != nil { return []byte{}, err } - noOfPages := int(math.Ceil(float64(msg.Total) / float64(limit))) + noOfPages := int(math.Ceil(float64(msg.Total) / float64(mpgm.Limit))) crumbs := []breadcrumb{ {Name: "Read Messages"}, } + currentPage := int(math.Ceil(float64(mpgm.Offset)/float64(mpgm.Limit)) + 1) + data := struct { NavbarActive string CollapseActive string @@ -1818,9 +1818,9 @@ func (us *uiService) ReadMessages(s Session, channelID, thingKey string, page, l channelID, thingKey, msg.Messages, - int(page), + currentPage, noOfPages, - int(limit), + int(mpgm.Limit), crumbs, s, } @@ -1833,6 +1833,20 @@ func (us *uiService) ReadMessages(s Session, channelID, thingKey string, page, l return btpl.Bytes(), nil } +func (us *uiService) FetchChartData(token string, channelID string, mpgm sdk.MessagePageMetadata) ([]byte, error) { + msg, sdkErr := us.sdk.ReadMessages(mpgm, channelID, token) + if sdkErr != nil { + return []byte{}, sdkErr + } + + data, err := json.Marshal(msg) + if err != nil { + return []byte{}, errors.Wrap(err, ErrJSONMarshal) + } + + return data, nil +} + func (us *uiService) Publish(channelID, thingKey string, message Message) error { jsonMessage, err := json.Marshal(message) if err != nil { @@ -2173,7 +2187,7 @@ func (us *uiService) GetEntities(token, entity, entityName, domainID, permission data, err := json.Marshal(items) if err != nil { - return []byte{}, err + return []byte{}, errors.Wrap(err, ErrJSONMarshal) } return data, nil }