Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

admin: add pagination to message logs query #2644

Merged
merged 41 commits into from
Nov 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
706be25
move debugMessages query to admin gql file
Forfold Sep 12, 2022
c705894
wip use pagination for messagelogs
Forfold Sep 15, 2022
ace54d3
remove single log query and update schema + types
Forfold Sep 21, 2022
45f8767
get query working without dest type
Forfold Sep 22, 2022
3acb9b7
get destination info for message log
Forfold Sep 22, 2022
542daf8
address nullable fields
Forfold Sep 23, 2022
44cc1ef
fix pagination
Forfold Sep 27, 2022
474e23d
wip admin pages
Forfold Sep 27, 2022
9a7a0f0
fix layout
Forfold Sep 27, 2022
eb1dbc6
fix for external id nil
Forfold Sep 27, 2022
7a49386
remove unused components
Forfold Sep 27, 2022
4c89cce
dont show bundled alerts
Forfold Sep 27, 2022
224c89e
show log details on click
Forfold Sep 27, 2022
a9033d5
fix selectedlog styling
Forfold Oct 3, 2022
ae20d07
reintroduce debugmessages query as deprecated
Forfold Oct 4, 2022
d642768
show user, service, and destination properly
Forfold Oct 4, 2022
1fb325d
update ui tests
Forfold Oct 4, 2022
08dfcc2
Merge remote-tracking branch 'origin/master' into msg-logs-paginate
Forfold Oct 4, 2022
6ab4d00
Merge branch 'master' of https://github.com/target/goalert into msg-l…
tony-tvu Oct 10, 2022
c9d2a06
Update notification/search.go
Forfold Nov 1, 2022
f2bca30
prefer id over index
mastercactapus Nov 1, 2022
2ef3d69
don't set id's with print values
mastercactapus Nov 1, 2022
315db66
Merge branch 'msg-logs-paginate' of github.com:target/goalert into ms…
mastercactapus Nov 1, 2022
122bcab
run all admin tests
mastercactapus Nov 1, 2022
f923e63
keep note about why eslint exception is necessary
mastercactapus Nov 1, 2022
0d65948
add note for eslint exception
mastercactapus Nov 1, 2022
5fa171e
remove unused key
mastercactapus Nov 1, 2022
5210867
update gql layer
mastercactapus Nov 1, 2022
06115ab
add missing fields to search
mastercactapus Nov 1, 2022
3b38e36
dont fetch auth link unless we have a token
mastercactapus Nov 1, 2022
3fcb419
map query variables
mastercactapus Nov 1, 2022
aae7102
always scan into local vars
mastercactapus Nov 1, 2022
a174d79
set userid
mastercactapus Nov 1, 2022
cfc4603
handle nullable last_status_at
mastercactapus Nov 1, 2022
dab6149
make before check exclusive
mastercactapus Nov 1, 2022
7e6063d
fix next page calc
mastercactapus Nov 1, 2022
6699080
remove duplicate SrcValue
mastercactapus Nov 1, 2022
f02df68
Merge branch 'master' into msg-logs-paginate
mastercactapus Nov 1, 2022
9bdefba
remove duplicate CreateFAB
mastercactapus Nov 1, 2022
cafe758
skip tests using broken test util
Forfold Nov 1, 2022
1deadac
Update notification/search.go
Forfold Nov 2, 2022
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
385 changes: 385 additions & 0 deletions graphql2/generated.go

Large diffs are not rendered by default.

235 changes: 235 additions & 0 deletions graphql2/graphqlapp/messagelog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package graphqlapp

import (
"context"
"fmt"
"strings"

"github.com/google/uuid"
"github.com/target/goalert/graphql2"
"github.com/target/goalert/notification"
"github.com/target/goalert/notificationchannel"
"github.com/target/goalert/search"
"github.com/target/goalert/validation/validate"
)

type MessageLog App

func (a *App) formatNC(ctx context.Context, id string) (string, error) {
if id == "" {
return "", nil
}
uid, err := uuid.Parse(id)
if err != nil {
return "", err
}

n, err := a.FindOneNC(ctx, uid)
if err != nil {
return "", err
}
var typeName string
switch n.Type {
case notificationchannel.TypeSlack:
typeName = "Slack"
default:
typeName = string(n.Type)
}

return fmt.Sprintf("%s (%s)", n.Name, typeName), nil
}

func (q *Query) formatDest(ctx context.Context, dst notification.Dest) (string, error) {
if !dst.Type.IsUserCM() {
return (*App)(q).formatNC(ctx, dst.ID)
}

var str strings.Builder
str.WriteString((*App)(q).FormatDestFunc(ctx, dst.Type, dst.Value))
switch dst.Type {
case notification.DestTypeSMS:
str.WriteString(" (SMS)")
case notification.DestTypeUserEmail:
str.WriteString(" (Email)")
case notification.DestTypeVoice:
str.WriteString(" (Voice)")
case notification.DestTypeUserWebhook:
str.Reset()
str.WriteString("Webhook")
default:
str.Reset()
str.WriteString(dst.Type.String())
}

return str.String(), nil
}

func msgStatus(stat notification.Status) string {
var str strings.Builder
switch stat.State {
case notification.StateBundled:
str.WriteString("Bundled")
case notification.StateUnknown:
str.WriteString("Unknown")
case notification.StateSending:
str.WriteString("Sending")
case notification.StatePending:
str.WriteString("Pending")
case notification.StateSent:
str.WriteString("Sent")
case notification.StateDelivered:
str.WriteString("Delivered")
case notification.StateFailedTemp:
str.WriteString("Failed (temporary)")
case notification.StateFailedPerm:
str.WriteString("Failed (permanent)")
}
if stat.Details != "" {
str.WriteString(": ")
str.WriteString(stat.Details)
}
return str.String()
}

func (q *Query) MessageLogs(ctx context.Context, opts *graphql2.MessageLogSearchOptions) (conn *graphql2.MessageLogConnection, err error) {
if opts == nil {
opts = &graphql2.MessageLogSearchOptions{}
}
var searchOpts notification.SearchOptions
if opts.Search != nil {
searchOpts.Search = *opts.Search
}
searchOpts.Omit = opts.Omit
if opts.After != nil && *opts.After != "" {
err = search.ParseCursor(*opts.After, &searchOpts)
if err != nil {
return nil, err
}
}
if opts.First != nil {
err := validate.Range("First", *opts.First, 0, 100)
if err != nil {
return nil, err
}
searchOpts.Limit = *opts.First
}
if opts.CreatedAfter != nil {
searchOpts.CreatedAfter = *opts.CreatedAfter
}
if opts.CreatedBefore != nil {
searchOpts.CreatedBefore = *opts.CreatedBefore
}
if searchOpts.Limit == 0 {
searchOpts.Limit = 50
}

searchOpts.Limit++
logs, err := q.NotificationStore.Search(ctx, &searchOpts)
hasNextPage := len(logs) == searchOpts.Limit
searchOpts.Limit-- // prevent confusion later
if err != nil {
return nil, err
}

conn = new(graphql2.MessageLogConnection)
conn.PageInfo = &graphql2.PageInfo{
HasNextPage: hasNextPage,
}

if hasNextPage {
last := logs[len(logs)-1]
searchOpts.After.CreatedAt = last.CreatedAt
searchOpts.After.ID = last.ID

cur, err := search.Cursor(searchOpts)
if err != nil {
return conn, err
}
conn.PageInfo.EndCursor = &cur
}

if len(logs) > searchOpts.Limit {
// If we have next page, we've fetched MORE than one page, but we only want to return one page.
logs = logs[:searchOpts.Limit]
}

for _, _log := range logs {
log := _log
var dest notification.Dest
switch {
case log.ContactMethodID != "":
cm, err := (*App)(q).FindOneCM(ctx, log.ContactMethodID)
if err != nil {
return nil, fmt.Errorf("lookup contact method %s: %w", log.ContactMethodID, err)
}
dest = notification.DestFromPair(cm, nil)

case log.ChannelID != uuid.Nil:
nc, err := (*App)(q).FindOneNC(ctx, log.ChannelID)
if err != nil {
return nil, fmt.Errorf("lookup notification channel %s: %w", log.ChannelID, err)
}
dest = notification.DestFromPair(nil, nc)
}

dm := graphql2.DebugMessage{
ID: log.ID,
CreatedAt: log.CreatedAt,
UpdatedAt: log.LastStatusAt,
Type: strings.TrimPrefix(log.MessageType.String(), "MessageType"),
Status: msgStatus(notification.Status{State: log.LastStatus, Details: log.StatusDetails}),
AlertID: &log.AlertID,
}
if dest.ID != "" {
dm.Destination, err = q.formatDest(ctx, dest)
if err != nil {
return nil, fmt.Errorf("format dest: %w", err)
}
}
if log.UserID != "" {
dm.UserID = &log.UserID
}
if log.UserName != "" {
dm.UserName = &log.UserName
}
if log.SrcValue != "" {
src, err := q.formatDest(ctx, notification.Dest{Type: dest.Type, Value: log.SrcValue})
if err != nil {
return nil, fmt.Errorf("format src: %w", err)
}
dm.Source = &src
}
if log.ServiceID != "" {
dm.ServiceID = &log.ServiceID
}
if log.ServiceName != "" {
dm.ServiceName = &log.ServiceName
}
if log.AlertID != 0 {
dm.AlertID = &log.AlertID
}
if log.ProviderMsgID != nil {
dm.ProviderID = &log.ProviderMsgID.ExternalID
}

conn.Nodes = append(conn.Nodes, dm)
}

return conn, nil
}

func (q *Query) DebugMessages(ctx context.Context, input *graphql2.DebugMessagesInput) ([]graphql2.DebugMessage, error) {
if input.First != nil && *input.First > 100 {
*input.First = 100
}
conn, err := q.MessageLogs(ctx, &graphql2.MessageLogSearchOptions{
CreatedBefore: input.CreatedBefore,
CreatedAfter: input.CreatedAfter,
First: input.First,
})
if err != nil {
return nil, err
}

return conn.Nodes, nil
}
Loading