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

ui/services: add integration key filter for services list #2587

Merged
merged 19 commits into from
Sep 28, 2022
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
353 changes: 353 additions & 0 deletions graphql2/generated.go

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions graphql2/graphqlapp/integrationkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/target/goalert/config"
"github.com/target/goalert/graphql2"
"github.com/target/goalert/integrationkey"
"github.com/target/goalert/search"
)

type IntegrationKey App
Expand Down Expand Up @@ -58,3 +59,51 @@ func (key *IntegrationKey) Href(ctx context.Context, raw *integrationkey.Integra

return "", nil
}

func (q *Query) IntegrationKeys(ctx context.Context, input *graphql2.IntegrationKeySearchOptions) (conn *graphql2.IntegrationKeyConnection, err error) {
if input == nil {
input = &graphql2.IntegrationKeySearchOptions{}
}

var opts integrationkey.InKeySearchOptions
if input.Search != nil {
opts.Search = *input.Search
}
opts.Omit = input.Omit
if input.After != nil && *input.After != "" {
err = search.ParseCursor(*input.After, &opts)
if err != nil {
return conn, err
}
}
if input.First != nil {
opts.Limit = *input.First
}
if opts.Limit == 0 {
opts.Limit = 15
}

opts.Limit++
intKeys, err := q.IntKeyStore.Search(ctx, &opts)
if err != nil {
return nil, err
}
conn = new(graphql2.IntegrationKeyConnection)
conn.PageInfo = &graphql2.PageInfo{}
if len(intKeys) == opts.Limit {
intKeys = intKeys[:len(intKeys)-1]
conn.PageInfo.HasNextPage = true
}
if len(intKeys) > 0 {
lastKey := intKeys[len(intKeys)-1]
opts.After = lastKey.Name

cur, err := search.Cursor(opts)
if err != nil {
return nil, err
}
conn.PageInfo.EndCursor = &cur
}
conn.Nodes = intKeys
return conn, err
}
13 changes: 13 additions & 0 deletions graphql2/models_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions graphql2/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ type Query {
# Allows searching for label values.
labelValues(input: LabelValueSearchOptions): StringConnection!

# Allows searching for integration keys.
integrationKeys(input: IntegrationKeySearchOptions): IntegrationKeyConnection!

# Allows searching for user overrides.
userOverrides(input: UserOverrideSearchOptions): UserOverrideConnection!

Expand Down Expand Up @@ -231,6 +234,12 @@ type UserOverrideConnection {
nodes: [UserOverride!]!
pageInfo: PageInfo!
}

type IntegrationKeyConnection {
nodes: [IntegrationKey!]!
pageInfo: PageInfo!
}

type UserOverride {
id: ID!

Expand Down Expand Up @@ -268,6 +277,13 @@ input LabelValueSearchOptions {
omit: [String!]
}

input IntegrationKeySearchOptions {
first: Int = 15
after: String = ""
search: String = ""
omit: [String!]
}

type LabelConnection {
nodes: [Label!]!
pageInfo: PageInfo!
Expand Down
118 changes: 118 additions & 0 deletions integrationkey/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package integrationkey

import (
"context"
"database/sql"
"text/template"

"github.com/pkg/errors"
"github.com/target/goalert/permission"
"github.com/target/goalert/search"
"github.com/target/goalert/util/sqlutil"
"github.com/target/goalert/validation/validate"
)

// InKeySearchOptions allow filtering and paginating the list of rotations.
type InKeySearchOptions struct {
Search string `json:"s,omitempty"`
After string `json:"a,omitempty"`

// Omit specifies a list of key ids to exclude from the results.
Omit []string `json:"o,omitempty"`

Limit int `json:"-"`
}

var intKeySearchTemplate = template.Must(template.New("integration-key-search").Parse(`
SELECT DISTINCT
key.id, key.name, key.type, key.service_id
FROM integration_keys key
WHERE true
{{if .Omit}}
AND not key.id = any(:omit)
{{end}}
{{if .Search}}
AND (key.id::text ILIKE :search)
{{end}}
{{if .After}}
lower(key.name) > lower(:after)
{{end}}
LIMIT {{.Limit}}
`))

type intKeyRenderData InKeySearchOptions

func (opts intKeyRenderData) Normalize() (*intKeyRenderData, error) {
if opts.Limit == 0 {
opts.Limit = search.DefaultMaxResults
}

err := validate.Many(
validate.Search("Search", opts.Search),
validate.Range("Limit", opts.Limit, 0, search.MaxResults),
validate.Range("Omit", len(opts.Omit), 0, 50),
)

if err != nil {
return nil, err
}

return &opts, err
}

func (opts intKeyRenderData) SearchStr() string {
if opts.Search == "" {
return ""
}

return search.Escape(opts.Search) + "%"
}

func (opts intKeyRenderData) QueryArgs() []sql.NamedArg {

return []sql.NamedArg{
sql.Named("search", opts.SearchStr()),
sql.Named("after", opts.After),
sql.Named("omit", sqlutil.StringArray(opts.Omit)),
}
}

func (s *Store) Search(ctx context.Context, opts *InKeySearchOptions) ([]IntegrationKey, error) {
err := permission.LimitCheckAny(ctx, permission.User)
if err != nil {
return nil, err
}
if opts == nil {
opts = &InKeySearchOptions{}
}
data, err := (*intKeyRenderData)(opts).Normalize()
if err != nil {
return nil, err
}
query, args, err := search.RenderQuery(ctx, intKeySearchTemplate, data)
if err != nil {
return nil, errors.Wrap(err, "render query")
}

rows, err := s.db.QueryContext(ctx, query, args...)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
defer rows.Close()

var result []IntegrationKey
for rows.Next() {
var intKey IntegrationKey
err = rows.Scan(&intKey.ID, &intKey.Name, &intKey.Type, &intKey.ServiceID)
if err != nil {
return nil, errors.Wrap(err, "scan row")
}

result = append(result, intKey)
}

return result, nil
}
38 changes: 33 additions & 5 deletions service/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ var searchTemplate = template.Must(template.New("search").Funcs(search.Helpers()
fav IS DISTINCT FROM NULL
FROM services svc
{{if not .FavoritesOnly }}LEFT {{end}}JOIN user_favorites fav ON svc.id = fav.tgt_service_id AND {{if .FavoritesUserID}}fav.user_id = :favUserID{{else}}false{{end}}
{{if and .IntegrationKey}}
JOIN integration_keys intKey ON
intKey.service_id = svc.id AND
intKey.id = :integrationKey
{{end}}
{{if and .LabelKey (not .LabelNegate)}}
JOIN labels l ON
l.tgt_service_id = svc.id AND
Expand All @@ -71,7 +76,7 @@ var searchTemplate = template.Must(template.New("search").Funcs(search.Helpers()
{{if ne .LabelValue "*"}} AND value = :labelValue{{end}}
)
{{end}}
{{- if and .Search (not .LabelKey)}}
{{- if and .Search (not .LabelKey) (not .IntegrationKey)}}
AND {{prefixSearch "search" "svc.name"}}
{{- end}}
{{- if .After.Name}}
Expand All @@ -98,19 +103,38 @@ func (opts renderData) OrderBy() string {
return "lower(svc.name)"
}

func (opts renderData) IntegrationKey() string {
if !strings.Contains(opts.Search, "token=") {
return ""
}
return opts.Search[6:42]
}

func (opts renderData) LabelKey() string {
idx := strings.IndexByte(opts.Search, '=')
searchStr := opts.Search
if strings.Contains(opts.Search, "token=") {
// strip token string
searchStr = opts.Search[42:]
searchStr = strings.TrimSpace(searchStr)
}
idx := strings.IndexByte(searchStr, '=')
if idx == -1 {
return ""
}
return strings.TrimSuffix(opts.Search[:idx], "!") // if `!=`` is used
return strings.TrimSuffix(searchStr[:idx], "!") // if `!=`` is used
}
func (opts renderData) LabelValue() string {
idx := strings.IndexByte(opts.Search, '=')
searchStr := opts.Search
if strings.Contains(opts.Search, "token=") {
// strip token string
searchStr = opts.Search[42:]
searchStr = strings.TrimSpace(searchStr)
}
idx := strings.IndexByte(searchStr, '=')
if idx == -1 {
return ""
}
val := opts.Search[idx+1:]
val := searchStr[idx+1:]
if val == "" {
return "*"
}
Expand Down Expand Up @@ -144,6 +168,9 @@ func (opts renderData) Normalize() (*renderData, error) {
if err != nil {
return nil, err
}
if opts.IntegrationKey() != "" {
err = validate.Search("IntegrationKey", opts.IntegrationKey())
}
if opts.LabelKey() != "" {
err = validate.Search("LabelKey", opts.LabelKey())
if opts.LabelValue() != "*" {
Expand All @@ -162,6 +189,7 @@ func (opts renderData) Normalize() (*renderData, error) {
func (opts renderData) QueryArgs() []sql.NamedArg {
return []sql.NamedArg{
sql.Named("favUserID", opts.FavoritesUserID),
sql.Named("integrationKey", opts.IntegrationKey()),
sql.Named("labelKey", opts.LabelKey()),
sql.Named("labelValue", opts.LabelValue()),
sql.Named("labelNegate", opts.LabelNegate()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ import {
Box,
} from '@mui/material'
import makeStyles from '@mui/styles/makeStyles'
import ServiceLabelFilterContainer from '../../../services/ServiceLabelFilterContainer'
import ServiceLabelFilterContainer from '../../../services/ServiceFilterContainer'
import { Search as SearchIcon } from '@mui/icons-material'
import { FavoriteIcon } from '../../../util/SetFavoriteButton'
import { ServiceChip } from '../../../util/Chips'
import AddIcon from '@mui/icons-material/Add'
import _ from 'lodash'
import getServiceLabel from '../../../util/getServiceLabel'
import getServiceFilters from '../../../util/getServiceFilters'
import { CREATE_ALERT_LIMIT, DEBOUNCE_DELAY } from '../../../config'

import { allErrors } from '../../../util/errutil'
Expand Down Expand Up @@ -116,7 +116,7 @@ export function CreateAlertServiceSelect(props) {
return () => clearTimeout(t)
}, [searchUserInput])

const { labelKey, labelValue } = getServiceLabel(searchUserInput)
const { labelKey, labelValue } = getServiceFilters(searchUserInput)

const addAll = (e) => {
e.stopPropagation()
Expand Down
25 changes: 25 additions & 0 deletions web/src/app/selection/IntegrationKeySelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { gql } from '@apollo/client'
import { IntegrationKey } from '../../schema'
import { makeQuerySelect } from './QuerySelect'

const query = gql`
query ($input: IntegrationKeySearchOptions) {
integrationKeys(input: $input) {
nodes {
id
name
type
serviceID
}
}
}
`

export const IntegrationKeySelect = makeQuerySelect('IntegrationKeySelect', {
query,
mapDataNode: (key: IntegrationKey) => ({
label: key.id,
subText: key.name,
value: key.id,
}),
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { gql } from '@apollo/client'
import { makeQuerySelect } from './QuerySelect'
import p from 'prop-types'

const query = gql`
query ($input: LabelKeySearchOptions) {
Expand All @@ -12,8 +11,5 @@ const query = gql`

export const LabelKeySelect = makeQuerySelect('LabelKeySelect', {
query,
mapDataNode: (key) => ({ label: key, value: key }),
mapDataNode: (key: string) => ({ label: key, value: key }),
})
LabelKeySelect.propTypes = {
value: p.string,
}
Loading