Merge remote-tracking branch 'origin/master' into sgc/tsm1

# Conflicts:
#	go.mod
#	go.sum
#	query/promql/internal/promqltests/go.mod
pull/19446/head
Stuart Carnie 2020-08-12 09:07:30 -07:00
commit 56e27b8893
No known key found for this signature in database
GPG Key ID: 848D9C9718D78B4F
58 changed files with 1479 additions and 281 deletions

View File

@ -150,7 +150,7 @@ jobs:
- run: make protoc
- run: make build
- run:
command: ./bin/linux/influxd --store=memory --e2e-testing=true
command: ./bin/linux/influxd --store=memory --e2e-testing=true --feature-flags=communityTemplates=true
background: true
- run: make e2e
- store_test_results:

View File

@ -13,6 +13,7 @@
1. [19223](https://github.com/influxdata/influxdb/pull/19223): Add dashboards command to influx CLI
1. [19225](https://github.com/influxdata/influxdb/pull/19225): Allow user onboarding to optionally set passwords
1. [18841](https://github.com/influxdata/influxdb/pull/18841): Limit query response sizes for queries built in QueryBuilder by requiring an aggregate window
1. [19135](https://github.com/influxdata/influxdb/pull/19135): Add telegram notification.
### Bug Fixes

View File

@ -376,22 +376,29 @@ func convertQueries(qs []chronograf.DashboardQuery) []influxdb.DashboardQuery {
return ds
}
type dbrpMapper struct {
type dbrpMapper struct{}
// FindBy returns the dbrp mapping for the specified ID.
func (d dbrpMapper) FindByID(ctx context.Context, orgID influxdb.ID, id influxdb.ID) (*influxdb.DBRPMappingV2, error) {
return nil, errors.New("mapping not found")
}
func (m dbrpMapper) FindBy(ctx context.Context, cluster string, db string, rp string) (*influxdb.DBRPMapping, error) {
return nil, errors.New("mapping not found")
}
func (m dbrpMapper) Find(ctx context.Context, filter influxdb.DBRPMappingFilter) (*influxdb.DBRPMapping, error) {
return nil, errors.New("mapping not found")
}
func (m dbrpMapper) FindMany(ctx context.Context, filter influxdb.DBRPMappingFilter, opt ...influxdb.FindOptions) ([]*influxdb.DBRPMapping, int, error) {
// FindMany returns a list of dbrp mappings that match filter and the total count of matching dbrp mappings.
func (d dbrpMapper) FindMany(ctx context.Context, dbrp influxdb.DBRPMappingFilterV2, opts ...influxdb.FindOptions) ([]*influxdb.DBRPMappingV2, int, error) {
return nil, 0, errors.New("mapping not found")
}
func (m dbrpMapper) Create(ctx context.Context, dbrpMap *influxdb.DBRPMapping) error {
// Create creates a new dbrp mapping, if a different mapping exists an error is returned.
func (d dbrpMapper) Create(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error {
return errors.New("dbrpMapper does not support creating new mappings")
}
func (m dbrpMapper) Delete(ctx context.Context, cluster string, db string, rp string) error {
return errors.New("dbrpMapper does not support deleteing mappings")
// Update a new dbrp mapping
func (d dbrpMapper) Update(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error {
return errors.New("dbrpMapper does not support updating mappings")
}
// Delete removes a dbrp mapping.
func (d dbrpMapper) Delete(ctx context.Context, orgID influxdb.ID, id influxdb.ID) error {
return errors.New("dbrpMapper does not support deleting mappings")
}

View File

@ -62,19 +62,27 @@ func transpileF(cmd *cobra.Command, args []string) error {
type dbrpMapper struct{}
func (m dbrpMapper) FindBy(ctx context.Context, cluster string, db string, rp string) (*influxdb.DBRPMapping, error) {
// FindBy returns the dbrp mapping for the specified ID.
func (d dbrpMapper) FindByID(ctx context.Context, orgID influxdb.ID, id influxdb.ID) (*influxdb.DBRPMappingV2, error) {
return nil, errors.New("mapping not found")
}
func (m dbrpMapper) Find(ctx context.Context, filter influxdb.DBRPMappingFilter) (*influxdb.DBRPMapping, error) {
return nil, errors.New("mapping not found")
}
func (m dbrpMapper) FindMany(ctx context.Context, filter influxdb.DBRPMappingFilter, opt ...influxdb.FindOptions) ([]*influxdb.DBRPMapping, int, error) {
return nil, 0, errors.New("mapping not found")
// FindMany returns a list of dbrp mappings that match filter and the total count of matching dbrp mappings.
func (d dbrpMapper) FindMany(ctx context.Context, dbrp influxdb.DBRPMappingFilterV2, opts ...influxdb.FindOptions) ([]*influxdb.DBRPMappingV2, int, error) {
return nil, 0, errors.New("mapping not found")
}
func (m dbrpMapper) Create(ctx context.Context, dbrpMap *influxdb.DBRPMapping) error {
// Create creates a new dbrp mapping, if a different mapping exists an error is returned.
func (d dbrpMapper) Create(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error {
return errors.New("dbrpMapper does not support creating new mappings")
}
func (m dbrpMapper) Delete(ctx context.Context, cluster string, db string, rp string) error {
return errors.New("dbrpMapper does not support deleteing mappings")
// Update a new dbrp mapping
func (d dbrpMapper) Update(ctx context.Context, dbrp *influxdb.DBRPMappingV2) error {
return errors.New("dbrpMapper does not support updating mappings")
}
// Delete removes a dbrp mapping.
func (d dbrpMapper) Delete(ctx context.Context, orgID influxdb.ID, id influxdb.ID) error {
return errors.New("dbrpMapper does not support deleting mappings")
}

View File

@ -11169,6 +11169,7 @@ components:
- $ref: "#/components/schemas/SMTPNotificationRule"
- $ref: "#/components/schemas/PagerDutyNotificationRule"
- $ref: "#/components/schemas/HTTPNotificationRule"
- $ref: "#/components/schemas/TelegramNotificationRule"
discriminator:
propertyName: type
mapping:
@ -11176,6 +11177,7 @@ components:
smtp: "#/components/schemas/SMTPNotificationRule"
pagerduty: "#/components/schemas/PagerDutyNotificationRule"
http: "#/components/schemas/HTTPNotificationRule"
telegram: "#/components/schemas/TelegramNotificationRule"
NotificationRule:
allOf:
- $ref: "#/components/schemas/NotificationRuleDiscriminator"
@ -11377,6 +11379,31 @@ components:
enum: [pagerduty]
messageTemplate:
type: string
TelegramNotificationRule:
allOf:
- $ref: "#/components/schemas/NotificationRuleBase"
- $ref: "#/components/schemas/TelegramNotificationRuleBase"
TelegramNotificationRuleBase:
type: object
required: [type, messageTemplate, channel]
properties:
type:
description: The discriminator between other types of notification rules is "telegram".
type: string
enum: [telegram]
messageTemplate:
description: The message template as a flux interpolated string.
type: string
parseMode:
description: Parse mode of the message text per https://core.telegram.org/bots/api#formatting-options . Defaults to "MarkdownV2" .
type: string
enum:
- MarkdownV2
- HTML
- Markdown
disableWebPagePreview:
description: Disables preview of web links in the sent messages when "true". Defaults to "false" .
type: boolean
NotificationEndpointUpdate:
type: object
@ -11395,12 +11422,14 @@ components:
- $ref: "#/components/schemas/SlackNotificationEndpoint"
- $ref: "#/components/schemas/PagerDutyNotificationEndpoint"
- $ref: "#/components/schemas/HTTPNotificationEndpoint"
- $ref: "#/components/schemas/TelegramNotificationEndpoint"
discriminator:
propertyName: type
mapping:
slack: "#/components/schemas/SlackNotificationEndpoint"
pagerduty: "#/components/schemas/PagerDutyNotificationEndpoint"
http: "#/components/schemas/HTTPNotificationEndpoint"
telegram: "#/components/schemas/TelegramNotificationEndpoint"
NotificationEndpoint:
allOf:
- $ref: "#/components/schemas/NotificationEndpointDiscrimator"
@ -11519,9 +11548,22 @@ components:
description: Customized headers.
additionalProperties:
type: string
TelegramNotificationEndpoint:
type: object
allOf:
- $ref: "#/components/schemas/NotificationEndpointBase"
- type: object
required: [token, channel]
properties:
token:
description: Specifies the Telegram bot token. See https://core.telegram.org/bots#creating-a-new-bot .
type: string
channel:
description: ID of the telegram channel, a chat_id in https://core.telegram.org/bots/api#sendmessage .
type: string
NotificationEndpointType:
type: string
enum: ["slack", "pagerduty", "http"]
enum: ["slack", "pagerduty", "http", "telegram"]
DBRP:
required:
- orgID

10
node_modules/.yarn-integrity generated vendored Normal file
View File

@ -0,0 +1,10 @@
{
"systemParams": "darwin-x64-83",
"modulesFolders": [],
"flags": [],
"linkedModules": [],
"topLevelPatterns": [],
"lockfileEntries": {},
"files": [],
"artifacts": {}
}

View File

@ -12,12 +12,14 @@ const (
SlackType = "slack"
PagerDutyType = "pagerduty"
HTTPType = "http"
TelegramType = "telegram"
)
var typeToEndpoint = map[string]func() influxdb.NotificationEndpoint{
SlackType: func() influxdb.NotificationEndpoint { return &Slack{} },
PagerDutyType: func() influxdb.NotificationEndpoint { return &PagerDuty{} },
HTTPType: func() influxdb.NotificationEndpoint { return &HTTP{} },
TelegramType: func() influxdb.NotificationEndpoint { return &Telegram{} },
}
// UnmarshalJSON will convert the bytes to notification endpoint.

View File

@ -55,7 +55,7 @@ func TestValidEndpoint(t *testing.T) {
},
},
{
name: "empty name",
name: "empty name PagerDuty",
src: &endpoint.PagerDuty{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
@ -70,6 +70,22 @@ func TestValidEndpoint(t *testing.T) {
Msg: "Notification Endpoint Name can't be empty",
},
},
{
name: "empty name Telegram",
src: &endpoint.Telegram{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
},
Token: influxdb.SecretField{Key: id1 + "-token"},
Channel: "-1001406363649",
},
err: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "Notification Endpoint Name can't be empty",
},
},
{
name: "empty slack url",
src: &endpoint.Slack{
@ -136,6 +152,36 @@ func TestValidEndpoint(t *testing.T) {
Msg: "invalid http username/password for basic auth",
},
},
{
name: "empty telegram token",
src: &endpoint.Telegram{
Base: goodBase,
},
err: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "empty telegram bot token",
},
},
{
name: "empty telegram channel",
src: &endpoint.Telegram{
Base: goodBase,
Token: influxdb.SecretField{Key: id1 + "-token"},
},
err: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "empty telegram channel",
},
},
{
name: "valid telegram token",
src: &endpoint.Telegram{
Base: goodBase,
Token: influxdb.SecretField{Key: id1 + "-token"},
Channel: "-1001406363649",
},
err: nil,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@ -226,6 +272,22 @@ func TestJSON(t *testing.T) {
Password: influxdb.SecretField{Key: "password-key"},
},
},
{
name: "simple Telegram",
src: &endpoint.Telegram{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "nameTelegram",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
Token: influxdb.SecretField{Key: "token-key-1"},
},
},
}
for _, c := range cases {
b, err := json.Marshal(c.src)
@ -365,6 +427,40 @@ func TestBackFill(t *testing.T) {
},
},
},
{
name: "simple Telegram",
src: &endpoint.Telegram{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "name1",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
Token: influxdb.SecretField{
Value: strPtr("token-value"),
},
},
target: &endpoint.Telegram{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "name1",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
Token: influxdb.SecretField{
Key: id1 + "-token",
Value: strPtr("token-value"),
},
},
},
}
for _, c := range cases {
c.src.BackfillSecretKeys()
@ -374,6 +470,133 @@ func TestBackFill(t *testing.T) {
}
}
func TestSecretFields(t *testing.T) {
cases := []struct {
name string
src influxdb.NotificationEndpoint
secrets []influxdb.SecretField
}{
{
name: "simple Slack",
src: &endpoint.Slack{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "name1",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
URL: "https://slack.com/api/chat.postMessage",
Token: influxdb.SecretField{
Key: id1 + "-token",
Value: strPtr("token-value"),
},
},
secrets: []influxdb.SecretField{
{
Key: id1 + "-token",
Value: strPtr("token-value"),
},
},
},
{
name: "simple pagerduty",
src: &endpoint.PagerDuty{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "name1",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
ClientURL: "https://events.pagerduty.com/v2/enqueue",
RoutingKey: influxdb.SecretField{
Key: id1 + "-routing-key",
Value: strPtr("routing-key-value"),
},
},
secrets: []influxdb.SecretField{
{
Key: id1 + "-routing-key",
Value: strPtr("routing-key-value"),
},
},
},
{
name: "http with user and password",
src: &endpoint.HTTP{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "name1",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
AuthMethod: "basic",
URL: "http://example.com",
Username: influxdb.SecretField{
Key: id1 + "-username",
Value: strPtr("user1"),
},
Password: influxdb.SecretField{
Key: id1 + "-password",
Value: strPtr("password1"),
},
},
secrets: []influxdb.SecretField{
{
Key: id1 + "-username",
Value: strPtr("user1"),
},
{
Key: id1 + "-password",
Value: strPtr("password1"),
},
},
},
{
name: "simple Telegram",
src: &endpoint.Telegram{
Base: endpoint.Base{
ID: influxTesting.MustIDBase16Ptr(id1),
Name: "name1",
OrgID: influxTesting.MustIDBase16Ptr(id3),
Status: influxdb.Active,
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
Token: influxdb.SecretField{
Key: id1 + "-token",
Value: strPtr("token-value"),
},
},
secrets: []influxdb.SecretField{
{
Key: id1 + "-token",
Value: strPtr("token-value"),
},
},
},
}
for _, c := range cases {
secretFields := c.src.SecretFields()
if diff := cmp.Diff(c.secrets, secretFields); diff != "" {
t.Errorf("failed %s, NotificationEndpoint are different -got/+want\ndiff %s", c.name, diff)
}
}
}
func strPtr(s string) *string {
ss := new(string)
*ss = s

View File

@ -0,0 +1,75 @@
package endpoint
import (
"encoding/json"
"github.com/influxdata/influxdb/v2"
)
var _ influxdb.NotificationEndpoint = &Telegram{}
const telegramTokenSuffix = "-token"
// Telegram is the notification endpoint config of telegram.
type Telegram struct {
Base
// Token is the telegram bot token, see https://core.telegram.org/bots#creating-a-new-bot
Token influxdb.SecretField `json:"token"`
// Channel is an ID of the telegram channel, see https://core.telegram.org/bots/api#sendmessage
Channel string `json:"channel"`
}
// BackfillSecretKeys fill back the secret field key during the unmarshalling
// if value of that secret field is not nil.
func (s *Telegram) BackfillSecretKeys() {
if s.Token.Key == "" && s.Token.Value != nil {
s.Token.Key = s.idStr() + telegramTokenSuffix
}
}
// SecretFields return available secret fields.
func (s Telegram) SecretFields() []influxdb.SecretField {
arr := []influxdb.SecretField{}
if s.Token.Key != "" {
arr = append(arr, s.Token)
}
return arr
}
// Valid returns error if some configuration is invalid
func (s Telegram) Valid() error {
if err := s.Base.valid(); err != nil {
return err
}
if s.Token.Key == "" {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "empty telegram bot token",
}
}
if s.Channel == "" {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "empty telegram channel",
}
}
return nil
}
// MarshalJSON implement json.Marshaler interface.
func (s Telegram) MarshalJSON() ([]byte, error) {
type telegramAlias Telegram
return json.Marshal(
struct {
telegramAlias
Type string `json:"type"`
}{
telegramAlias: telegramAlias(s),
Type: s.Type(),
})
}
// Type returns the type.
func (s Telegram) Type() string {
return TelegramType
}

View File

@ -16,6 +16,7 @@ var typeToRule = map[string](func() influxdb.NotificationRule){
"slack": func() influxdb.NotificationRule { return &Slack{} },
"pagerduty": func() influxdb.NotificationRule { return &PagerDuty{} },
"http": func() influxdb.NotificationRule { return &HTTP{} },
"telegram": func() influxdb.NotificationRule { return &Telegram{} },
}
// UnmarshalJSON will convert

View File

@ -321,6 +321,41 @@ func TestJSON(t *testing.T) {
MessageTemplate: "msg1",
},
},
{
name: "simple telegram",
src: &rule.Telegram{
Base: rule.Base{
ID: influxTesting.MustIDBase16(id1),
OwnerID: influxTesting.MustIDBase16(id2),
Name: "name1",
OrgID: influxTesting.MustIDBase16(id3),
RunbookLink: "runbooklink1",
SleepUntil: &time3,
Every: mustDuration("1h"),
TagRules: []notification.TagRule{
{
Tag: influxdb.Tag{
Key: "k1",
Value: "v1",
},
Operator: influxdb.NotEqual,
},
{
Tag: influxdb.Tag{
Key: "k2",
Value: "v2",
},
Operator: influxdb.RegexEqual,
},
},
CRUDLog: influxdb.CRUDLog{
CreatedAt: timeGen1.Now(),
UpdatedAt: timeGen2.Now(),
},
},
MessageTemplate: "blah",
},
},
}
for _, c := range cases {
b, err := json.Marshal(c.src)

View File

@ -0,0 +1,140 @@
package rule
import (
"encoding/json"
"fmt"
"github.com/influxdata/flux/ast"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/notification/endpoint"
"github.com/influxdata/influxdb/v2/notification/flux"
)
// Telegram is the notification rule config of telegram.
type Telegram struct {
Base
MessageTemplate string `json:"messageTemplate"`
ParseMode string `json:"parseMode"`
DisableWebPagePreview bool `json:"disableWebPagePreview"`
}
// GenerateFlux generates a flux script for the telegram notification rule.
func (s *Telegram) GenerateFlux(e influxdb.NotificationEndpoint) (string, error) {
telegramEndpoint, ok := e.(*endpoint.Telegram)
if !ok {
return "", fmt.Errorf("endpoint provided is a %s, not a Telegram endpoint", e.Type())
}
p, err := s.GenerateFluxAST(telegramEndpoint)
if err != nil {
return "", err
}
return ast.Format(p), nil
}
// GenerateFluxAST generates a flux AST for the telegram notification rule.
func (s *Telegram) GenerateFluxAST(e *endpoint.Telegram) (*ast.Package, error) {
f := flux.File(
s.Name,
flux.Imports("influxdata/influxdb/monitor", "contrib/sranka/telegram", "influxdata/influxdb/secrets", "experimental"),
s.generateFluxASTBody(e),
)
return &ast.Package{Package: "main", Files: []*ast.File{f}}, nil
}
func (s *Telegram) generateFluxASTBody(e *endpoint.Telegram) []ast.Statement {
var statements []ast.Statement
statements = append(statements, s.generateTaskOption())
if e.Token.Key != "" {
statements = append(statements, s.generateFluxASTSecrets(e))
}
statements = append(statements, s.generateFluxASTEndpoint(e))
statements = append(statements, s.generateFluxASTNotificationDefinition(e))
statements = append(statements, s.generateFluxASTStatuses())
statements = append(statements, s.generateLevelChecks()...)
statements = append(statements, s.generateFluxASTNotifyPipe(e))
return statements
}
func (s *Telegram) generateFluxASTSecrets(e *endpoint.Telegram) ast.Statement {
call := flux.Call(flux.Member("secrets", "get"), flux.Object(flux.Property("key", flux.String(e.Token.Key))))
return flux.DefineVariable("telegram_secret", call)
}
func (s *Telegram) generateFluxASTEndpoint(e *endpoint.Telegram) ast.Statement {
props := []*ast.Property{}
if e.Token.Key != "" {
props = append(props, flux.Property("token", flux.Identifier("telegram_secret")))
}
if s.ParseMode != "" {
props = append(props, flux.Property("parseMode", flux.String(s.ParseMode)))
}
props = append(props, flux.Property("disableWebPagePreview", flux.Bool(s.DisableWebPagePreview)))
call := flux.Call(flux.Member("telegram", "endpoint"), flux.Object(props...))
return flux.DefineVariable("telegram_endpoint", call)
}
func (s *Telegram) generateFluxASTNotifyPipe(e *endpoint.Telegram) ast.Statement {
endpointProps := []*ast.Property{}
endpointProps = append(endpointProps, flux.Property("channel", flux.String(e.Channel)))
endpointProps = append(endpointProps, flux.Property("text", flux.String(s.MessageTemplate)))
endpointProps = append(endpointProps, flux.Property("silent", s.generateSilent()))
endpointFn := flux.Function(flux.FunctionParams("r"), flux.Object(endpointProps...))
props := []*ast.Property{}
props = append(props, flux.Property("data", flux.Identifier("notification")))
props = append(props, flux.Property("endpoint",
flux.Call(flux.Identifier("telegram_endpoint"), flux.Object(flux.Property("mapFn", endpointFn)))))
call := flux.Call(flux.Member("monitor", "notify"), flux.Object(props...))
return flux.ExpressionStatement(flux.Pipe(flux.Identifier("all_statuses"), call))
}
func (s *Telegram) generateSilent() ast.Expression {
level := flux.Member("r", "_level")
return flux.If(
flux.Equal(level, flux.String("crit")),
flux.Bool(true),
flux.If(
flux.Equal(level, flux.String("warn")),
flux.Bool(true),
flux.Bool(false),
),
)
}
type telegramAlias Telegram
// MarshalJSON implement json.Marshaler interface.
func (s Telegram) MarshalJSON() ([]byte, error) {
return json.Marshal(
struct {
telegramAlias
Type string `json:"type"`
}{
telegramAlias: telegramAlias(s),
Type: s.Type(),
})
}
// Valid returns where the config is valid.
func (s Telegram) Valid() error {
if err := s.Base.valid(); err != nil {
return err
}
if s.MessageTemplate == "" {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "Telegram MessageTemplate is invalid",
}
}
return nil
}
// Type returns the type of the rule config.
func (s Telegram) Type() string {
return "telegram"
}

View File

@ -0,0 +1,308 @@
package rule_test
import (
"testing"
"github.com/andreyvit/diff"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/notification"
"github.com/influxdata/influxdb/v2/notification/endpoint"
"github.com/influxdata/influxdb/v2/notification/rule"
influxTesting "github.com/influxdata/influxdb/v2/testing"
)
var _ influxdb.NotificationRule = &rule.Telegram{}
func TestTelegram_GenerateFlux(t *testing.T) {
tests := []struct {
name string
rule *rule.Telegram
endpoint influxdb.NotificationEndpoint
script string
}{
{
name: "incompatible with endpoint",
endpoint: &endpoint.Slack{
Base: endpoint.Base{
ID: idPtr(3),
Name: "foo",
},
URL: "http://whatever",
},
rule: &rule.Telegram{
MessageTemplate: "blah",
Base: rule.Base{
ID: 1,
EndpointID: 3,
Name: "foo",
Every: mustDuration("1h"),
StatusRules: []notification.StatusRule{
{
CurrentLevel: notification.Critical,
},
},
TagRules: []notification.TagRule{
{
Tag: influxdb.Tag{
Key: "foo",
Value: "bar",
},
Operator: influxdb.Equal,
},
{
Tag: influxdb.Tag{
Key: "baz",
Value: "bang",
},
Operator: influxdb.Equal,
},
},
},
},
script: "", //no script generater, because of incompatible endpoint
},
{
name: "notify on crit",
endpoint: &endpoint.Telegram{
Base: endpoint.Base{
ID: idPtr(3),
Name: "foo",
},
Token: influxdb.SecretField{Key: "3-key"},
Channel: "-12345",
},
rule: &rule.Telegram{
MessageTemplate: "blah",
Base: rule.Base{
ID: 1,
EndpointID: 3,
Name: "foo",
Every: mustDuration("1h"),
StatusRules: []notification.StatusRule{
{
CurrentLevel: notification.Critical,
},
},
TagRules: []notification.TagRule{
{
Tag: influxdb.Tag{
Key: "foo",
Value: "bar",
},
Operator: influxdb.Equal,
},
{
Tag: influxdb.Tag{
Key: "baz",
Value: "bang",
},
Operator: influxdb.Equal,
},
},
},
},
script: `package main
// foo
import "influxdata/influxdb/monitor"
import "contrib/sranka/telegram"
import "influxdata/influxdb/secrets"
import "experimental"
option task = {name: "foo", every: 1h}
telegram_secret = secrets["get"](key: "3-key")
telegram_endpoint = telegram["endpoint"](token: telegram_secret, disableWebPagePreview: false)
notification = {
_notification_rule_id: "0000000000000001",
_notification_rule_name: "foo",
_notification_endpoint_id: "0000000000000003",
_notification_endpoint_name: "foo",
}
statuses = monitor["from"](start: -2h, fn: (r) =>
(r["foo"] == "bar" and r["baz"] == "bang"))
crit = statuses
|> filter(fn: (r) =>
(r["_level"] == "crit"))
all_statuses = crit
|> filter(fn: (r) =>
(r["_time"] > experimental["subDuration"](from: now(), d: 1h)))
all_statuses
|> monitor["notify"](data: notification, endpoint: telegram_endpoint(mapFn: (r) =>
({channel: "-12345", text: "blah", silent: if r["_level"] == "crit" then true else if r["_level"] == "warn" then true else false})))`,
},
{
name: "with DisableWebPagePreview and ParseMode",
endpoint: &endpoint.Telegram{
Base: endpoint.Base{
ID: idPtr(3),
Name: "foo",
},
Token: influxdb.SecretField{Key: "3-key"},
Channel: "-12345",
},
rule: &rule.Telegram{
MessageTemplate: "blah",
DisableWebPagePreview: true,
ParseMode: "HTML",
Base: rule.Base{
ID: 1,
EndpointID: 3,
Name: "foo",
Every: mustDuration("1h"),
StatusRules: []notification.StatusRule{
{
CurrentLevel: notification.Any,
},
},
TagRules: []notification.TagRule{
{
Tag: influxdb.Tag{
Key: "foo",
Value: "bar",
},
Operator: influxdb.Equal,
},
{
Tag: influxdb.Tag{
Key: "baz",
Value: "bang",
},
Operator: influxdb.Equal,
},
},
},
},
script: `package main
// foo
import "influxdata/influxdb/monitor"
import "contrib/sranka/telegram"
import "influxdata/influxdb/secrets"
import "experimental"
option task = {name: "foo", every: 1h}
telegram_secret = secrets["get"](key: "3-key")
telegram_endpoint = telegram["endpoint"](token: telegram_secret, parseMode: "HTML", disableWebPagePreview: true)
notification = {
_notification_rule_id: "0000000000000001",
_notification_rule_name: "foo",
_notification_endpoint_id: "0000000000000003",
_notification_endpoint_name: "foo",
}
statuses = monitor["from"](start: -2h, fn: (r) =>
(r["foo"] == "bar" and r["baz"] == "bang"))
any = statuses
|> filter(fn: (r) =>
(true))
all_statuses = any
|> filter(fn: (r) =>
(r["_time"] > experimental["subDuration"](from: now(), d: 1h)))
all_statuses
|> monitor["notify"](data: notification, endpoint: telegram_endpoint(mapFn: (r) =>
({channel: "-12345", text: "blah", silent: if r["_level"] == "crit" then true else if r["_level"] == "warn" then true else false})))`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
script, err := tt.rule.GenerateFlux(tt.endpoint)
if err != nil {
if script != "" {
t.Errorf("Failed to generate flux: %v", err)
}
return
}
if got, want := script, tt.script; got != want {
t.Errorf("\n\nStrings do not match:\n\n%s", diff.LineDiff(got, want))
}
})
}
}
func TestTelegram_Valid(t *testing.T) {
cases := []struct {
name string
rule *rule.Telegram
err error
}{
{
name: "valid template",
rule: &rule.Telegram{
MessageTemplate: "blah",
Base: rule.Base{
ID: 1,
EndpointID: 3,
OwnerID: 4,
OrgID: 5,
Name: "foo",
Every: mustDuration("1h"),
StatusRules: []notification.StatusRule{
{
CurrentLevel: notification.Critical,
},
},
TagRules: []notification.TagRule{},
},
},
err: nil,
},
{
name: "missing MessageTemplate",
rule: &rule.Telegram{
MessageTemplate: "",
Base: rule.Base{
ID: 1,
EndpointID: 3,
OwnerID: 4,
OrgID: 5,
Name: "foo",
Every: mustDuration("1h"),
StatusRules: []notification.StatusRule{
{
CurrentLevel: notification.Critical,
},
},
TagRules: []notification.TagRule{},
},
},
err: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "Telegram MessageTemplate is invalid",
},
},
{
name: "missing EndpointID",
rule: &rule.Telegram{
MessageTemplate: "",
Base: rule.Base{
ID: 1,
// EndpointID: 3,
OwnerID: 4,
OrgID: 5,
Name: "foo",
Every: mustDuration("1h"),
StatusRules: []notification.StatusRule{
{
CurrentLevel: notification.Critical,
},
},
TagRules: []notification.TagRule{},
},
},
err: &influxdb.Error{
Code: influxdb.EInvalid,
Msg: "Notification Rule EndpointID is invalid",
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := c.rule.Valid()
influxTesting.ErrorsEqual(t, got, c.err)
})
}
}

View File

@ -13,7 +13,7 @@ import (
const CompilerType = "influxql"
// AddCompilerMappings adds the influxql specific compiler mappings.
func AddCompilerMappings(mappings flux.CompilerMappings, dbrpMappingSvc platform.DBRPMappingService) error {
func AddCompilerMappings(mappings flux.CompilerMappings, dbrpMappingSvc platform.DBRPMappingServiceV2) error {
return mappings.Add(CompilerType, func() flux.Compiler {
return NewCompiler(dbrpMappingSvc)
})
@ -30,12 +30,12 @@ type Compiler struct {
logicalPlannerOptions []plan.LogicalOption
dbrpMappingSvc platform.DBRPMappingService
dbrpMappingSvc platform.DBRPMappingServiceV2
}
var _ flux.Compiler = &Compiler{}
func NewCompiler(dbrpMappingSvc platform.DBRPMappingService) *Compiler {
func NewCompiler(dbrpMappingSvc platform.DBRPMappingServiceV2) *Compiler {
return &Compiler{
dbrpMappingSvc: dbrpMappingSvc,
}

View File

@ -27,25 +27,21 @@ import (
const generatedInfluxQLDataDir = "testdata"
var dbrpMappingSvcE2E = mock.NewDBRPMappingService()
var dbrpMappingSvcE2E = &mock.DBRPMappingServiceV2{}
func init() {
mapping := platform.DBRPMapping{
Cluster: "cluster",
mapping := platform.DBRPMappingV2{
Database: "db0",
RetentionPolicy: "autogen",
Default: true,
OrganizationID: platformtesting.MustIDBase16("cadecadecadecade"),
BucketID: platformtesting.MustIDBase16("da7aba5e5eedca5e"),
}
dbrpMappingSvcE2E.FindByFn = func(ctx context.Context, cluster string, db string, rp string) (*platform.DBRPMapping, error) {
dbrpMappingSvcE2E.FindByIDFn = func(ctx context.Context, orgID, id platform.ID) (*platform.DBRPMappingV2, error) {
return &mapping, nil
}
dbrpMappingSvcE2E.FindFn = func(ctx context.Context, filter platform.DBRPMappingFilter) (*platform.DBRPMapping, error) {
return &mapping, nil
}
dbrpMappingSvcE2E.FindManyFn = func(ctx context.Context, filter platform.DBRPMappingFilter, opt ...platform.FindOptions) ([]*platform.DBRPMapping, int, error) {
return []*platform.DBRPMapping{&mapping}, 1, nil
dbrpMappingSvcE2E.FindManyFn = func(ctx context.Context, filter platform.DBRPMappingFilterV2, opt ...platform.FindOptions) ([]*platform.DBRPMappingV2, int, error) {
return []*platform.DBRPMappingV2{&mapping}, 1, nil
}
}

View File

@ -16,46 +16,35 @@ import (
platformtesting "github.com/influxdata/influxdb/v2/testing"
)
var dbrpMappingSvc = mock.NewDBRPMappingService()
var dbrpMappingSvc = &mock.DBRPMappingServiceV2{}
var organizationID platform.ID
var bucketID platform.ID
var altBucketID platform.ID
func init() {
mapping := platform.DBRPMapping{
Cluster: "cluster",
mapping := platform.DBRPMappingV2{
Database: "db0",
RetentionPolicy: "autogen",
Default: true,
OrganizationID: organizationID,
BucketID: bucketID,
}
altMapping := platform.DBRPMapping{
Cluster: "cluster",
altMapping := platform.DBRPMappingV2{
Database: "db0",
RetentionPolicy: "autogen",
Default: true,
OrganizationID: organizationID,
BucketID: altBucketID,
}
dbrpMappingSvc.FindByFn = func(ctx context.Context, cluster string, db string, rp string) (*platform.DBRPMapping, error) {
if rp == "alternate" {
return &altMapping, nil
}
dbrpMappingSvc.FindByIDFn = func(ctx context.Context, orgID, id platform.ID) (*platform.DBRPMappingV2, error) {
return &mapping, nil
}
dbrpMappingSvc.FindFn = func(ctx context.Context, filter platform.DBRPMappingFilter) (*platform.DBRPMapping, error) {
if filter.RetentionPolicy != nil && *filter.RetentionPolicy == "alternate" {
return &altMapping, nil
}
return &mapping, nil
}
dbrpMappingSvc.FindManyFn = func(ctx context.Context, filter platform.DBRPMappingFilter, opt ...platform.FindOptions) ([]*platform.DBRPMapping, int, error) {
dbrpMappingSvc.FindManyFn = func(ctx context.Context, filter platform.DBRPMappingFilterV2, opt ...platform.FindOptions) ([]*platform.DBRPMappingV2, int, error) {
m := &mapping
if filter.RetentionPolicy != nil && *filter.RetentionPolicy == "alternate" {
m = &altMapping
}
return []*platform.DBRPMapping{m}, 1, nil
return []*platform.DBRPMappingV2{m}, 1, nil
}
}

View File

@ -16,14 +16,14 @@ import (
// Transpiler converts InfluxQL queries into a query spec.
type Transpiler struct {
Config *Config
dbrpMappingSvc influxdb.DBRPMappingService
dbrpMappingSvc influxdb.DBRPMappingServiceV2
}
func NewTranspiler(dbrpMappingSvc influxdb.DBRPMappingService) *Transpiler {
func NewTranspiler(dbrpMappingSvc influxdb.DBRPMappingServiceV2) *Transpiler {
return NewTranspilerWithConfig(dbrpMappingSvc, Config{})
}
func NewTranspilerWithConfig(dbrpMappingSvc influxdb.DBRPMappingService, cfg Config) *Transpiler {
func NewTranspilerWithConfig(dbrpMappingSvc influxdb.DBRPMappingServiceV2, cfg Config) *Transpiler {
return &Transpiler{
Config: &cfg,
dbrpMappingSvc: dbrpMappingSvc,
@ -56,10 +56,10 @@ type transpilerState struct {
config Config
file *ast.File
assignments map[string]ast.Expression
dbrpMappingSvc influxdb.DBRPMappingService
dbrpMappingSvc influxdb.DBRPMappingServiceV2
}
func newTranspilerState(dbrpMappingSvc influxdb.DBRPMappingService, config *Config) *transpilerState {
func newTranspilerState(dbrpMappingSvc influxdb.DBRPMappingServiceV2, config *Config) *transpilerState {
state := &transpilerState{
file: &ast.File{
Package: &ast.PackageClause{
@ -695,8 +695,7 @@ func (t *transpilerState) from(m *influxql.Measurement) (ast.Expression, error)
}
}
var filter influxdb.DBRPMappingFilter
filter.Cluster = &t.config.Cluster
var filter influxdb.DBRPMappingFilterV2
if db != "" {
filter.Database = &db
}
@ -705,8 +704,8 @@ func (t *transpilerState) from(m *influxql.Measurement) (ast.Expression, error)
}
defaultRP := rp == ""
filter.Default = &defaultRP
mapping, err := t.dbrpMappingSvc.Find(context.TODO(), filter)
if err != nil {
mappings, _, err := t.dbrpMappingSvc.FindMany(context.TODO(), filter)
if err != nil || len(mappings) == 0 {
if !t.config.FallbackToDBRP {
return nil, err
}
@ -735,7 +734,7 @@ func (t *transpilerState) from(m *influxql.Measurement) (ast.Expression, error)
Name: "bucketID",
},
Value: &ast.StringLiteral{
Value: mapping.BucketID.String(),
Value: mappings[0].BucketID.String(),
},
},
},

View File

@ -13,25 +13,21 @@ import (
"github.com/pkg/errors"
)
var dbrpMappingSvc = mock.NewDBRPMappingService()
var dbrpMappingSvc = &mock.DBRPMappingServiceV2{}
func init() {
mapping := platform.DBRPMapping{
Cluster: "cluster",
mapping := platform.DBRPMappingV2{
Database: "db0",
RetentionPolicy: "autogen",
Default: true,
OrganizationID: platformtesting.MustIDBase16("aaaaaaaaaaaaaaaa"),
BucketID: platformtesting.MustIDBase16("bbbbbbbbbbbbbbbb"),
}
dbrpMappingSvc.FindByFn = func(ctx context.Context, cluster string, db string, rp string) (*platform.DBRPMapping, error) {
dbrpMappingSvc.FindByIDFn = func(ctx context.Context, orgID, id platform.ID) (*platform.DBRPMappingV2, error) {
return &mapping, nil
}
dbrpMappingSvc.FindFn = func(ctx context.Context, filter platform.DBRPMappingFilter) (*platform.DBRPMapping, error) {
return &mapping, nil
}
dbrpMappingSvc.FindManyFn = func(ctx context.Context, filter platform.DBRPMappingFilter, opt ...platform.FindOptions) ([]*platform.DBRPMapping, int, error) {
return []*platform.DBRPMapping{&mapping}, 1, nil
dbrpMappingSvc.FindManyFn = func(ctx context.Context, filter platform.DBRPMappingFilterV2, opt ...platform.FindOptions) ([]*platform.DBRPMappingV2, int, error) {
return []*platform.DBRPMappingV2{&mapping}, 1, nil
}
}

View File

@ -389,8 +389,8 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/cron v0.0.0-20191203200038-ded12750aac6 h1:OtjKkeWDjUbyMi82C7XXy7Tvm2LXMwiBBXyFIGNPaGA=
github.com/influxdata/cron v0.0.0-20191203200038-ded12750aac6/go.mod h1:XabtPPW2qsCg0tl+kjaPU+cFS+CjQXEXbT1VJvHT4og=
github.com/influxdata/flux v0.77.1 h1:cOC/Kash0jSyeFnYdTEGicHvJnm+4BtazwglSpOIKIM=
github.com/influxdata/flux v0.77.1/go.mod h1:sAAIEgQTlTpsXCUQ49ymoRsKqraPzIb7F3paT72/lE0=
github.com/influxdata/flux v0.80.0 h1:lKZyJNgJf/oCzSUknAqAEq0OwwV6WUU+ZNKFUx2TaBQ=
github.com/influxdata/flux v0.80.0/go.mod h1:sAAIEgQTlTpsXCUQ49ymoRsKqraPzIb7F3paT72/lE0=
github.com/influxdata/httprouter v1.3.1-0.20191122104820-ee83e2772f69 h1:WQsmW0fXO4ZE/lFGIE84G6rIV5SJN3P3sjIXAP1a8eU=
github.com/influxdata/httprouter v1.3.1-0.20191122104820-ee83e2772f69/go.mod h1:pwymjR6SrP3gD3pRj9RJwdl1j5s3doEEV8gS4X9qSzA=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=

View File

@ -69,7 +69,7 @@ func init() {
type DatabasesDecoder struct {
orgID platform.ID
deps *DatabasesDependencies
databases []*platform.DBRPMapping
databases []*platform.DBRPMappingV2
alloc *memory.Allocator
}
@ -78,7 +78,7 @@ func (bd *DatabasesDecoder) Connect(ctx context.Context) error {
}
func (bd *DatabasesDecoder) Fetch(ctx context.Context) (bool, error) {
b, _, err := bd.deps.DBRP.FindMany(ctx, platform.DBRPMappingFilter{})
b, _, err := bd.deps.DBRP.FindMany(ctx, platform.DBRPMappingFilterV2{})
if err != nil {
return false, err
}
@ -88,7 +88,7 @@ func (bd *DatabasesDecoder) Fetch(ctx context.Context) (bool, error) {
func (bd *DatabasesDecoder) Decode(ctx context.Context) (flux.Table, error) {
type databaseInfo struct {
*platform.DBRPMapping
*platform.DBRPMappingV2
RetentionPeriod time.Duration
}
@ -103,7 +103,7 @@ func (bd *DatabasesDecoder) Decode(ctx context.Context) (flux.Table, error) {
return nil, err
}
databases = append(databases, databaseInfo{
DBRPMapping: db,
DBRPMappingV2: db,
RetentionPeriod: bucket.RetentionPeriod,
})
}
@ -198,7 +198,7 @@ type key int
const dependenciesKey key = iota
type DatabasesDependencies struct {
DBRP platform.DBRPMappingService
DBRP platform.DBRPMappingServiceV2
BucketLookup platform.BucketService
}

View File

@ -96,6 +96,10 @@ var FluxEndToEndSkipList = map[string]map[string]string{
"join": "unbounded test",
"alignTime": "unbounded test",
},
"experimental/array": {
"from": "test not meant to be consumed by influxdb",
"from_group": "test not meant to be consumed by influxdb",
},
"experimental/geo": {
"filterRowsNotStrict": "tableFind does not work in e2e tests: https://github.com/influxdata/influxdb/issues/13975",
"filterRowsStrict": "tableFind does not work in e2e tests: https://github.com/influxdata/influxdb/issues/13975",

View File

@ -41,7 +41,7 @@ func (s *UserResourceMappingClient) FindUserResourceMappings(ctx context.Context
urs[k] = &influxdb.UserResourceMapping{
ResourceID: f.ResourceID,
ResourceType: f.ResourceType,
UserID: item.ID,
UserID: item.User.ID,
UserType: item.Role,
}
}
@ -90,7 +90,7 @@ func (s *SpecificURMSvc) FindUserResourceMappings(ctx context.Context, f influxd
urs[k] = &influxdb.UserResourceMapping{
ResourceID: f.ResourceID,
ResourceType: f.ResourceType,
UserID: item.ID,
UserID: item.User.ID,
UserType: item.Role,
}
}

View File

@ -63,7 +63,7 @@ func (h *urmHandler) getURMsByType(w http.ResponseWriter, r *http.Request) {
return
}
users := make([]influxdb.User, 0, len(mappings))
users := make([]*influxdb.User, 0, len(mappings))
for _, m := range mappings {
if m.MappingType == influxdb.OrgMappingType {
continue
@ -74,7 +74,7 @@ func (h *urmHandler) getURMsByType(w http.ResponseWriter, r *http.Request) {
return
}
users = append(users, *user)
users = append(users, user)
}
h.log.Debug("Members/owners retrieved", zap.String("users", fmt.Sprint(users)))
@ -134,7 +134,7 @@ func (h *urmHandler) postURMByType(w http.ResponseWriter, r *http.Request) {
}
h.log.Debug("Member/owner created", zap.String("mapping", fmt.Sprint(mapping)))
h.api.Respond(w, r, http.StatusCreated, newResourceUserResponse(*user, userType))
h.api.Respond(w, r, http.StatusCreated, newResourceUserResponse(user, userType))
}
type postRequest struct {
@ -229,27 +229,15 @@ func (h *urmHandler) decodeDeleteRequest(ctx context.Context, r *http.Request) (
}, nil
}
type URMUserResponse struct {
Links map[string]string `json:"links"`
ID influxdb.ID `json:"id,omitempty"`
Status influxdb.Status `json:"status"`
}
type resourceUserResponse struct {
Role influxdb.UserType `json:"role"`
*URMUserResponse
*UserResponse
}
func newResourceUserResponse(u influxdb.User, userType influxdb.UserType) *resourceUserResponse {
func newResourceUserResponse(u *influxdb.User, userType influxdb.UserType) *resourceUserResponse {
return &resourceUserResponse{
Role: userType,
URMUserResponse: &URMUserResponse{
Links: map[string]string{
"self": fmt.Sprintf("/api/v2/users/%s", u.ID),
},
ID: u.ID,
Status: u.Status,
},
Role: userType,
UserResponse: newUserResponse(u),
}
}
@ -258,7 +246,7 @@ type resourceUsersResponse struct {
Users []*resourceUserResponse `json:"users"`
}
func newResourceUsersResponse(f influxdb.UserResourceMappingFilter, users []influxdb.User) *resourceUsersResponse {
func newResourceUsersResponse(f influxdb.UserResourceMappingFilter, users []*influxdb.User) *resourceUsersResponse {
rs := resourceUsersResponse{
Links: map[string]string{
"self": fmt.Sprintf("/api/v2/%s/%s/%ss", f.ResourceType, f.ResourceID, f.UserType),

View File

@ -87,6 +87,7 @@ func TestUserResourceMappingService_GetMembersHandler(t *testing.T) {
"self": "/api/v2/users/0000000000000001"
},
"id": "0000000000000001",
"name": "user0000000000000001",
"status": "active"
},
{
@ -95,6 +96,7 @@ func TestUserResourceMappingService_GetMembersHandler(t *testing.T) {
"self": "/api/v2/users/0000000000000002"
},
"id": "0000000000000002",
"name": "user0000000000000002",
"status": "active"
}
]
@ -148,6 +150,7 @@ func TestUserResourceMappingService_GetMembersHandler(t *testing.T) {
"self": "/api/v2/users/0000000000000001"
},
"id": "0000000000000001",
"name": "user0000000000000001",
"status": "active"
},
{
@ -156,6 +159,7 @@ func TestUserResourceMappingService_GetMembersHandler(t *testing.T) {
"self": "/api/v2/users/0000000000000002"
},
"id": "0000000000000002",
"name": "user0000000000000002",
"status": "active"
}
]
@ -267,6 +271,7 @@ func TestUserResourceMappingService_PostMembersHandler(t *testing.T) {
"self": "/api/v2/users/0000000000000001"
},
"id": "0000000000000001",
"name": "user0000000000000001",
"status": "active"
}`,
},
@ -304,6 +309,7 @@ func TestUserResourceMappingService_PostMembersHandler(t *testing.T) {
"self": "/api/v2/users/0000000000000002"
},
"id": "0000000000000002",
"name": "user0000000000000002",
"status": "active"
}`,
},

View File

@ -0,0 +1,231 @@
describe('Community Templates', () => {
beforeEach(() => {
cy.flush()
cy.signin().then(({body}) => {
const {
org: {id},
} = body
cy.wrap(body.org).as('org')
cy.fixture('routes').then(({orgs}) => {
cy.visit(`${orgs}/${id}/settings/templates`)
})
})
})
it('The browse community template button launches github', () => {
cy.getByTestID('browse-template-button')
.should('have.prop', 'href')
.and(
'equal',
'https://github.com/influxdata/community-templates#templates'
)
})
it('The lookup template errors on invalid data', () => {
//on empty
cy.getByTestID('lookup-template-button').click()
cy.getByTestID('notification-error').should('be.visible')
//lookup template errors on bad url
cy.getByTestID('lookup-template-input').type('www.badURL.com')
cy.getByTestID('lookup-template-button').click()
cy.getByTestID('notification-error').should('be.visible')
//lookup template errors on bad file type
cy.getByTestID('lookup-template-input').clear()
cy.getByTestID('lookup-template-input').type('variables.html')
cy.getByTestID('lookup-template-button').click()
cy.getByTestID('notification-error').should('be.visible')
//lookup template errors on github folder
cy.getByTestID('lookup-template-input').clear()
cy.getByTestID('lookup-template-input').type(
'https://github.com/influxdata/community-templates/tree/master/kafka'
)
cy.getByTestID('lookup-template-button').click()
cy.getByTestID('notification-error').should('be.visible')
})
it.skip('Can install from CLI', () => {
//authorization is preventing this from working
cy.exec(
'go run ../cmd/influx apply -t eiDTSTOZ_WAgLfw9eK5_JUsVnqeIYWWBY2QHXe6KC-UneLThJBGveTMm8k6_W1cAmswzLEKJTPeqoirvHH5kQg== -f pkger/testdata/variables.yml'
).then(result => {
result
})
})
it('Simple Download', () => {
//The lookup template accepts github raw link
cy.getByTestID('lookup-template-input').type(
'https://raw.githubusercontent.com/influxdata/community-templates/master/downsampling/dashboard.yml'
)
cy.getByTestID('lookup-template-button').click()
cy.getByTestID('template-install-overlay').should('be.visible')
//check that with 1 resource pluralization is correct
cy.getByTestID('template-install-title').should('contain', 'resource')
cy.getByTestID('template-install-title').should('not.contain', 'resources')
//check that no resources check lead to disabled install button
cy.getByTestID('heading-Dashboards').click()
cy.getByTestID('templates-toggle--Downsampling Status').should('be.visible')
cy.getByTestID('template-install-button').should('exist')
cy.getByTestID('templates-toggle--Downsampling Status').click()
cy.getByTestID('template-install-button').should('not.exist')
//and check that 0 resources pluralization is correct
cy.getByTestID('template-install-title').should('contain', 'resources')
})
describe('Opening the install overlay', () => {
beforeEach(() => {
//lookup normal github link
cy.getByTestID('lookup-template-input').type(
'https://github.com/influxdata/community-templates/blob/master/docker/docker.yml'
)
cy.getByTestID('lookup-template-button').click()
cy.getByTestID('template-install-overlay').should('be.visible')
})
it('Complicated Download', () => {
//check that with multiple resources pluralization is correct
cy.getByTestID('template-install-title').should('contain', 'resources')
//no uncheck of buckets
cy.getByTestID('template-install-title').should('contain', '22')
cy.getByTestID('heading-Buckets').click()
cy.getByTestID('templates-toggle--docker').should('be.visible')
cy.getByTestID('template-install-title').should('contain', '22')
// cy.getByTestID('templates-toggle--docker').should('be.disabled')
//no uncheck of variables
cy.getByTestID('template-install-title').should('contain', '22')
cy.getByTestID('heading-Variables').click()
cy.getByTestID('templates-toggle--bucket').should('be.visible')
cy.getByTestID('template-install-title').should('contain', '22')
// cy.getByTestID('templates-toggle--bucket').should('be.disabled')
//can check and uncheck other resources
cy.getByTestID('template-install-title').should('contain', '22')
cy.getByTestID('heading-Checks').click()
cy.getByTestID('templates-toggle--Container Disk Usage').should(
'be.visible'
)
cy.getByTestID('templates-toggle--Container Disk Usage').click()
cy.getByTestID('template-install-title').should('contain', '21')
cy.getByTestID('heading-Notification Rules').click()
cy.getByTestID('templates-toggle--Crit Notifier').should('be.visible')
cy.getByTestID('templates-toggle--Crit Notifier').click()
cy.getByTestID('template-install-title').should('contain', '20')
})
it('Can install template', () => {
cy.getByTestID('template-install-button').click()
cy.getByTestID('notification-success').should('be.visible')
cy.getByTestID('installed-template-docker').should('be.visible')
})
})
describe('Install Completed', () => {
beforeEach(() => {
cy.getByTestID('lookup-template-input').type(
'https://github.com/influxdata/community-templates/blob/master/docker/docker.yml'
)
cy.getByTestID('lookup-template-button').click()
cy.getByTestID('template-install-overlay').should('be.visible')
cy.getByTestID('template-install-button').should('exist')
cy.getByTestID('template-install-button').click()
cy.getByTestID('notification-success').should('be.visible')
cy.getByTestID('installed-template-docker').should('be.visible')
})
it('Install Identical template', () => {
cy.getByTestID('lookup-template-input').clear()
cy.getByTestID('lookup-template-input').type(
'https://github.com/influxdata/community-templates/blob/master/docker/docker.yml'
)
cy.getByTestID('lookup-template-button').click()
cy.getByTestID('template-install-overlay').should('be.visible')
cy.getByTestID('template-install-button').should('exist')
cy.getByTestID('template-install-button').click()
cy.getByTestID('notification-success').should('be.visible')
cy.getByTestID('installed-template-list').should('have', '2')
})
it('Can click on template resources', () => {
//buckets
cy.getByTestID('template-resource-link')
.contains('Bucket')
.click()
cy.url().should('include', 'load-data/buckets')
cy.go('back')
//telegraf
cy.getByTestID('template-resource-link')
.contains('Telegraf')
.click()
cy.url().should('include', 'load-data/telegrafs')
cy.go('back')
//check
cy.getByTestID('template-resource-link')
.contains('Check')
.click()
cy.url().should('include', 'alerting/checks')
cy.go('back')
//label
cy.getByTestID('template-resource-link')
.contains('Label')
.click()
cy.url().should('include', 'settings/labels')
cy.go('back')
//Dashboard
cy.getByTestID('template-resource-link')
.contains('Dashboard')
.click()
cy.url().should('include', 'dashboards')
cy.go('back')
//Notification Endpoint
cy.getByTestID('template-resource-link')
.contains('NotificationEndpoint')
.click()
cy.url().should('include', 'alerting')
cy.go('back')
//Notification Rule
cy.getByTestID('template-resource-link')
.contains('NotificationRule')
.click()
cy.url().should('include', 'alerting')
cy.go('back')
//Variable
cy.getByTestID('template-resource-link')
.contains('Variable')
.click()
cy.url().should('include', 'settings/variables')
cy.go('back')
})
it('Click on source takes you to github', () => {
cy.getByTestID('template-source-link').should(
'contain',
'https://github.com/influxdata/community-templates/blob/master/docker/docker.yml'
)
//TODO: add the link from CLI
})
it('Can delete template', () => {
cy.getByTestID('template-delete-button-docker--button').click()
cy.getByTestID('template-delete-button-docker--confirm-button').click()
cy.getByTestID('installed-template-docker').should('not.be.visible')
})
})
})

View File

@ -1,32 +0,0 @@
describe('Templates', () => {
beforeEach(() => {
cy.flush()
cy.signin().then(({body}) => {
cy.wrap(body.org).as('org')
cy.visit(`orgs/${body.org.id}/settings/templates`)
})
})
it('keeps user input in text area when attempting to import invalid JSON', () => {
cy.get('button[title*="Import"]').click()
cy.contains('Paste').click()
cy.getByTestID('import-overlay--textarea')
.click()
.type('this is invalid JSON')
cy.get('button[title*="Import JSON"').click()
cy.getByTestID('import-overlay--textarea--error').should('have.length', 1)
cy.getByTestID('import-overlay--textarea').should($s =>
expect($s).to.contain('this is invalid JSON')
)
cy.getByTestID('import-overlay--textarea').type(
'{backspace}{backspace}{backspace}{backspace}{backspace}'
)
cy.get('button[title*="Import JSON"').click()
cy.getByTestID('import-overlay--textarea--error').should('have.length', 1)
cy.getByTestID('import-overlay--textarea').should($s =>
expect($s).to.contain('this is invalid')
)
})
})

View File

@ -135,7 +135,7 @@
"@influxdata/clockface": "2.3.1",
"@influxdata/flux": "^0.5.1",
"@influxdata/flux-lsp-browser": "^0.5.11",
"@influxdata/giraffe": "0.23.0",
"@influxdata/giraffe": "0.24.0",
"@influxdata/influx": "0.5.5",
"@influxdata/influxdb-templates": "0.9.0",
"@influxdata/react-custom-scrollbars": "4.3.8",

View File

@ -2,7 +2,6 @@
import React, {Component} from 'react'
import {connect, ConnectedProps} from 'react-redux'
import {Switch, Route} from 'react-router-dom'
import uuid from 'uuid'
// Components
import {Page} from '@influxdata/clockface'
@ -24,8 +23,7 @@ import {event} from 'src/cloud/utils/reporting'
import {resetQueryCache} from 'src/shared/apis/queryCache'
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
// Selectors & Actions
import {setRenderID as setRenderIDAction} from 'src/perf/actions'
// Selectors
import {getByID} from 'src/resources/selectors'
// Types
@ -51,34 +49,11 @@ const dashRoute = `/${ORGS}/${ORG_ID}/${DASHBOARDS}/${DASHBOARD_ID}`
@ErrorHandling
class DashboardPage extends Component<Props> {
public componentDidMount() {
const {dashboard, setRenderID} = this.props
const renderID = uuid.v4()
setRenderID('dashboard', renderID)
const tags = {
dashboardID: dashboard.id,
}
const fields = {renderID}
event('Dashboard Mounted', tags, fields)
if (isFlagEnabled('queryCacheForDashboards')) {
resetQueryCache()
}
}
public componentDidUpdate(prevProps) {
const {setRenderID, dashboard, manualRefresh} = this.props
if (prevProps.manualRefresh !== manualRefresh) {
const renderID = uuid.v4()
setRenderID('dashboard', renderID)
const tags = {
dashboardID: dashboard.id,
}
const fields = {renderID}
event('Dashboard Mounted', tags, fields)
}
this.emitRenderCycleEvent()
}
public componentWillUnmount() {
@ -124,6 +99,20 @@ class DashboardPage extends Component<Props> {
return pageTitleSuffixer([title])
}
private emitRenderCycleEvent = () => {
const {dashboard, startVisitMs} = this.props
const tags = {
dashboardID: dashboard.id,
}
const now = new Date().getTime()
const timeToAppearMs = now - startVisitMs
const fields = {timeToAppearMs}
event('Dashboard and Variable Initial Render', tags, fields)
}
}
const mstp = (state: AppState) => {
@ -134,14 +123,11 @@ const mstp = (state: AppState) => {
)
return {
startVisitMs: state.perf.dashboard.byID[dashboard.id]?.startVisitMs,
dashboard,
}
}
const mdtp = {
setRenderID: setRenderIDAction,
}
const connector = connect(mstp, mdtp)
const connector = connect(mstp)
export default connector(ManualRefresh<OwnProps>(DashboardPage))

View File

@ -8,6 +8,7 @@ import FilterList from 'src/shared/components/FilterList'
// Types
import {NotificationEndpoint} from 'src/types'
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
interface Props {
endpoints: NotificationEndpoint[]
@ -51,13 +52,17 @@ const EmptyEndpointList: FC<{searchTerm: string}> = ({searchTerm}) => {
</EmptyState>
)
}
const conditionalEndpoints: Array<string> = []
if (isFlagEnabled('notification-endpoint-telegram')) {
conditionalEndpoints.push('Telegram')
}
return (
<EmptyState size={ComponentSize.Small} className="alert-column--empty">
<EmptyState.Text>
Want to send notifications to Slack,
Want to send notifications to Slack, PagerDuty,
<br />
PagerDuty or an HTTP server?
{conditionalEndpoints.join(', ')}or an HTTP server?
<br />
<br />
Try creating a <b>Notification Endpoint</b>

View File

@ -5,6 +5,7 @@ import React, {FC, ChangeEvent} from 'react'
import EndpointOptionsSlack from './EndpointOptionsSlack'
import EndpointOptionsPagerDuty from './EndpointOptionsPagerDuty'
import EndpointOptionsHTTP from './EndpointOptionsHTTP'
import EndpointOptionsTelegram from './EndpointOptionsTelegram'
// Types
import {
@ -12,6 +13,7 @@ import {
SlackNotificationEndpoint,
PagerDutyNotificationEndpoint,
HTTPNotificationEndpoint,
TelegramNotificationEndpoint,
} from 'src/types'
interface Props {
@ -40,6 +42,16 @@ const EndpointOptions: FC<Props> = ({
/>
)
}
case 'telegram': {
const {token, channel} = endpoint as TelegramNotificationEndpoint
return (
<EndpointOptionsTelegram
token={token}
channel={channel}
onChange={onChange}
/>
)
}
case 'http': {
const {
url,

View File

@ -0,0 +1,56 @@
// Libraries
import React, {FC, ChangeEvent} from 'react'
// Components
import {
Input,
InputType,
FormElement,
Panel,
Grid,
Columns,
} from '@influxdata/clockface'
interface Props {
token: string
channel: string
onChange: (e: ChangeEvent<HTMLInputElement>) => void
}
const EndpointOptionsTelegram: FC<Props> = ({token, channel, onChange}) => {
return (
<Panel>
<Panel.Header>
<h4>Telegram Options</h4>
</Panel.Header>
<Panel.Body>
<Grid>
<Grid.Row>
<Grid.Column widthXS={Columns.Twelve}>
<FormElement label="Bot Token">
<Input
name="token"
value={token}
testID="token"
onChange={onChange}
type={InputType.Password}
/>
</FormElement>
<FormElement label="Chat ID">
<Input
name="channel"
value={channel}
testID="channel"
onChange={onChange}
type={InputType.Text}
/>
</FormElement>
</Grid.Column>
</Grid.Row>
</Grid>
</Panel.Body>
</Panel>
)
}
export default EndpointOptionsTelegram

View File

@ -59,6 +59,13 @@ export const reducer = (
url: DEFAULT_ENDPOINT_URLS.slack,
token: '',
}
case 'telegram':
return {
...baseProps,
type: 'telegram',
token: '',
channel: '',
}
}
}
return state

View File

@ -10,6 +10,7 @@ import {extractBlockedEndpoints} from 'src/cloud/utils/limits'
// Types
import {NotificationEndpointType, AppState} from 'src/types'
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
interface EndpointType {
id: NotificationEndpointType
@ -17,6 +18,13 @@ interface EndpointType {
name: string
}
function isFlaggedOn(type: string) {
if (type === 'telegram') {
return isFlagEnabled('notification-endpoint-telegram')
}
return true
}
interface StateProps {
blockedEndpoints: string[]
}
@ -32,6 +40,7 @@ const types: EndpointType[] = [
{name: 'HTTP', type: 'http', id: 'http'},
{name: 'Slack', type: 'slack', id: 'slack'},
{name: 'Pagerduty', type: 'pagerduty', id: 'pagerduty'},
{name: 'Telegram', type: 'telegram', id: 'telegram'},
]
const EndpointTypeDropdown: FC<Props> = ({
@ -40,7 +49,7 @@ const EndpointTypeDropdown: FC<Props> = ({
blockedEndpoints,
}) => {
const items = types
.filter(({type}) => !blockedEndpoints.includes(type))
.filter(({type}) => !blockedEndpoints.includes(type) && isFlaggedOn(type))
.map(({id, type, name}) => (
<Dropdown.Item
key={id}

View File

@ -11,6 +11,7 @@ import {AppState, NotificationEndpoint, ResourceType} from 'src/types'
// Utils
import {getAll} from 'src/resources/selectors'
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
interface StateProps {
endpoints: NotificationEndpoint[]
@ -27,13 +28,18 @@ const EndpointsColumn: FC<Props> = ({history, match, endpoints, tabIndex}) => {
history.push(newRuleRoute)
}
const conditionalEndpoints: Array<string> = []
if (isFlagEnabled('notification-endpoint-telegram')) {
conditionalEndpoints.push('Telegram')
}
const tooltipContents = (
<>
A <strong>Notification Endpoint</strong> stores the information to connect
<br />
to a third party service that can receive notifications
<br />
like Slack, PagerDuty, or an HTTP server
like Slack, PagerDuty, {conditionalEndpoints.join(', ')}or an HTTP server
<br />
<br />
<a

View File

@ -5,6 +5,7 @@ import React, {FC} from 'react'
import SlackMessage from './SlackMessage'
import SMTPMessage from './SMTPMessage'
import PagerDutyMessage from './PagerDutyMessage'
import TelegramMessage from './TelegramMessage'
// Utils
import {useRuleDispatch} from './RuleOverlayProvider'
@ -61,6 +62,15 @@ const RuleMessageContents: FC<Props> = ({rule}) => {
/>
)
}
case 'telegram': {
const {messageTemplate} = rule
return (
<TelegramMessage
messageTemplate={messageTemplate}
onChange={onChange}
/>
)
}
case 'http': {
return <></>
}

View File

@ -0,0 +1,41 @@
// Libraries
import React, {FC, ChangeEvent} from 'react'
// Components
import {Form, TextArea} from '@influxdata/clockface'
import {TelegramNotificationRuleBase} from 'src/types/alerting'
interface EventHandlers {
onChange: (e: ChangeEvent) => void
}
type Props = Omit<TelegramNotificationRuleBase, 'type'> & EventHandlers
const TelegramMessage: FC<Props> = ({messageTemplate, onChange}) => {
return (
<Form.Element label="Message Template">
<TextArea
name="messageTemplate"
testID="slack-message-template--textarea"
value={messageTemplate}
onChange={onChange}
rows={3}
/>
</Form.Element>
/*
// keep it simple, the following elements are possible, but too advanced
<Form.Element label="Parse Mode">
<Input value={parseMode} name="parseMode" onChange={onChange} />
</Form.Element>
<Form.Element label="">
<Input
value={String(!disableWebPagePreview)}
name="disableWebPagePreview"
onChange={onChange}
type={InputType.Checkbox}
/>
</Form.Element>
*/
)
}
export default TelegramMessage

View File

@ -8,6 +8,7 @@ import {
SlackNotificationRuleBase,
SMTPNotificationRuleBase,
PagerDutyNotificationRuleBase,
TelegramNotificationRuleBase,
NotificationEndpoint,
NotificationRuleDraft,
HTTPNotificationRuleBase,
@ -22,6 +23,7 @@ type RuleVariantFields =
| SMTPNotificationRuleBase
| PagerDutyNotificationRuleBase
| HTTPNotificationRuleBase
| TelegramNotificationRuleBase
const defaultMessage =
'Notification Rule: ${ r._notification_rule_name } triggered by check: ${ r._check_name }: ${ r._message }'
@ -45,6 +47,20 @@ export const getRuleVariantDefaults = (
return {type: 'http', url: ''}
}
case 'telegram': {
// wrap all variable values into `` to prevent telegram's markdown errors
const messageTemplate = defaultMessage.replace(
/\$\{[^\}]+\}/g,
x => `\`${x}\``
)
return {
messageTemplate: messageTemplate,
parseMode: 'MarkdownV2',
disableWebPagePreview: false,
type: 'telegram',
}
}
default: {
throw new Error(`Could not find NotificationEndpoint with id "${id}"`)
}

View File

@ -1,22 +1,15 @@
export const SET_RENDER_ID = 'SET_RENDER_ID'
export const SET_SCROLL = 'SET_SCROLL'
export const SET_CELL_MOUNT = 'SET_CELL_MOUNT'
export const SET_DASHBOARD_VISIT = 'SET_DASHBOARD_VISIT'
export type Action =
| ReturnType<typeof setRenderID>
| ReturnType<typeof setScroll>
| ReturnType<typeof setCellMount>
| ReturnType<typeof setDashboardVisit>
export type ComponentKey = 'dashboard'
export type ScrollState = 'not scrolled' | 'scrolled'
export const setRenderID = (component: ComponentKey, renderID: string) =>
({
type: SET_RENDER_ID,
component,
renderID,
} as const)
export const setScroll = (component: ComponentKey, scroll: ScrollState) =>
({
type: SET_SCROLL,
@ -30,3 +23,10 @@ export const setCellMount = (cellID: string, mountStartMs: number) =>
cellID,
mountStartMs,
} as const)
export const setDashboardVisit = (dashboardID: string, startVisitMs: number) =>
({
type: SET_DASHBOARD_VISIT,
dashboardID,
startVisitMs,
} as const)

View File

@ -16,11 +16,10 @@ interface Props {
const getState = (cellID: string) => (state: AppState) => {
const {perf} = state
const {dashboard, cells} = perf
const {scroll, renderID} = dashboard
const {scroll} = dashboard
return {
scroll,
renderID,
cellMountStartMs: cells.byID[cellID]?.mountStartMs,
}
}
@ -29,31 +28,14 @@ const CellEvent: FC<Props> = ({id, type}) => {
const params = useParams<{dashboardID?: string}>()
const dashboardID = params?.dashboardID
const {renderID, scroll, cellMountStartMs} = useSelector(getState(id))
useEffect(() => {
if (scroll === 'scrolled') {
return
}
const hasIDs = dashboardID && id && renderID
if (!hasIDs) {
return
}
const tags = {dashboardID, cellID: id, type}
const fields = {renderID}
event('Cell Visualized', tags, fields)
}, [dashboardID, id, renderID, type, scroll])
const {cellMountStartMs} = useSelector(getState(id))
useEffect(() => {
if (!cellMountStartMs) {
return
}
const hasIDs = dashboardID && id && renderID
const hasIDs = dashboardID && id
if (!hasIDs) {
return
}
@ -62,10 +44,10 @@ const CellEvent: FC<Props> = ({id, type}) => {
const timeToAppearMs = visRenderedMs - cellMountStartMs
const tags = {dashboardID, cellID: id, type}
const fields = {timeToAppearMs, renderID}
const fields = {timeToAppearMs}
event('Cell Render Cycle', tags, fields)
}, [cellMountStartMs, dashboardID, id, renderID, type])
}, [cellMountStartMs, dashboardID, id, type])
return null
}

View File

@ -3,7 +3,7 @@ import {produce} from 'immer'
// Actions
import {
SET_RENDER_ID,
SET_DASHBOARD_VISIT,
SET_SCROLL,
Action,
SET_CELL_MOUNT,
@ -12,7 +12,11 @@ import {
export interface PerfState {
dashboard: {
scroll: 'not scrolled' | 'scrolled'
renderID: string
byID: {
[id: string]: {
startVisitMs: number
}
}
}
cells: {
byID: {
@ -26,7 +30,7 @@ export interface PerfState {
const initialState = (): PerfState => ({
dashboard: {
scroll: 'not scrolled',
renderID: '',
byID: {},
},
cells: {
byID: {},
@ -39,16 +43,24 @@ const perfReducer = (
): PerfState =>
produce(state, draftState => {
switch (action.type) {
case SET_RENDER_ID: {
const {component, renderID} = action
draftState[component].renderID = renderID
case SET_SCROLL: {
const {component, scroll} = action
draftState[component].scroll = scroll
return
}
case SET_SCROLL: {
const {component, scroll} = action
draftState[component].scroll = scroll
case SET_DASHBOARD_VISIT: {
const {dashboardID, startVisitMs} = action
const exists = draftState.dashboard.byID[dashboardID]
if (!exists) {
draftState.dashboard.byID[dashboardID] = {startVisitMs}
return
}
draftState.dashboard.byID[dashboardID].startVisitMs = startVisitMs
return
}

View File

@ -1,10 +1,21 @@
// Libraries
import React, {PureComponent} from 'react'
import qs from 'qs'
import {connect, ConnectedProps} from 'react-redux'
import {withRouter, RouteComponentProps} from 'react-router-dom'
// Actions
import {setDashboard} from 'src/shared/actions/currentDashboard'
import {getVariables} from 'src/variables/selectors'
import {selectValue} from 'src/variables/actions/thunks'
import {setDashboardVisit} from 'src/perf/actions'
// Utils
import {event} from 'src/cloud/utils/reporting'
// Selector
import {getVariables} from 'src/variables/selectors'
// Types
import {AppState} from 'src/types'
type ReduxProps = ConnectedProps<typeof connector>
@ -44,8 +55,10 @@ class DashboardRoute extends PureComponent<Props> {
}
componentDidMount() {
const {dashboard, updateDashboard, variables} = this.props
const {dashboard, updateDashboard, variables, dashboardVisit} = this.props
const dashboardID = this.props.match.params.dashboardID
dashboardVisit(dashboardID, new Date().getTime())
event('Dashboard Visit', {dashboardID})
const urlVars = qs.parse(this.props.location.search, {
ignoreQueryPrefix: true,
})
@ -113,6 +126,7 @@ const mstp = (state: AppState) => {
}
const mdtp = {
dashboardVisit: setDashboardVisit,
updateDashboard: setDashboard,
selectValue: selectValue,
}

View File

@ -16,6 +16,7 @@ export const OSS_FLAGS = {
'notebook-panel--spotify': false,
'notebook-panel--test-flux': false,
disableDefaultTableSort: false,
'notification-endpoint-telegram': false,
}
export const CLOUD_FLAGS = {
@ -33,6 +34,7 @@ export const CLOUD_FLAGS = {
'notebook-panel--spotify': false,
'notebook-panel--test-flux': false,
disableDefaultTableSort: false,
'notification-endpoint-telegram': false,
}
export const activeFlags = (state: AppState): FlagMap => {

View File

@ -31,7 +31,7 @@ export type Action =
| ReturnType<typeof setExportTemplate>
| ReturnType<typeof setTemplatesStatus>
| ReturnType<typeof setTemplateSummary>
| ReturnType<typeof setCommunityTemplateToInstall>
| ReturnType<typeof setStagedCommunityTemplate>
| ReturnType<typeof toggleTemplateResourceInstall>
| ReturnType<typeof setStacks>
| ReturnType<typeof removeStack>
@ -91,7 +91,7 @@ export const setTemplateSummary = (
schema,
} as const)
export const setCommunityTemplateToInstall = (template: CommunityTemplate) =>
export const setStagedCommunityTemplate = (template: CommunityTemplate) =>
({
type: SET_COMMUNITY_TEMPLATE_TO_INSTALL,
template,

View File

@ -3,10 +3,10 @@ import {withRouter, RouteComponentProps} from 'react-router-dom'
import {connect, ConnectedProps} from 'react-redux'
// Components
import {CommunityTemplateInstallerOverlay} from 'src/templates/components/CommunityTemplateInstallerOverlay'
import {CommunityTemplateOverlay} from 'src/templates/components/CommunityTemplateOverlay'
// Actions
import {setCommunityTemplateToInstall} from 'src/templates/actions/creators'
import {setStagedCommunityTemplate} from 'src/templates/actions/creators'
import {createTemplate, fetchAndSetStacks} from 'src/templates/actions/thunks'
import {notify} from 'src/shared/actions/notifications'
@ -62,12 +62,8 @@ class UnconnectedTemplateImportOverlay extends PureComponent<Props> {
}
public render() {
if (!this.props.flags.communityTemplates) {
return null
}
return (
<CommunityTemplateInstallerOverlay
<CommunityTemplateOverlay
onDismissOverlay={this.onDismiss}
onInstall={this.handleInstallTemplate}
resourceCount={this.props.resourceCount}
@ -93,7 +89,7 @@ class UnconnectedTemplateImportOverlay extends PureComponent<Props> {
try {
const summary = await reviewTemplate(orgID, yamlLocation)
this.props.setCommunityTemplateToInstall(summary)
this.props.setStagedCommunityTemplate(summary)
return summary
} catch (err) {
this.props.notify(communityTemplateInstallFailed(err.message))
@ -161,17 +157,17 @@ const mstp = (state: AppState, props: RouterProps) => {
templateExtension: props.match.params.templateExtension,
flags: state.flags.original,
resourceCount: getTotalResourceCount(
state.resources.templates.communityTemplateToInstall.summary
state.resources.templates.stagedCommunityTemplate.summary
),
resourcesToSkip:
state.resources.templates.communityTemplateToInstall.resourcesToSkip,
state.resources.templates.stagedCommunityTemplate.resourcesToSkip,
}
}
const mdtp = {
createTemplate,
notify,
setCommunityTemplateToInstall,
setStagedCommunityTemplate,
fetchAndSetStacks,
}

View File

@ -15,7 +15,8 @@ import {
AlignItems,
InfluxColors,
} from '@influxdata/clockface'
import CommunityTemplateNameIcon from 'src/templates/components/CommunityTemplateNameIcon'
import {CommunityTemplateInstallInstructionsIcon} from 'src/templates/components/CommunityTemplateInstallInstructionsIcon'
interface Props {
templateName: string
@ -23,9 +24,7 @@ interface Props {
onClickInstall?: () => void
}
import {} from 'react'
const CommunityTemplateName: FC<Props> = ({
export const CommunityTemplateInstallInstructions: FC<Props> = ({
templateName,
resourceCount,
onClickInstall,
@ -44,6 +43,7 @@ const CommunityTemplateName: FC<Props> = ({
color={ComponentColor.Success}
size={ComponentSize.Medium}
onClick={onClickInstall}
testID="template-install-button"
/>
)
}
@ -55,13 +55,13 @@ const CommunityTemplateName: FC<Props> = ({
direction={FlexDirection.Row}
alignItems={AlignItems.Center}
>
<CommunityTemplateNameIcon
<CommunityTemplateInstallInstructionsIcon
strokeWidth={2}
strokeColor={InfluxColors.Neutrino}
width={54}
height={54}
/>
<FlexBox.Child grow={1} shrink={0}>
<FlexBox.Child grow={1} shrink={0} testID="template-install-title">
<Heading
className="community-templates--template-name"
element={HeadingElement.H4}
@ -86,5 +86,3 @@ const CommunityTemplateName: FC<Props> = ({
</Panel>
)
}
export default CommunityTemplateName

View File

@ -8,7 +8,7 @@ interface Props {
height: number
}
const CommunityTemplateNameIcon: FC<Props> = ({
export const CommunityTemplateInstallInstructionsIcon: FC<Props> = ({
strokeColor,
fillColor = 'none',
strokeWidth = 2,
@ -73,5 +73,3 @@ const CommunityTemplateNameIcon: FC<Props> = ({
</svg>
)
}
export default CommunityTemplateNameIcon

View File

@ -47,7 +47,9 @@ const CommunityTemplateListGroup: FC<Props> = ({title, count, children}) => {
<div className="community-templates--list-toggle">
<Icon glyph={IconFont.CaretRight} />
</div>
<Heading element={HeadingElement.H5}>{title}</Heading>
<Heading element={HeadingElement.H5} testID={`heading-${title}`}>
{title}
</Heading>
<Heading
element={HeadingElement.Div}
appearance={HeadingElement.H6}

View File

@ -58,6 +58,7 @@ const CommunityTemplateListItem: FC<Props> = ({
icon={IconFont.Checkmark}
color={ComponentColor.Success}
disabled={shouldDisableToggle}
testID={`templates-toggle--${title}`}
/>
<FlexBox
alignItems={AlignItems.FlexStart}

View File

@ -4,9 +4,9 @@ import {withRouter, RouteComponentProps} from 'react-router-dom'
// Components
import {Alignment, Orientation, Overlay, Tabs} from '@influxdata/clockface'
import CommunityTemplateName from 'src/templates/components/CommunityTemplateName'
import {CommunityTemplateInstallInstructions} from 'src/templates/components/CommunityTemplateInstallInstructions'
import {CommunityTemplateReadme} from 'src/templates/components/CommunityTemplateReadme'
import {CommunityTemplateContents} from 'src/templates/components/CommunityTemplateContents'
import {CommunityTemplateOverlayContents} from 'src/templates/components/CommunityTemplateOverlayContents'
// Types
import {ComponentStatus} from '@influxdata/clockface'
@ -34,10 +34,7 @@ type ActiveTab = Tab.IncludedResources | Tab.Readme
type Props = OwnProps & RouteComponentProps<{orgID: string}>
class CommunityTemplateInstallerOverlayUnconnected extends PureComponent<
Props,
State
> {
class CommunityTemplateOverlayUnconnected extends PureComponent<Props, State> {
state: State = {
activeTab: Tab.IncludedResources,
}
@ -51,13 +48,13 @@ class CommunityTemplateInstallerOverlayUnconnected extends PureComponent<
return (
<Overlay visible={isVisible}>
<Overlay.Container maxWidth={800}>
<Overlay.Container maxWidth={800} testID="template-install-overlay">
<Overlay.Header
title="Template Installer"
onDismiss={this.onDismiss}
/>
<Overlay.Body>
<CommunityTemplateName
<CommunityTemplateInstallInstructions
templateName={templateName}
resourceCount={resourceCount}
onClickInstall={onInstall}
@ -78,7 +75,7 @@ class CommunityTemplateInstallerOverlayUnconnected extends PureComponent<
/>
</Tabs.Tabs>
{this.state.activeTab === Tab.IncludedResources ? (
<CommunityTemplateContents />
<CommunityTemplateOverlayContents />
) : (
<CommunityTemplateReadme />
)}
@ -102,6 +99,6 @@ class CommunityTemplateInstallerOverlayUnconnected extends PureComponent<
}
}
export const CommunityTemplateInstallerOverlay = withRouter(
CommunityTemplateInstallerOverlayUnconnected
export const CommunityTemplateOverlay = withRouter(
CommunityTemplateOverlayUnconnected
)

View File

@ -23,7 +23,7 @@ import {getResourceInstallCount} from 'src/templates/selectors'
type ReduxProps = ConnectedProps<typeof connector>
type Props = ReduxProps
class CommunityTemplateContentsUnconnected extends PureComponent<Props> {
class CommunityTemplateOverlayContentsUnconnected extends PureComponent<Props> {
render() {
const {summary} = this.props
if (!Object.keys(summary).length) {
@ -219,7 +219,7 @@ class CommunityTemplateContentsUnconnected extends PureComponent<Props> {
}
const mstp = (state: AppState) => {
return {summary: state.resources.templates.communityTemplateToInstall.summary}
return {summary: state.resources.templates.stagedCommunityTemplate.summary}
}
const mdtp = {
@ -228,6 +228,6 @@ const mdtp = {
const connector = connect(mstp, mdtp)
export const CommunityTemplateContents = connector(
CommunityTemplateContentsUnconnected
export const CommunityTemplateOverlayContents = connector(
CommunityTemplateOverlayContentsUnconnected
)

View File

@ -238,20 +238,26 @@ class CommunityTemplatesInstalledListUnconnected extends PureComponent<Props> {
<Table.Body>
{this.props.stacks.map(stack => {
return (
<Table.Row key={`stack-${stack.id}`}>
<Table.Cell>{stack.name}</Table.Cell>
<Table.Cell>
<Table.Row
testID="installed-template-list"
key={`stack-${stack.id}`}
>
<Table.Cell testID={`installed-template-${stack.name}`}>
{stack.name}
</Table.Cell>
<Table.Cell testID="template-resource-link">
{this.renderStackResources(stack.resources)}
</Table.Cell>
<Table.Cell>
{new Date(stack.createdAt).toDateString()}
</Table.Cell>
<Table.Cell>
<Table.Cell testID="template-source-link">
{this.renderStackSources(stack.sources)}
</Table.Cell>
<Table.Cell>
<ConfirmationButton
confirmationButtonText="Delete"
testID={`template-delete-button-${stack.name}`}
confirmationButtonColor={ComponentColor.Danger}
confirmationLabel="Really Delete All Resources?"
popoverColor={ComponentColor.Default}

View File

@ -110,6 +110,7 @@ class UnconnectedTemplatesIndex extends Component<Props> {
size={ComponentSize.Small}
target={LinkTarget.Blank}
text="Browse Community Templates"
testID="browse-template-button"
/>
</Panel.SymbolHeader>
</Panel>
@ -131,11 +132,13 @@ class UnconnectedTemplatesIndex extends Component<Props> {
placeholder="Enter the URL of an InfluxDB Template..."
style={{width: '80%'}}
value={this.state.templateUrl}
testID="lookup-template-input"
/>
<Button
onClick={this.startTemplateInstall}
size={ComponentSize.Small}
text="Lookup Template"
testID="lookup-template-button"
/>
</div>
</Panel.Body>

View File

@ -12,7 +12,6 @@ import TemplatesPage from 'src/templates/components/TemplatesPage'
import GetResources from 'src/resources/components/GetResources'
import TemplateImportOverlay from 'src/templates/components/TemplateImportOverlay'
import TemplateExportOverlay from 'src/templates/components/TemplateExportOverlay'
import {CommunityTemplateImportOverlay} from 'src/templates/components/CommunityTemplateImportOverlay'
import TemplateViewOverlay from 'src/templates/components/TemplateViewOverlay'
import StaticTemplateViewOverlay from 'src/templates/components/StaticTemplateViewOverlay'
@ -54,10 +53,6 @@ class TemplatesIndex extends Component<Props> {
path={`${templatesPath}/import`}
component={TemplateImportOverlay}
/>
<Route
path={`${templatesPath}/import/:templateName`}
component={CommunityTemplateImportOverlay}
/>
<Route
path={`${templatesPath}/:id/export`}
component={TemplateExportOverlay}

View File

@ -42,10 +42,10 @@ const templateSummary = {
const exportTemplate = {status, item: null}
const communityTemplateToInstall: CommunityTemplate = {}
const stagedCommunityTemplate: CommunityTemplate = {}
const initialState = () => ({
communityTemplateToInstall,
stagedCommunityTemplate,
status,
byID: {
['1']: templateSummary,
@ -98,7 +98,7 @@ describe('templates reducer', () => {
byID,
allIDs,
exportTemplate,
communityTemplateToInstall,
stagedCommunityTemplate,
stacks: [],
}
const actual = reducer(state, removeTemplateSummary(state.allIDs[1]))

View File

@ -36,7 +36,7 @@ const defaultCommunityTemplate = (): CommunityTemplate => {
}
export const defaultState = (): TemplatesState => ({
communityTemplateToInstall: defaultCommunityTemplate(),
stagedCommunityTemplate: defaultCommunityTemplate(),
status: RemoteDataState.NotStarted,
byID: {},
allIDs: [],
@ -78,12 +78,12 @@ export const templatesReducer = (
case SET_COMMUNITY_TEMPLATE_TO_INSTALL: {
const {template} = action
const communityTemplateToInstall = {
const stagedCommunityTemplate = {
...defaultCommunityTemplate(),
...template,
}
communityTemplateToInstall.summary.dashboards = (
stagedCommunityTemplate.summary.dashboards = (
template.summary.dashboards || []
).map(dashboard => {
if (!dashboard.hasOwnProperty('shouldInstall')) {
@ -92,7 +92,7 @@ export const templatesReducer = (
return dashboard
})
communityTemplateToInstall.summary.telegrafConfigs = (
stagedCommunityTemplate.summary.telegrafConfigs = (
template.summary.telegrafConfigs || []
).map(telegrafConfig => {
if (!telegrafConfig.hasOwnProperty('shouldInstall')) {
@ -101,7 +101,7 @@ export const templatesReducer = (
return telegrafConfig
})
communityTemplateToInstall.summary.buckets = (
stagedCommunityTemplate.summary.buckets = (
template.summary.buckets || []
).map(bucket => {
if (!bucket.hasOwnProperty('shouldInstall')) {
@ -110,7 +110,7 @@ export const templatesReducer = (
return bucket
})
communityTemplateToInstall.summary.checks = (
stagedCommunityTemplate.summary.checks = (
template.summary.checks || []
).map(check => {
if (!check.hasOwnProperty('shouldInstall')) {
@ -119,7 +119,7 @@ export const templatesReducer = (
return check
})
communityTemplateToInstall.summary.variables = (
stagedCommunityTemplate.summary.variables = (
template.summary.variables || []
).map(variable => {
if (!variable.hasOwnProperty('shouldInstall')) {
@ -128,7 +128,7 @@ export const templatesReducer = (
return variable
})
communityTemplateToInstall.summary.notificationRules = (
stagedCommunityTemplate.summary.notificationRules = (
template.summary.notificationRules || []
).map(notificationRule => {
if (!notificationRule.hasOwnProperty('shouldInstall')) {
@ -137,7 +137,7 @@ export const templatesReducer = (
return notificationRule
})
communityTemplateToInstall.summary.notificationEndpoints = (
stagedCommunityTemplate.summary.notificationEndpoints = (
template.summary.notificationEndpoints || []
).map(notificationEndpoint => {
if (!notificationEndpoint.hasOwnProperty('shouldInstall')) {
@ -146,7 +146,7 @@ export const templatesReducer = (
return notificationEndpoint
})
communityTemplateToInstall.summary.labels = (
stagedCommunityTemplate.summary.labels = (
template.summary.labels || []
).map(label => {
if (!label.hasOwnProperty('shouldInstall')) {
@ -155,7 +155,7 @@ export const templatesReducer = (
return label
})
communityTemplateToInstall.summary.summaryTask = (
stagedCommunityTemplate.summary.summaryTask = (
template.summary.summaryTask || []
).map(summaryTask => {
if (!summaryTask.hasOwnProperty('shouldInstall')) {
@ -164,7 +164,7 @@ export const templatesReducer = (
return summaryTask
})
draftState.communityTemplateToInstall = communityTemplateToInstall
draftState.stagedCommunityTemplate = stagedCommunityTemplate
return
}
@ -195,7 +195,7 @@ export const templatesReducer = (
case TOGGLE_TEMPLATE_RESOURCE_INSTALL: {
const {resourceType, shouldInstall, templateMetaName} = action
const templateToInstall = {...draftState.communityTemplateToInstall}
const templateToInstall = {...draftState.stagedCommunityTemplate}
templateToInstall.summary[resourceType].forEach(resource => {
if (resource.templateMetaName === templateMetaName) {
@ -214,7 +214,7 @@ export const templatesReducer = (
}
})
draftState.communityTemplateToInstall = templateToInstall
draftState.stagedCommunityTemplate = templateToInstall
return
}

View File

@ -19,6 +19,8 @@ import {
Threshold,
CheckBase as GenCheckBase,
NotificationEndpointBase as GenEndpointBase,
TelegramNotificationRuleBase,
TelegramNotificationEndpoint,
} from 'src/client'
import {RemoteDataState} from 'src/types'
@ -50,6 +52,8 @@ export type NotificationEndpoint =
| (Omit<PagerDutyNotificationEndpoint, 'status' | 'labels'> &
EndpointOverrides)
| (Omit<HTTPNotificationEndpoint, 'status' | 'labels'> & EndpointOverrides)
| (Omit<TelegramNotificationEndpoint, 'status' | 'labels'> &
EndpointOverrides)
export type NotificationEndpointBase = Omit<GenEndpointBase, 'labels'> &
EndpointOverrides
@ -76,7 +80,7 @@ export type NotificationRuleBaseDraft = Overwrite<
}
>
type RuleDraft = SlackRule | SMTPRule | PagerDutyRule | HTTPRule
type RuleDraft = SlackRule | SMTPRule | PagerDutyRule | HTTPRule | TelegramRule
export type NotificationRuleDraft = RuleDraft
@ -96,6 +100,10 @@ type HTTPRule = NotificationRuleBaseDraft &
HTTPNotificationRuleBase &
RuleOverrides
type TelegramRule = NotificationRuleBaseDraft &
TelegramNotificationRuleBase &
RuleOverrides
export type LowercaseCheckStatusLevel =
| 'crit'
| 'warn'
@ -189,4 +197,7 @@ export {
PostNotificationRule,
CheckPatch,
TaskStatusType,
TelegramNotificationEndpoint,
TelegramNotificationRuleBase,
TelegramNotificationRule,
} from '../client'

View File

@ -35,7 +35,7 @@ export type CommunityTemplate = any
export interface TemplatesState extends NormalizedState<TemplateSummary> {
exportTemplate: {status: RemoteDataState; item: DocumentCreate}
communityTemplateToInstall: CommunityTemplate
stagedCommunityTemplate: CommunityTemplate
stacks: InstalledStack[]
}

4
yarn.lock Normal file
View File

@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1