From c6dffb11a4f23ab6c8ac349bde791f1638b582c9 Mon Sep 17 00:00:00 2001 From: Jimmie Han Date: Wed, 28 Sep 2022 21:49:12 +0800 Subject: [PATCH 01/62] Tempo: Fix unexpected trimming of leading zeroes in traceID (#55167) * tempo: fix unexpected trim traceID leading zero (#55166) * fix lint --- pkg/tsdb/tempo/trace_transform.go | 2 +- pkg/tsdb/tempo/trace_transform_test.go | 38 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/tempo/trace_transform.go b/pkg/tsdb/tempo/trace_transform.go index 1b390833d6f..46ba1b2c077 100644 --- a/pkg/tsdb/tempo/trace_transform.go +++ b/pkg/tsdb/tempo/trace_transform.go @@ -111,7 +111,7 @@ func resourceSpansToRows(rs pdata.ResourceSpans) ([][]interface{}, error) { func spanToSpanRow(span pdata.Span, libraryTags pdata.InstrumentationLibrary, resource pdata.Resource) ([]interface{}, error) { // If the id representation changed from hexstring to something else we need to change the transformBase64IDToHexString in the frontend code traceID := span.TraceID().HexString() - traceID = strings.TrimLeft(traceID, "0") + traceID = strings.TrimPrefix(traceID, strings.Repeat("0", 16)) spanID := span.SpanID().HexString() diff --git a/pkg/tsdb/tempo/trace_transform_test.go b/pkg/tsdb/tempo/trace_transform_test.go index ae153162a39..5f378c6bce6 100644 --- a/pkg/tsdb/tempo/trace_transform_test.go +++ b/pkg/tsdb/tempo/trace_transform_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/require" otlp "go.opentelemetry.io/collector/model/otlp" + "go.opentelemetry.io/collector/model/pdata" ) func TestTraceToFrame(t *testing.T) { @@ -51,6 +52,43 @@ func TestTraceToFrame(t *testing.T) { require.Equal(t, json.RawMessage("[{\"timestamp\":1616072924072.856,\"fields\":[{\"value\":1,\"key\":\"chunks requested\"}]},{\"timestamp\":1616072924072.9448,\"fields\":[{\"value\":1,\"key\":\"chunks fetched\"}]}]"), span["logs"]) require.Equal(t, json.RawMessage("[{\"value\":0,\"key\":\"status.code\"}]"), span["tags"]) }) + + t.Run("should transform correct traceID", func(t *testing.T) { + proto, err := os.ReadFile("testData/tempo_proto_response") + require.NoError(t, err) + + otTrace, err := otlp.NewProtobufTracesUnmarshaler().UnmarshalTraces(proto) + require.NoError(t, err) + + var index int + otTrace.ResourceSpans().RemoveIf(func(rsp pdata.ResourceSpans) bool { + rsp.InstrumentationLibrarySpans().RemoveIf(func(sp pdata.InstrumentationLibrarySpans) bool { + sp.Spans().RemoveIf(func(span pdata.Span) bool { + if index == 0 { + span.SetTraceID(pdata.NewTraceID([16]byte{0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7})) + } + if index == 1 { + span.SetTraceID(pdata.NewTraceID([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7})) + } + index++ + return false + }) + return false + }) + return false + }) + frame, err := TraceToFrame(otTrace) + require.NoError(t, err) + bFrame := &BetterFrame{frame} + + traceID128Bit := bFrame.GetRow(0) + require.NotNil(t, traceID128Bit) + require.Equal(t, "00010203040506070001020304050607", traceID128Bit["traceID"]) + + traceID64Bit := bFrame.GetRow(1) + require.NotNil(t, traceID64Bit) + require.Equal(t, "0001020304050607", traceID64Bit["traceID"]) + }) } type Row map[string]interface{} From b0b1e8d568418b15fe734df13c5265644b95bd63 Mon Sep 17 00:00:00 2001 From: Emil Tullstedt Date: Wed, 28 Sep 2022 15:53:48 +0200 Subject: [PATCH 02/62] Tracing: Document Tracer and Span interfaces (#55926) --- pkg/infra/tracing/opentelemetry_tracing.go | 20 ++----- pkg/infra/tracing/tracing.go | 61 ++++++++++++++++++++-- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/pkg/infra/tracing/opentelemetry_tracing.go b/pkg/infra/tracing/opentelemetry_tracing.go index b51dddac107..2ce2e6ba5f0 100644 --- a/pkg/infra/tracing/opentelemetry_tracing.go +++ b/pkg/infra/tracing/opentelemetry_tracing.go @@ -8,8 +8,6 @@ import ( "time" "github.com/go-kit/log/level" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/setting" "go.etcd.io/etcd/api/v3/version" jaegerpropagator "go.opentelemetry.io/contrib/propagators/jaeger" "go.opentelemetry.io/otel" @@ -23,6 +21,9 @@ import ( tracesdk "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" trace "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/setting" ) const ( @@ -34,21 +35,6 @@ const ( w3cPropagator string = "w3c" ) -type Tracer interface { - Run(context.Context) error - Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, Span) - Inject(context.Context, http.Header, Span) -} - -type Span interface { - End() - SetAttributes(key string, value interface{}, kv attribute.KeyValue) - SetName(name string) - SetStatus(code codes.Code, description string) - RecordError(err error, options ...trace.EventOption) - AddEvents(keys []string, values []EventValue) -} - type Opentelemetry struct { enabled string address string diff --git a/pkg/infra/tracing/tracing.go b/pkg/infra/tracing/tracing.go index e125f0e501b..82c53eff6a1 100644 --- a/pkg/infra/tracing/tracing.go +++ b/pkg/infra/tracing/tracing.go @@ -8,9 +8,6 @@ import ( "os" "strings" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/setting" opentracing "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" ol "github.com/opentracing/opentracing-go/log" @@ -20,6 +17,10 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" trace "go.opentelemetry.io/otel/trace" + + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/setting" ) const ( @@ -27,6 +28,56 @@ const ( envJaegerAgentPort = "JAEGER_AGENT_PORT" ) +// Tracer defines the service used to create new spans. +type Tracer interface { + // Run implements registry.BackgroundService. + Run(context.Context) error + // Start creates a new [Span] and places trace metadata on the + // [context.Context] passed to the method. + // Chose a low cardinality spanName and use [Span.SetAttributes] + // or [Span.AddEvents] for high cardinality data. + Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, Span) + // Inject adds identifying information for the span to the + // headers defined in [http.Header] map (this mutates http.Header). + // + // Implementation quirk: Where OpenTelemetry is used, the [Span] is + // picked up from [context.Context] and for OpenTracing the + // information passed as [Span] is preferred. + // Both the context and span must be derived from the same call to + // [Tracer.Start]. + Inject(context.Context, http.Header, Span) +} + +// Span defines a time range for an operation. This is equivalent to a +// single line in a flame graph. +type Span interface { + // End finalizes the Span and adds its end timestamp. + // Any further operations on the Span are not permitted after + // End has been called. + End() + // SetAttributes adds additional data to a span. + // SetAttributes repeats the key value pair with [string] and [any] + // used for OpenTracing and [attribute.KeyValue] used for + // OpenTelemetry. + SetAttributes(key string, value interface{}, kv attribute.KeyValue) + // SetName renames the span. + SetName(name string) + // SetStatus can be used to indicate whether the span was + // successfully or unsuccessfully executed. + // + // Only useful for OpenTelemetry. + SetStatus(code codes.Code, description string) + // RecordError adds an error to the span. + // + // Only useful for OpenTelemetry. + RecordError(err error, options ...trace.EventOption) + // AddEvents adds additional data with a temporal dimension to the + // span. + // + // Panics if the length of keys is shorter than the length of values. + AddEvents(keys []string, values []EventValue) +} + func ProvideService(cfg *setting.Cfg) (Tracer, error) { ts, ots, err := parseSettings(cfg) if err != nil { @@ -239,7 +290,9 @@ func (s OpentracingSpan) SetName(name string) { } func (s OpentracingSpan) SetStatus(code codes.Code, description string) { - ext.Error.Set(s.span, true) + if code == codes.Error { + ext.Error.Set(s.span, true) + } } func (s OpentracingSpan) RecordError(err error, options ...trace.EventOption) { From 6aef353b59836941456cc25573858d9081f94f38 Mon Sep 17 00:00:00 2001 From: Emil Tullstedt Date: Wed, 28 Sep 2022 15:54:47 +0200 Subject: [PATCH 03/62] Tracing: Fix bug where errors are not reported to OTel (#55925) --- pkg/infra/tracing/opentelemetry_tracing.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/infra/tracing/opentelemetry_tracing.go b/pkg/infra/tracing/opentelemetry_tracing.go index 2ce2e6ba5f0..1774e33458f 100644 --- a/pkg/infra/tracing/opentelemetry_tracing.go +++ b/pkg/infra/tracing/opentelemetry_tracing.go @@ -293,9 +293,7 @@ func (s OpentelemetrySpan) SetStatus(code codes.Code, description string) { } func (s OpentelemetrySpan) RecordError(err error, options ...trace.EventOption) { - for _, o := range options { - s.span.RecordError(err, o) - } + s.span.RecordError(err, options...) } func (s OpentelemetrySpan) AddEvents(keys []string, values []EventValue) { From 0ec253bfa00cd03a8c0b7d7f700c15e74e9f5e7f Mon Sep 17 00:00:00 2001 From: Hamas Shafiq Date: Wed, 28 Sep 2022 15:07:10 +0100 Subject: [PATCH 04/62] Tempo: Add a placeholder to the TraceQL query editor (#55257) --- .../datasource/tempo/traceql/QueryEditor.tsx | 1 + .../tempo/traceql/TraceQLEditor.tsx | 52 +++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx b/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx index a88677957a3..f950c630045 100644 --- a/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx +++ b/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx @@ -34,6 +34,7 @@ export function QueryEditor(props: Props) { void; onRunQuery: () => void; @@ -21,9 +22,11 @@ interface Props { } export function TraceQLEditor(props: Props) { - const { onRunQuery } = props; + const { onRunQuery, placeholder } = props; const setupAutocompleteFn = useAutocomplete(props.datasource); - const styles = useStyles2(getStyles); + const theme = useTheme2(); + const styles = getStyles(theme, placeholder); + return ( { setupAutocompleteFn(editor, monaco); setupActions(editor, monaco, onRunQuery); + setupPlaceholder(editor, monaco, styles); }} /> ); } +function setupPlaceholder(editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco, styles: EditorStyles) { + const placeholderDecorators = [ + { + range: new monaco.Range(1, 1, 1, 1), + options: { + className: styles.placeholder, // The placeholder text is in styles.placeholder + isWholeLine: true, + }, + }, + ]; + + let decorators: string[] = []; + + const checkDecorators = (): void => { + const model = editor.getModel(); + + if (!model) { + return; + } + + const newDecorators = model.getValueLength() === 0 ? placeholderDecorators : []; + decorators = model.deltaDecorations(decorators, newDecorators); + }; + + checkDecorators(); + editor.onDidChangeModelContent(checkDecorators); +} + function setupActions(editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco, onRunQuery: () => void) { editor.addAction({ id: 'run-query', @@ -135,12 +167,24 @@ function ensureTraceQL(monaco: Monaco) { } } -const getStyles = (theme: GrafanaTheme2) => { +interface EditorStyles { + placeholder: string; + queryField: string; +} + +const getStyles = (theme: GrafanaTheme2, placeholder: string): EditorStyles => { return { queryField: css` border-radius: ${theme.shape.borderRadius()}; border: 1px solid ${theme.components.input.borderColor}; flex: 1; `, + placeholder: css` + ::after { + content: '${placeholder}'; + font-family: ${theme.typography.fontFamilyMonospace}; + opacity: 0.3; + } + `, }; }; From 122e7c141d2801d5e576865c29bff3b14bfd6717 Mon Sep 17 00:00:00 2001 From: idafurjes <36131195+idafurjes@users.noreply.github.com> Date: Wed, 28 Sep 2022 16:17:09 +0200 Subject: [PATCH 05/62] Chore: Delete UpdateUser, ChangeUserPassword, UpdateLastSeenAt from sqlstore (#55928) --- pkg/services/sqlstore/user.go | 61 --------------- pkg/services/sqlstore/user_test.go | 67 ----------------- pkg/services/user/userimpl/store.go | 65 ++++++++++++++++ pkg/services/user/userimpl/store_test.go | 94 ++++++++++++++++++++++++ pkg/services/user/userimpl/user.go | 24 +----- pkg/services/user/userimpl/user_test.go | 12 +++ 6 files changed, 174 insertions(+), 149 deletions(-) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index eacaa039dc3..a683e2a13ce 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -201,67 +201,6 @@ func (ss *SQLStore) GetUserById(ctx context.Context, query *models.GetUserByIdQu }) } -func (ss *SQLStore) UpdateUser(ctx context.Context, cmd *models.UpdateUserCommand) error { - if ss.Cfg.CaseInsensitiveLogin { - cmd.Login = strings.ToLower(cmd.Login) - cmd.Email = strings.ToLower(cmd.Email) - } - - return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { - user := user.User{ - Name: cmd.Name, - Email: cmd.Email, - Login: cmd.Login, - Theme: cmd.Theme, - Updated: TimeNow(), - } - - if _, err := sess.ID(cmd.UserId).Where(notServiceAccountFilter(ss)).Update(&user); err != nil { - return err - } - - if ss.Cfg.CaseInsensitiveLogin { - if err := ss.userCaseInsensitiveLoginConflict(ctx, sess, user.Login, user.Email); err != nil { - return err - } - } - - sess.publishAfterCommit(&events.UserUpdated{ - Timestamp: user.Created, - Id: user.ID, - Name: user.Name, - Login: user.Login, - Email: user.Email, - }) - - return nil - }) -} - -func (ss *SQLStore) ChangeUserPassword(ctx context.Context, cmd *models.ChangeUserPasswordCommand) error { - return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { - user := user.User{ - Password: cmd.NewPassword, - Updated: TimeNow(), - } - - _, err := sess.ID(cmd.UserId).Where(notServiceAccountFilter(ss)).Update(&user) - return err - }) -} - -func (ss *SQLStore) UpdateUserLastSeenAt(ctx context.Context, cmd *models.UpdateUserLastSeenAtCommand) error { - return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error { - user := user.User{ - ID: cmd.UserId, - LastSeenAt: TimeNow(), - } - - _, err := sess.ID(cmd.UserId).Update(&user) - return err - }) -} - func (ss *SQLStore) SetUsingOrg(ctx context.Context, cmd *models.SetUsingOrgCommand) error { getOrgsForUserCmd := &models.GetUserOrgListQuery{UserId: cmd.UserId} if err := ss.GetUserOrgList(ctx, getOrgsForUserCmd); err != nil { diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index 51e1d56993c..2094a8263c5 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -12,73 +12,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestIntegrationUserUpdate(t *testing.T) { - if testing.Short() { - t.Skip("skipping integration test") - } - - ss := InitTestDB(t) - - users := createFiveTestUsers(t, ss, func(i int) *user.CreateUserCommand { - return &user.CreateUserCommand{ - Email: fmt.Sprint("USER", i, "@test.com"), - Name: fmt.Sprint("USER", i), - Login: fmt.Sprint("loginUSER", i), - IsDisabled: false, - } - }) - - ss.Cfg.CaseInsensitiveLogin = true - - t.Run("Testing DB - update generates duplicate user", func(t *testing.T) { - err := ss.UpdateUser(context.Background(), &models.UpdateUserCommand{ - Login: "loginuser2", - UserId: users[0].ID, - }) - - require.Error(t, err) - }) - - t.Run("Testing DB - update lowercases existing user", func(t *testing.T) { - err := ss.UpdateUser(context.Background(), &models.UpdateUserCommand{ - Login: "loginUSER0", - Email: "USER0@test.com", - UserId: users[0].ID, - }) - require.NoError(t, err) - - query := models.GetUserByIdQuery{Id: users[0].ID} - err = ss.GetUserById(context.Background(), &query) - require.NoError(t, err) - - require.Equal(t, "loginuser0", query.Result.Login) - require.Equal(t, "user0@test.com", query.Result.Email) - }) - - t.Run("Testing DB - no user info provided", func(t *testing.T) { - err := ss.UpdateUser(context.Background(), &models.UpdateUserCommand{ - Login: "", - Email: "", - Name: "Change Name", - UserId: users[3].ID, - }) - require.NoError(t, err) - - query := models.GetUserByIdQuery{Id: users[3].ID} - err = ss.GetUserById(context.Background(), &query) - require.NoError(t, err) - - // Changed - require.Equal(t, "Change Name", query.Result.Name) - - // Unchanged - require.Equal(t, "loginUSER3", query.Result.Login) - require.Equal(t, "USER3@test.com", query.Result.Email) - }) - - ss.Cfg.CaseInsensitiveLogin = false -} - func TestIntegrationUserDataAccess(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go index e986352254b..f2ed0c5c8dd 100644 --- a/pkg/services/user/userimpl/store.go +++ b/pkg/services/user/userimpl/store.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/log" @@ -23,6 +24,9 @@ type store interface { CaseInsensitiveLoginConflict(context.Context, string, string) error GetByLogin(context.Context, *user.GetUserByLoginQuery) (*user.User, error) GetByEmail(context.Context, *user.GetUserByEmailQuery) (*user.User, error) + Update(context.Context, *user.UpdateUserCommand) error + ChangePassword(context.Context, *user.ChangeUserPasswordCommand) error + UpdateLastSeenAt(context.Context, *user.UpdateUserLastSeenAtCommand) error } type sqlStore struct { @@ -246,3 +250,64 @@ func (ss *sqlStore) userCaseInsensitiveLoginConflict(ctx context.Context, sess * return nil } + +func (ss *sqlStore) Update(ctx context.Context, cmd *user.UpdateUserCommand) error { + if ss.cfg.CaseInsensitiveLogin { + cmd.Login = strings.ToLower(cmd.Login) + cmd.Email = strings.ToLower(cmd.Email) + } + + return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + user := user.User{ + Name: cmd.Name, + Email: cmd.Email, + Login: cmd.Login, + Theme: cmd.Theme, + Updated: time.Now(), + } + + if _, err := sess.ID(cmd.UserID).Where(ss.notServiceAccountFilter()).Update(&user); err != nil { + return err + } + + if ss.cfg.CaseInsensitiveLogin { + if err := ss.userCaseInsensitiveLoginConflict(ctx, sess, user.Login, user.Email); err != nil { + return err + } + } + + sess.PublishAfterCommit(&events.UserUpdated{ + Timestamp: user.Created, + Id: user.ID, + Name: user.Name, + Login: user.Login, + Email: user.Email, + }) + + return nil + }) +} + +func (ss *sqlStore) ChangePassword(ctx context.Context, cmd *user.ChangeUserPasswordCommand) error { + return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + user := user.User{ + Password: cmd.NewPassword, + Updated: time.Now(), + } + + _, err := sess.ID(cmd.UserID).Where(ss.notServiceAccountFilter()).Update(&user) + return err + }) +} + +func (ss *sqlStore) UpdateLastSeenAt(ctx context.Context, cmd *user.UpdateUserLastSeenAtCommand) error { + return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + user := user.User{ + ID: cmd.UserID, + LastSeenAt: time.Now(), + } + + _, err := sess.ID(cmd.UserID).Update(&user) + return err + }) +} diff --git a/pkg/services/user/userimpl/store_test.go b/pkg/services/user/userimpl/store_test.go index 9e865038c2b..bf1c8cef735 100644 --- a/pkg/services/user/userimpl/store_test.go +++ b/pkg/services/user/userimpl/store_test.go @@ -2,6 +2,7 @@ package userimpl import ( "context" + "fmt" "testing" "time" @@ -194,4 +195,97 @@ func TestIntegrationUserDataAccess(t *testing.T) { ss.Cfg.CaseInsensitiveLogin = false }) + + t.Run("Change user password", func(t *testing.T) { + err := userStore.ChangePassword(context.Background(), &user.ChangeUserPasswordCommand{}) + require.NoError(t, err) + }) + + t.Run("update last seen at", func(t *testing.T) { + err := userStore.UpdateLastSeenAt(context.Background(), &user.UpdateUserLastSeenAtCommand{}) + require.NoError(t, err) + }) +} + +func TestIntegrationUserUpdate(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + ss := sqlstore.InitTestDB(t) + userStore := ProvideStore(ss, setting.NewCfg()) + + users := createFiveTestUsers(t, ss, func(i int) *user.CreateUserCommand { + return &user.CreateUserCommand{ + Email: fmt.Sprint("USER", i, "@test.com"), + Name: fmt.Sprint("USER", i), + Login: fmt.Sprint("loginUSER", i), + IsDisabled: false, + } + }) + + userStore.cfg.CaseInsensitiveLogin = true + + t.Run("Testing DB - update generates duplicate user", func(t *testing.T) { + err := userStore.Update(context.Background(), &user.UpdateUserCommand{ + Login: "loginuser2", + UserID: users[0].ID, + }) + + require.Error(t, err) + }) + + t.Run("Testing DB - update lowercases existing user", func(t *testing.T) { + err := userStore.Update(context.Background(), &user.UpdateUserCommand{ + Login: "loginUSER0", + Email: "USER0@test.com", + UserID: users[0].ID, + }) + require.NoError(t, err) + + result, err := userStore.GetByID(context.Background(), users[0].ID) + require.NoError(t, err) + + require.Equal(t, "loginuser0", result.Login) + require.Equal(t, "user0@test.com", result.Email) + }) + + t.Run("Testing DB - no user info provided", func(t *testing.T) { + err := userStore.Update(context.Background(), &user.UpdateUserCommand{ + Login: "", + Email: "", + Name: "Change Name", + UserID: users[3].ID, + }) + require.NoError(t, err) + + // query := user.GetUserByIDQuery{ID: users[3].ID} + result, err := userStore.GetByID(context.Background(), users[3].ID) + require.NoError(t, err) + + // Changed + require.Equal(t, "Change Name", result.Name) + + // Unchanged + require.Equal(t, "loginUSER3", result.Login) + require.Equal(t, "USER3@test.com", result.Email) + }) + + ss.Cfg.CaseInsensitiveLogin = false +} + +func createFiveTestUsers(t *testing.T, sqlStore *sqlstore.SQLStore, fn func(i int) *user.CreateUserCommand) []user.User { + t.Helper() + + users := []user.User{} + for i := 0; i < 5; i++ { + cmd := fn(i) + + user, err := sqlStore.CreateUser(context.Background(), *cmd) + users = append(users, *user) + + require.Nil(t, err) + } + + return users } diff --git a/pkg/services/user/userimpl/user.go b/pkg/services/user/userimpl/user.go index e713e1be78b..1e1a9df869e 100644 --- a/pkg/services/user/userimpl/user.go +++ b/pkg/services/user/userimpl/user.go @@ -160,34 +160,16 @@ func (s *Service) GetByEmail(ctx context.Context, query *user.GetUserByEmailQuer return s.store.GetByEmail(ctx, query) } -// TODO: remove wrapper around sqlstore func (s *Service) Update(ctx context.Context, cmd *user.UpdateUserCommand) error { - q := &models.UpdateUserCommand{ - Name: cmd.Name, - Email: cmd.Email, - Login: cmd.Login, - Theme: cmd.Theme, - UserId: cmd.UserID, - } - return s.sqlStore.UpdateUser(ctx, q) + return s.store.Update(ctx, cmd) } -// TODO: remove wrapper around sqlstore func (s *Service) ChangePassword(ctx context.Context, cmd *user.ChangeUserPasswordCommand) error { - q := &models.ChangeUserPasswordCommand{ - UserId: cmd.UserID, - NewPassword: cmd.NewPassword, - OldPassword: cmd.OldPassword, - } - return s.sqlStore.ChangeUserPassword(ctx, q) + return s.store.ChangePassword(ctx, cmd) } -// TODO: remove wrapper around sqlstore func (s *Service) UpdateLastSeenAt(ctx context.Context, cmd *user.UpdateUserLastSeenAtCommand) error { - q := &models.UpdateUserLastSeenAtCommand{ - UserId: cmd.UserID, - } - return s.sqlStore.UpdateUserLastSeenAt(ctx, q) + return s.store.UpdateLastSeenAt(ctx, cmd) } // TODO: remove wrapper around sqlstore diff --git a/pkg/services/user/userimpl/user_test.go b/pkg/services/user/userimpl/user_test.go index d69329a0d64..4bf87408b3e 100644 --- a/pkg/services/user/userimpl/user_test.go +++ b/pkg/services/user/userimpl/user_test.go @@ -129,3 +129,15 @@ func (f *FakeUserStore) GetByLogin(ctx context.Context, query *user.GetUserByLog func (f *FakeUserStore) GetByEmail(ctx context.Context, query *user.GetUserByEmailQuery) (*user.User, error) { return f.ExpectedUser, f.ExpectedError } + +func (f *FakeUserStore) Update(ctx context.Context, cmd *user.UpdateUserCommand) error { + return f.ExpectedError +} + +func (f *FakeUserStore) ChangePassword(ctx context.Context, cmd *user.ChangeUserPasswordCommand) error { + return f.ExpectedError +} + +func (f *FakeUserStore) UpdateLastSeenAt(ctx context.Context, cmd *user.UpdateUserLastSeenAtCommand) error { + return f.ExpectedError +} From 4ffa0532f086c6099719d255311842a57bb88029 Mon Sep 17 00:00:00 2001 From: emerson_gong Date: Wed, 28 Sep 2022 22:35:06 +0800 Subject: [PATCH 06/62] Security: Fix CVE-2022-27664 (#55361) * Fix CVE-2022-27664 * update golang.org/x/sys in go.sum --- go.mod | 4 ++-- go.sum | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 92fd45c35d6..1644feccc83 100644 --- a/go.mod +++ b/go.mod @@ -106,7 +106,7 @@ require ( go.opentelemetry.io/otel/trace v1.6.3 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d - golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect + golang.org/x/net v0.0.0-20220909164309-bea034e7d591 // indirect golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 golang.org/x/time v0.0.0-20220609170525-579cf78fd858 @@ -231,7 +231,7 @@ require ( go.opencensus.io v0.23.0 // indirect go.uber.org/atomic v1.9.0 go.uber.org/goleak v1.1.12 // indirect - golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect + golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect golang.org/x/text v0.3.7 golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 272fd7e2903..95fb8f0cf29 100644 --- a/go.sum +++ b/go.sum @@ -2883,6 +2883,8 @@ golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220418201149-a630d4f3e7a2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -3099,6 +3101,8 @@ golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= From 71578d0932192bb10efd0b572a6b83aca237a2aa Mon Sep 17 00:00:00 2001 From: Tania Date: Wed, 28 Sep 2022 17:03:43 +0200 Subject: [PATCH 07/62] Docs: Document Reporting API (#55434) * Docs: Document reporting api * Update docs/sources/developers/http_api/reporting.md Co-authored-by: Alex Khomenko * Update docs/sources/developers/http_api/reporting.md Co-authored-by: Alex Khomenko * Update docs/sources/developers/http_api/reporting.md Co-authored-by: Alex Khomenko * Document the rest of endpoints * Document more endpoints * Fix scopes and links * Remove rendering endpoints * Fix status codes * Apply review feedback * Typo Co-authored-by: Alex Khomenko --- docs/sources/developers/http_api/reporting.md | 630 +++++++++++++++++- 1 file changed, 612 insertions(+), 18 deletions(-) diff --git a/docs/sources/developers/http_api/reporting.md b/docs/sources/developers/http_api/reporting.md index 12ffa51ed33..19fcb80d471 100644 --- a/docs/sources/developers/http_api/reporting.md +++ b/docs/sources/developers/http_api/reporting.md @@ -15,16 +15,416 @@ title: Reporting API This API allows you to interact programmatically with the [Reporting]({{< relref "../../dashboards/create-reports/" >}}) feature. +> The Reporting API is not stabilized yet, it is still in active development and may change without prior notice. + > Reporting is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "../../enterprise/" >}}). > If you are running Grafana Enterprise, for some endpoints you'll need to have specific permissions. Refer to [Role-based access control permissions]({{< relref "../../administration/roles-and-permissions/access-control/custom-role-actions-scopes/" >}}) for more information. +## List all reports + +`GET /api/reports` + +#### Required permissions + +See note in the [introduction]({{< ref "#reporting-api" >}}) for an explanation. + +| Action | Scope | +| ------------ | --------------------------- | +| reports:read | reports:\*
reports:id:\* | + +### Example request + +```http +GET /api/reports HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +### Example response + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 1840 + +[ + { + "id": 2, + "userId": 1, + "orgId": 1, + "name": "Report 2", + "recipients": "example-report@grafana.com", + "replyTo": "", + "message": "Hi, \nPlease find attached a PDF status report. If you have any questions, feel free to contact me!\nBest,", + "schedule": { + "startDate": "2022-10-02T00:00:00+02:00", + "endDate": null, + "frequency": "once", + "intervalFrequency": "", + "intervalAmount": 0, + "workdaysOnly": false, + "dayOfMonth": "2", + "timeZone": "Europe/Warsaw" + }, + "options": { + "orientation": "landscape", + "layout": "grid", + }, + "enableDashboardUrl": true, + "state": "scheduled", + "dashboards": [ + { + "dashboard": { + "id": 463, + "uid": "7MeksYbmk", + "name": "Alerting with TestData" + }, + "reportVariables": { + "namefilter": "TestData" + } + } + ], + "formats": [ + "pdf", + "csv" + ], + "created": "2022-09-19T11:44:42+02:00", + "updated": "2022-09-19T11:44:42+02:00" + } +] +``` + +### Status Codes + +- **200** – OK +- **401** - Authentication failed, refer to [Authentication API]({{< relref "auth/" >}}). +- **500** – Unexpected error or server misconfiguration. Refer to server logs for more details. + +## Get a report + +`GET /api/reports/:id` + +#### Required permissions + +See note in the [introduction]({{< ref "#reporting-api" >}}) for an explanation. + +| Action | Scope | +| ------------ | ---------------------------------------------------------- | +| reports:read | reports:\*
reports:id:\*
reports:id:1(single report) | + +### Example request + +```http +GET /api/reports/2 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +### Example response + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 940 + +{ + "id": 2, + "userId": 1, + "orgId": 1, + "name": "Report 2", + "recipients": "example-report@grafana.com", + "replyTo": "", + "message": "Hi, \nPlease find attached a PDF status report. If you have any questions, feel free to contact me!\nBest,", + "schedule": { + "startDate": "2022-10-02T00:00:00+02:00", + "endDate": null, + "frequency": "once", + "intervalFrequency": "", + "intervalAmount": 0, + "workdaysOnly": false, + "dayOfMonth": "2", + "timeZone": "Europe/Warsaw" + }, + "options": { + "orientation": "landscape", + "layout": "grid", + }, + "enableDashboardUrl": true, + "state": "scheduled", + "dashboards": [ + { + "dashboard": { + "id": 463, + "uid": "7MeksYbmk", + "name": "Alerting with TestData" + }, + "timeRange": { + "from": "", + "to": "" + }, + "reportVariables": { + "namefilter": "TestData" + } + } + ], + "formats": [ + "pdf", + "csv" + ], + "created": "2022-09-12T11:44:42+02:00", + "updated": "2022-09-12T11:44:42+02:00" +} +``` + +### Status Codes + +- **200** – OK +- **400** – Bad request (invalid report ID). +- **401** - Authentication failed, refer to [Authentication API]({{< relref "auth/" >}}). +- **403** – Forbidden (access denied to a report or a dashboard used in the report). +- **404** – Not found (such report does not exist). +- **500** – Unexpected error or server misconfiguration. Refer to server logs for more details. + +## Create a report + +`POST /api/reports` + +#### Required permissions + +See note in the [introduction]({{< ref "#reporting-api" >}}) for an explanation. + +| Action | Scope | +| -------------- | ----- | +| reports:create | n/a | + +### Example request + +```http +POST /api/reports HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "name": "Report 4", + "recipients": "texample-report@grafana.com", + "replyTo": "", + "message": "Hello, please, find the report attached", + "schedule": { + "startDate": "2022-10-02T10:00:00+02:00", + "endDate": "2022-11-02T20:00:00+02:00", + "frequency": "daily", + "intervalFrequency": "", + "intervalAmount": 0, + "workdaysOnly": true, + "timeZone": "Europe/Warsaw" + }, + "options": { + "orientation": "landscape", + "layout": "grid" + }, + "enableDashboardUrl": true, + "dashboards": [ + { + "dashboard": { + "uid": "7MeksYbmk", + }, + "timeRange": { + "from": "2022-08-08T15:00:00+02:00", + "to": "2022-09-02T17:00:00+02:00" + }, + "reportVariables": { + "varibale1": "Value1" + } + } + ], + "formats": [ + "pdf", + "csv" + ] +} +``` + +#### Config JSON Body Schema + +| Field name | Data type | Description | +| ------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | Name of the report that is used as an email subject. | +| recipients | string | Comma-separated list of emails to which to send the report to. | +| replyTo | string | Comma-separated list of emails used in a reply-to field of the report email. | +| message | string | Text message used for the body of the report email. | +| startDate | string | Report distribution starts from this date. | +| endDate | string | Report distribution ends on this date. | +| frequency | string | Specifies how often the report should be sent. Can be `once`, `hourly`, `daily`, `weekly`, `monthly`, `last` or `custom`.

`last` - schedules the report for the last day of month.

`custom` - schedules the report to be sent on a custom interval.
It requires `intervalFrequency` and `intervalAmount` to be specified: for example, every 2 weeks, where 2 is an `intervalAmount` and `weeks` is an `intervalFrequency`. | +| intervalFrequency | string | The type of the `custom` interval: `hours`, `days`, `weeks`, `months`. | +| intervalAmount | number | `custom` interval amount. | +| workdaysOnly | string | Send the report only on Monday-Friday. Applicable to `hourly` and `daily` types of schedule. | +| timeZone | string | Time zone used to schedule report execution. | +| orientation | string | Can be `portrait` or `landscape`. | +| layout | string | Can be `grid` or `simple`. | +| enableDashboardUrl | bool | Adds a dashboard url to the bottom of the report email. | +| formats | []string | Specified what kind of attachment to generate for the report - `csv`, `pdf`, `image`.
`pdf` is the default one.
`csv` attaches a CSV file for each table panel.
`image` embeds an image of a dashboard into the email's body. | +| dashboards | []object | Dashboards to generate a report for.
See "Report Dashboard Schema" section below. | + +#### Report Dashboard Schema + +| Field name | Data type | Description | +| ------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| dashboard.uid | string | Dashboard [UID](../dashboard#identifier-id-vs-unique-identifier-uid). | +| timeRange.from | string | Dashboard time range from. | +| timeRange.to | string | Dashboard time range to. | +| reportVariables. | string | Key-value pairs containing the template variables for this report, in JSON format. If empty, the template variables from the report's dashboard will be used. | + +### Example response + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 35 + +{ + "id": 4, + "message": "Report created" +} +``` + +### Status Codes + +- **200** – OK +- **400** – Bad request (invalid json, missing or invalid fields values, etc.). +- **403** - Forbidden (access denied to a report or a dashboard used in the report). +- **500** - Unexpected error or server misconfiguration. Refer to server logs for more details + +## Update a report + +`PUT /api/reports/:id` + +#### Required permissions + +See note in the [introduction]({{< ref "#reporting-api" >}}) for an explanation. + +| Action | Scope | +| ------------- | --------------------------------------------------------- | +| reports:write | reports:\*
reports:id:\*
reports:1(single report) | + +### Example request + +See [JSON body schema]({{< ref "#config-json-body-schema" >}}) for fields description. + +```http +GET /api/reports HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "name": "Updated Report", + "recipients": "example-report@grafana.com", + "replyTo": "", + "message": "Hello, please, find the report attached", + "schedule": { + "frequency": "hourly", + "timeZone": "Africa/Cairo", + "workdaysOnly": true, + "startDate": "2022-10-10T10:00:00+02:00", + "endDate": "2022-11-20T19:00:00+02:00" + }, + "options": { + "orientation": "landscape", + "layout": "grid", + }, + "enableDashboardUrl": true, + "state": "scheduled", + "dashboards": [ + { + "dashboard": { + "id": 463, + "uid": "7MeksYbmk", + "name": "Alerting with TestData" + }, + "timeRange": { + "from": "2022-08-08T15:00:00+02:00", + "to": "2022-09-02T17:00:00+02:00" + }, + "reportVariables": { + "varibale1": "Value1" + } + } + ], + "formats": [ + "pdf", + "csv" + ] +} +``` + +### Example response + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 28 + +{ + "message": "Report updated" +} +``` + +### Status Codes + +- **200** – OK +- **400** – Bad request (invalid json, missing or invalid fields values, etc.). +- **401** - Authentication failed, refer to [Authentication API]({{< relref "auth/" >}}). +- **403** – Forbidden (access denied to a report or a dashboard used in the report). +- **404** – Not found (such report does not exist). +- **500** – Unexpected error or server misconfiguration. Refer to server logs for more details. + +## Delete a report + +`DELETE /api/reports/:id` + +#### Required permissions + +See note in the [introduction]({{< ref "#reporting-api" >}}) for an explanation. + +| Action | Scope | +| -------------- | --------------------------------------------------------- | +| reports:delete | reports:\*
reports:id:\*
reports:1(single report) | + +### Example request + +```http +GET /api/reports/6 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +### Example response + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 39 + +{ + "message": "Report config was removed" +} +``` + +### Status Codes + +- **200** – OK +- **400** – Bad request (invalid report ID). +- **401** - Authentication failed, refer to [Authentication API]({{< relref "auth/" >}}). +- **404** - Not found (report with this ID does not exist). +- **500** - Unexpected error or server misconfiguration. Refer to server logs for more details + ## Send a report -> Only available in Grafana Enterprise v7.0+. - -> This API endpoint is experimental and may be deprecated in a future release. On deprecation, a migration strategy will be provided and the endpoint will remain functional until the next major release of Grafana. - `POST /api/reports/email` Generate and send a report. This API waits for the report to be generated before returning. We recommend that you set the client's timeout to at least 60 seconds. @@ -51,13 +451,13 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk } ``` -### JSON Body Schema +#### JSON Body Schema -| Field name | Data type | Description | -| ------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | string | ID of the report to send. It is the same as in the URL when editing a report, not to be confused with the ID of the dashboard. Required. | -| emails | string | Comma-separated list of emails to which to send the report to. Overrides the emails from the report. Required if **useEmailsFromReport** is not present. | -| useEmailsFromReport | boolean | Send the report to the emails specified in the report. Required if **emails** is not present. | +| Field name | Data type | Description | +| ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| id | string | ID of the report to send. It is the same as in the URL when editing a report, not to be confused with the ID of the dashboard. Required. | +| emails | string | Comma-separated list of emails to which to send the report to. Overrides the emails from the report. Required if `useEmailsFromReport` is not present. | +| useEmailsFromReport | boolean | Send the report to the emails specified in the report. Required if `emails` is not present. | ### Example response @@ -71,11 +471,205 @@ Content-Length: 29 ### Status Codes -| Code | Description | -| ---- | ----------------------------------------------------------------------------------- | -| 200 | Report was sent. | -| 400 | Bad request (invalid json, missing content-type, missing or invalid fields, etc.). | -| 401 | Authentication failed, refer to [Authentication API]({{< relref "auth/" >}}). | -| 403 | User is authenticated but is not authorized to generate the report. | -| 404 | Report not found. | -| 500 | Unexpected error or server misconfiguration. Refer to server logs for more details. | +- **200** – Report was sent. +- **400** – Bad request (invalid json, missing content-type, missing or invalid fields, etc.). +- **401** - Authentication failed, refer to [Authentication API]({{< relref "auth/" >}}). +- **403** - Forbidden (access denied to a report or a dashboard used in the report). +- **404** - Report not found. +- **500** - Unexpected error or server misconfiguration. Refer to server logs for more details. + +## Get reports branding settings + +`GET /api/reports/settings` + +Returns reports branding settings that are global and used across all the reports. + +#### Required permissions + +See note in the [introduction]({{< ref "#reporting-api" >}}) for an explanation. + +| Action | Scope | +| --------------------- | ----- | +| reports.settings:read | n/a | + +### Example request + +```http +GET /api/reports/settings HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +### Example response + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 181 + +{ + "id": 1, + "userId": 1, + "orgId": 1, + "branding": { + "reportLogoUrl": "", + "emailLogoUrl": "", + "emailFooterMode": "sent-by", + "emailFooterText": "Grafana Labs", + "emailFooterLink": "https://grafana.com/" + } +} +``` + +### Status Codes + +- **200** – OK +- **401** - Authentication failed, refer to [Authentication API]({{< relref "auth/" >}}). +- **500** - Unexpected error or server misconfiguration. Refer to server logs for more detail + +## Save reports branding settings + +`POST /api/reports/settings` + +Creates settings if they don't exist, otherwise updates them. These settings are global and used across all the reports. + +#### Required permissions + +See note in the [introduction]({{< ref "#reporting-api" >}}) for an explanation. + +| Action | Scope | +| ---------------------- | ----- | +| reports.settings:write | n/a | + +### Example request + +```http +POST /api/reports/settings HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "branding": { + "reportLogoUrl": "https://grafana.com/reportLogo.jpg", + "emailLogoUrl": "https://grafana.com/emailLogo.jpg", + "emailFooterMode": "sent-by", + "emailFooterText": "Grafana Labs", + "emailFooterLink": "https://grafana.com/" + } +} +``` + +#### JSON Body Schema + +| Field name | Data type | Description | +| ------------------------ | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| branding.reportLogoUrl | string | URL of an image used as a logo on every page of the report. | +| branding.emailLogoUrl | string | URL of an image used as a logo in the email. | +| branding.emailFooterMode | string | Can be `sent-by` or `none`.
`sent-by` adds a "Sent by `branding.emailFooterText`" footer link to the email. Requires specifying values in the `branding.emailFooterText` and `branding.emailFooterLink` fields.
`none` suppresses adding a "Sent by" footer link to the email. | +| branding.emailFooterText | string | Text of a URL added to the email "Sent by" footer. | +| branding.emailFooterLink | string | URL address value added to the email "Sent by" footer. | + +### Example response + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 35 + +{ + "message": "Report settings saved" +} +``` + +### Status Codes + +- **200** – OK +- **400** – Bad request (invalid json, missing or invalid fields values, etc.). +- **401** - Authentication failed, refer to [Authentication API]({{< relref "auth/" >}}). +- **500** - Unexpected error or server misconfiguration. Refer to server logs for more detail + +## Send a test email + +`POST /api/reports/test-email` + +Sends a test email with a report without persisting it in the database. + +#### Required permissions + +See note in the [introduction]({{< ref "#reporting-api" >}}) for an explanation. + +| Action | Scope | +| ------------ | ----- | +| reports:send | n/a | + +### Example request + +See [JSON body schema]({{< ref "#config-json-body-schema" >}}) for fields description. + +```http +POST /api/reports/test-email HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{{ + "name": "Report 4", + "recipients": "example-report@grafana.com", + "replyTo": "", + "message": "Hello, please, find the report attached", + "schedule": { + "startDate": "2022-10-02T10:00:00+02:00", + "endDate": "2022-11-02T20:00:00+02:00", + "frequency": "daily", + "intervalFrequency": "", + "intervalAmount": 0, + "workdaysOnly": true, + "timeZone": "Europe/Warsaw" + }, + "options": { + "orientation": "landscape", + "layout": "grid" + }, + "enableDashboardUrl": true, + "dashboards": [ + { + "dashboard": { + "uid": "7MeksYbmk", + }, + "timeRange": { + "from": "2022-08-08T15:00:00+02:00", + "to": "2022-09-02T17:00:00+02:00" + }, + "reportVariables": { + "varibale1": "Value1" + } + } + ], + "formats": [ + "pdf", + "csv" + ] +} +``` + +### Example response + +```http +HTTP/1.1 200 OK +Content-Type: application/json +Content-Length: 29 + +{ + "message": "Test email sent" +} +``` + +### Status Codes + +- **200** – OK +- **400** – Bad request (invalid json, missing or invalid fields values, etc.). +- **401** - Authentication failed, refer to [Authentication API]({{< relref "auth/" >}}). +- **403** - Forbidden (access denied to a report or a dashboard used in the report). +- **500** - Unexpected error or server misconfiguration. Refer to server logs for more details From b81f90cd0c3ebe03b489a32f3dfa38f1be0686a8 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Wed, 28 Sep 2022 17:04:55 +0200 Subject: [PATCH 08/62] Toolkit: Fix script failures due to missing import paths (#55857) * fix(toolkit): package without dist folder so config file paths are correct * chore(packages): make sure to bundle readme, changelog and apache2 license * refactor(packages): recursively copy src when prepacking toolkit * chore(toolkit): lock down babel dependencies. use grafana-eslint-config@5 for TS4.8 support --- .gitignore | 1 + packages/grafana-data/package.json | 3 +- packages/grafana-e2e-selectors/package.json | 3 +- packages/grafana-e2e/package.json | 3 +- packages/grafana-runtime/package.json | 3 +- packages/grafana-schema/package.json | 3 +- packages/grafana-toolkit/package.json | 30 +-- .../src/cli/tasks/toolkit.build.ts | 10 +- packages/grafana-ui/package.json | 3 +- yarn.lock | 246 ++++++++---------- 10 files changed, 147 insertions(+), 158 deletions(-) diff --git a/.gitignore b/.gitignore index 865bc7dd70b..cf9fe79b44d 100644 --- a/.gitignore +++ b/.gitignore @@ -130,6 +130,7 @@ pkg/cmd/grafana-server/__debug_bin /packages/**/.rpt2_cache /packages/**/tsdoc-metadata.json /packages/**/package.tgz +/packages/grafana-toolkit/sass ## CI places the packages in a different location /npm-artifacts/*.tgz diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index e2d293a0fde..4e197b6fe70 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -23,7 +23,8 @@ }, "files": [ "dist", - "CHANGELOG.md", + "./README.md", + "./CHANGELOG.md", "LICENSE_APACHE2" ], "scripts": { diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index 9c71a7bbc92..45b1de30667 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -26,7 +26,8 @@ }, "files": [ "dist", - "CHANGELOG.md", + "./README.md", + "./CHANGELOG.md", "LICENSE_APACHE2" ], "scripts": { diff --git a/packages/grafana-e2e/package.json b/packages/grafana-e2e/package.json index e94f37a8481..bdbfa91a4e4 100644 --- a/packages/grafana-e2e/package.json +++ b/packages/grafana-e2e/package.json @@ -30,7 +30,8 @@ "dist", "cli.js", "cypress.json", - "CHANGELOG.md", + "./README.md", + "./CHANGELOG.md", "LICENSE_APACHE2" ], "scripts": { diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index ba5bee319c2..73f557c6965 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -24,7 +24,8 @@ }, "files": [ "dist", - "CHANGELOG.md", + "./README.md", + "./CHANGELOG.md", "LICENSE_APACHE2" ], "scripts": { diff --git a/packages/grafana-schema/package.json b/packages/grafana-schema/package.json index 66c9ac3206f..fe87ef1f45c 100644 --- a/packages/grafana-schema/package.json +++ b/packages/grafana-schema/package.json @@ -23,7 +23,8 @@ }, "files": [ "dist", - "CHANGELOG.md", + "./README.md", + "./CHANGELOG.md", "LICENSE_APACHE2" ], "scripts": { diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index f75a0e503a7..bd42b3a5c25 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -18,26 +18,24 @@ "bin": { "grafana-toolkit": "./bin/grafana-toolkit.js" }, - "publishConfig": { - "bin": { - "grafana-toolkit": "./dist/bin/grafana-toolkit.js" - }, - "access": "public" - }, "files": [ - "dist", - "README.md", - "CHANGELOG.md" + "config", + "src", + "sass", + "./README.md", + "./CHANGELOG.md", + "LICENSE_APACHE2" ], "scripts": { "build": "grafana-toolkit toolkit:build", - "clean": "rimraf ./dist ./compiled ./package.tgz", - "precommit": "npm run lint & npm run typecheck", + "clean": "rimraf ./dist ./compiled ./sass ./package.tgz", + "prepack": "mv ./src ./src_bak && cp -r ./dist/src ./src", + "postpack": "rimraf ./src && mv ./src_bak ./src", "typecheck": "tsc --noEmit" }, "main": "src/index.ts", "dependencies": { - "@babel/core": "^7.18.9", + "@babel/core": "7.18.9", "@babel/plugin-proposal-class-properties": "7.18.6", "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", "@babel/plugin-proposal-object-rest-spread": "7.18.9", @@ -46,11 +44,11 @@ "@babel/plugin-transform-react-constant-elements": "7.18.9", "@babel/plugin-transform-runtime": "7.18.10", "@babel/plugin-transform-typescript": "7.19.0", - "@babel/preset-env": "^7.18.9", - "@babel/preset-react": "^7.18.6", - "@babel/preset-typescript": "^7.18.6", + "@babel/preset-env": "7.18.9", + "@babel/preset-react": "7.18.6", + "@babel/preset-typescript": "7.18.6", "@grafana/data": "9.3.0-pre", - "@grafana/eslint-config": "^4.0.0", + "@grafana/eslint-config": "5.0.0", "@grafana/tsconfig": "^1.2.0-rc1", "@grafana/ui": "9.3.0-pre", "@jest/core": "27.5.1", diff --git a/packages/grafana-toolkit/src/cli/tasks/toolkit.build.ts b/packages/grafana-toolkit/src/cli/tasks/toolkit.build.ts index ae80a8a6645..2711425b2a4 100644 --- a/packages/grafana-toolkit/src/cli/tasks/toolkit.build.ts +++ b/packages/grafana-toolkit/src/cli/tasks/toolkit.build.ts @@ -24,8 +24,6 @@ const compile = () => const copyFiles = () => { const files = [ - 'config/circleci/config.yml', - 'bin/grafana-toolkit.js', 'src/config/prettier.plugin.config.json', 'src/config/prettier.plugin.rc.js', 'src/config/tsconfig.plugin.json', @@ -60,12 +58,16 @@ const copyFiles = () => { const copySassFiles = () => { const files = ['_variables.generated.scss', '_variables.dark.generated.scss', '_variables.light.generated.scss']; + const exportDir = `${cwd}/sass`; return useSpinner(`Copy scss files ${files.join(', ')} files`, async () => { const sassDir = path.resolve(cwd, '../../public/sass/'); + if (!fs.existsSync(exportDir)) { + fs.mkdirSync(exportDir); + } const promises = files.map((file) => { return new Promise((resolve, reject) => { const name = file.replace('.generated', ''); - fs.copyFile(`${sassDir}/${file}`, `${distDir}/sass/${name}`, (err) => { + fs.copyFile(`${sassDir}/${file}`, `${exportDir}/${name}`, (err) => { if (err) { reject(err); return; @@ -89,8 +91,6 @@ const toolkitBuildTaskRunner: TaskRunner = async () => { await clean(); await compile(); - fs.mkdirSync('./dist/bin'); - fs.mkdirSync('./dist/sass'); await copyFiles(); await copySassFiles(); }; diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index a79f602719d..35f09484cee 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -26,7 +26,8 @@ }, "files": [ "dist", - "CHANGELOG.md", + "./README.md", + "./CHANGELOG.md", "LICENSE_APACHE2" ], "scripts": { diff --git a/yarn.lock b/yarn.lock index 2a92297ca51..123189788c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -116,6 +116,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:7.18.9": + version: 7.18.9 + resolution: "@babel/core@npm:7.18.9" + dependencies: + "@ampproject/remapping": ^2.1.0 + "@babel/code-frame": ^7.18.6 + "@babel/generator": ^7.18.9 + "@babel/helper-compilation-targets": ^7.18.9 + "@babel/helper-module-transforms": ^7.18.9 + "@babel/helpers": ^7.18.9 + "@babel/parser": ^7.18.9 + "@babel/template": ^7.18.6 + "@babel/traverse": ^7.18.9 + "@babel/types": ^7.18.9 + convert-source-map: ^1.7.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.1 + semver: ^6.3.0 + checksum: 64b9088b03fdf659b334864ef93bed85d60c17b27fcbd72970f8eb9e0d3266ffa5a1926960f648f2db36b0bafec615f947ea5117d200599a0661b9f0a9cdf323 + languageName: node + linkType: hard + "@babel/core@npm:7.19.0": version: 7.19.0 resolution: "@babel/core@npm:7.19.0" @@ -208,29 +231,6 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/core@npm:7.18.9" - dependencies: - "@ampproject/remapping": ^2.1.0 - "@babel/code-frame": ^7.18.6 - "@babel/generator": ^7.18.9 - "@babel/helper-compilation-targets": ^7.18.9 - "@babel/helper-module-transforms": ^7.18.9 - "@babel/helpers": ^7.18.9 - "@babel/parser": ^7.18.9 - "@babel/template": ^7.18.6 - "@babel/traverse": ^7.18.9 - "@babel/types": ^7.18.9 - convert-source-map: ^1.7.0 - debug: ^4.1.0 - gensync: ^1.0.0-beta.2 - json5: ^2.2.1 - semver: ^6.3.0 - checksum: 64b9088b03fdf659b334864ef93bed85d60c17b27fcbd72970f8eb9e0d3266ffa5a1926960f648f2db36b0bafec615f947ea5117d200599a0661b9f0a9cdf323 - languageName: node - linkType: hard - "@babel/core@npm:^7.7.2": version: 7.16.0 resolution: "@babel/core@npm:7.16.0" @@ -3563,6 +3563,91 @@ __metadata: languageName: node linkType: hard +"@babel/preset-env@npm:7.18.9": + version: 7.18.9 + resolution: "@babel/preset-env@npm:7.18.9" + dependencies: + "@babel/compat-data": ^7.18.8 + "@babel/helper-compilation-targets": ^7.18.9 + "@babel/helper-plugin-utils": ^7.18.9 + "@babel/helper-validator-option": ^7.18.6 + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.18.6 + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.18.9 + "@babel/plugin-proposal-async-generator-functions": ^7.18.6 + "@babel/plugin-proposal-class-properties": ^7.18.6 + "@babel/plugin-proposal-class-static-block": ^7.18.6 + "@babel/plugin-proposal-dynamic-import": ^7.18.6 + "@babel/plugin-proposal-export-namespace-from": ^7.18.9 + "@babel/plugin-proposal-json-strings": ^7.18.6 + "@babel/plugin-proposal-logical-assignment-operators": ^7.18.9 + "@babel/plugin-proposal-nullish-coalescing-operator": ^7.18.6 + "@babel/plugin-proposal-numeric-separator": ^7.18.6 + "@babel/plugin-proposal-object-rest-spread": ^7.18.9 + "@babel/plugin-proposal-optional-catch-binding": ^7.18.6 + "@babel/plugin-proposal-optional-chaining": ^7.18.9 + "@babel/plugin-proposal-private-methods": ^7.18.6 + "@babel/plugin-proposal-private-property-in-object": ^7.18.6 + "@babel/plugin-proposal-unicode-property-regex": ^7.18.6 + "@babel/plugin-syntax-async-generators": ^7.8.4 + "@babel/plugin-syntax-class-properties": ^7.12.13 + "@babel/plugin-syntax-class-static-block": ^7.14.5 + "@babel/plugin-syntax-dynamic-import": ^7.8.3 + "@babel/plugin-syntax-export-namespace-from": ^7.8.3 + "@babel/plugin-syntax-import-assertions": ^7.18.6 + "@babel/plugin-syntax-json-strings": ^7.8.3 + "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 + "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 + "@babel/plugin-syntax-numeric-separator": ^7.10.4 + "@babel/plugin-syntax-object-rest-spread": ^7.8.3 + "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 + "@babel/plugin-syntax-optional-chaining": ^7.8.3 + "@babel/plugin-syntax-private-property-in-object": ^7.14.5 + "@babel/plugin-syntax-top-level-await": ^7.14.5 + "@babel/plugin-transform-arrow-functions": ^7.18.6 + "@babel/plugin-transform-async-to-generator": ^7.18.6 + "@babel/plugin-transform-block-scoped-functions": ^7.18.6 + "@babel/plugin-transform-block-scoping": ^7.18.9 + "@babel/plugin-transform-classes": ^7.18.9 + "@babel/plugin-transform-computed-properties": ^7.18.9 + "@babel/plugin-transform-destructuring": ^7.18.9 + "@babel/plugin-transform-dotall-regex": ^7.18.6 + "@babel/plugin-transform-duplicate-keys": ^7.18.9 + "@babel/plugin-transform-exponentiation-operator": ^7.18.6 + "@babel/plugin-transform-for-of": ^7.18.8 + "@babel/plugin-transform-function-name": ^7.18.9 + "@babel/plugin-transform-literals": ^7.18.9 + "@babel/plugin-transform-member-expression-literals": ^7.18.6 + "@babel/plugin-transform-modules-amd": ^7.18.6 + "@babel/plugin-transform-modules-commonjs": ^7.18.6 + "@babel/plugin-transform-modules-systemjs": ^7.18.9 + "@babel/plugin-transform-modules-umd": ^7.18.6 + "@babel/plugin-transform-named-capturing-groups-regex": ^7.18.6 + "@babel/plugin-transform-new-target": ^7.18.6 + "@babel/plugin-transform-object-super": ^7.18.6 + "@babel/plugin-transform-parameters": ^7.18.8 + "@babel/plugin-transform-property-literals": ^7.18.6 + "@babel/plugin-transform-regenerator": ^7.18.6 + "@babel/plugin-transform-reserved-words": ^7.18.6 + "@babel/plugin-transform-shorthand-properties": ^7.18.6 + "@babel/plugin-transform-spread": ^7.18.9 + "@babel/plugin-transform-sticky-regex": ^7.18.6 + "@babel/plugin-transform-template-literals": ^7.18.9 + "@babel/plugin-transform-typeof-symbol": ^7.18.9 + "@babel/plugin-transform-unicode-escapes": ^7.18.6 + "@babel/plugin-transform-unicode-regex": ^7.18.6 + "@babel/preset-modules": ^0.1.5 + "@babel/types": ^7.18.9 + babel-plugin-polyfill-corejs2: ^0.3.1 + babel-plugin-polyfill-corejs3: ^0.5.2 + babel-plugin-polyfill-regenerator: ^0.3.1 + core-js-compat: ^3.22.1 + semver: ^6.3.0 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 311002b9255d1aa261afe712ab73a93687652437804e2f44e6cc55438f8b199463f53bb2b8e0912b0034f208a42eee664a9e126a6061ca504a792ede97dd027e + languageName: node + linkType: hard + "@babel/preset-env@npm:7.19.0": version: 7.19.0 resolution: "@babel/preset-env@npm:7.19.0" @@ -3732,91 +3817,6 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.18.9": - version: 7.18.9 - resolution: "@babel/preset-env@npm:7.18.9" - dependencies: - "@babel/compat-data": ^7.18.8 - "@babel/helper-compilation-targets": ^7.18.9 - "@babel/helper-plugin-utils": ^7.18.9 - "@babel/helper-validator-option": ^7.18.6 - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ^7.18.6 - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ^7.18.9 - "@babel/plugin-proposal-async-generator-functions": ^7.18.6 - "@babel/plugin-proposal-class-properties": ^7.18.6 - "@babel/plugin-proposal-class-static-block": ^7.18.6 - "@babel/plugin-proposal-dynamic-import": ^7.18.6 - "@babel/plugin-proposal-export-namespace-from": ^7.18.9 - "@babel/plugin-proposal-json-strings": ^7.18.6 - "@babel/plugin-proposal-logical-assignment-operators": ^7.18.9 - "@babel/plugin-proposal-nullish-coalescing-operator": ^7.18.6 - "@babel/plugin-proposal-numeric-separator": ^7.18.6 - "@babel/plugin-proposal-object-rest-spread": ^7.18.9 - "@babel/plugin-proposal-optional-catch-binding": ^7.18.6 - "@babel/plugin-proposal-optional-chaining": ^7.18.9 - "@babel/plugin-proposal-private-methods": ^7.18.6 - "@babel/plugin-proposal-private-property-in-object": ^7.18.6 - "@babel/plugin-proposal-unicode-property-regex": ^7.18.6 - "@babel/plugin-syntax-async-generators": ^7.8.4 - "@babel/plugin-syntax-class-properties": ^7.12.13 - "@babel/plugin-syntax-class-static-block": ^7.14.5 - "@babel/plugin-syntax-dynamic-import": ^7.8.3 - "@babel/plugin-syntax-export-namespace-from": ^7.8.3 - "@babel/plugin-syntax-import-assertions": ^7.18.6 - "@babel/plugin-syntax-json-strings": ^7.8.3 - "@babel/plugin-syntax-logical-assignment-operators": ^7.10.4 - "@babel/plugin-syntax-nullish-coalescing-operator": ^7.8.3 - "@babel/plugin-syntax-numeric-separator": ^7.10.4 - "@babel/plugin-syntax-object-rest-spread": ^7.8.3 - "@babel/plugin-syntax-optional-catch-binding": ^7.8.3 - "@babel/plugin-syntax-optional-chaining": ^7.8.3 - "@babel/plugin-syntax-private-property-in-object": ^7.14.5 - "@babel/plugin-syntax-top-level-await": ^7.14.5 - "@babel/plugin-transform-arrow-functions": ^7.18.6 - "@babel/plugin-transform-async-to-generator": ^7.18.6 - "@babel/plugin-transform-block-scoped-functions": ^7.18.6 - "@babel/plugin-transform-block-scoping": ^7.18.9 - "@babel/plugin-transform-classes": ^7.18.9 - "@babel/plugin-transform-computed-properties": ^7.18.9 - "@babel/plugin-transform-destructuring": ^7.18.9 - "@babel/plugin-transform-dotall-regex": ^7.18.6 - "@babel/plugin-transform-duplicate-keys": ^7.18.9 - "@babel/plugin-transform-exponentiation-operator": ^7.18.6 - "@babel/plugin-transform-for-of": ^7.18.8 - "@babel/plugin-transform-function-name": ^7.18.9 - "@babel/plugin-transform-literals": ^7.18.9 - "@babel/plugin-transform-member-expression-literals": ^7.18.6 - "@babel/plugin-transform-modules-amd": ^7.18.6 - "@babel/plugin-transform-modules-commonjs": ^7.18.6 - "@babel/plugin-transform-modules-systemjs": ^7.18.9 - "@babel/plugin-transform-modules-umd": ^7.18.6 - "@babel/plugin-transform-named-capturing-groups-regex": ^7.18.6 - "@babel/plugin-transform-new-target": ^7.18.6 - "@babel/plugin-transform-object-super": ^7.18.6 - "@babel/plugin-transform-parameters": ^7.18.8 - "@babel/plugin-transform-property-literals": ^7.18.6 - "@babel/plugin-transform-regenerator": ^7.18.6 - "@babel/plugin-transform-reserved-words": ^7.18.6 - "@babel/plugin-transform-shorthand-properties": ^7.18.6 - "@babel/plugin-transform-spread": ^7.18.9 - "@babel/plugin-transform-sticky-regex": ^7.18.6 - "@babel/plugin-transform-template-literals": ^7.18.9 - "@babel/plugin-transform-typeof-symbol": ^7.18.9 - "@babel/plugin-transform-unicode-escapes": ^7.18.6 - "@babel/plugin-transform-unicode-regex": ^7.18.6 - "@babel/preset-modules": ^0.1.5 - "@babel/types": ^7.18.9 - babel-plugin-polyfill-corejs2: ^0.3.1 - babel-plugin-polyfill-corejs3: ^0.5.2 - babel-plugin-polyfill-regenerator: ^0.3.1 - core-js-compat: ^3.22.1 - semver: ^6.3.0 - peerDependencies: - "@babel/core": ^7.0.0-0 - checksum: 311002b9255d1aa261afe712ab73a93687652437804e2f44e6cc55438f8b199463f53bb2b8e0912b0034f208a42eee664a9e126a6061ca504a792ede97dd027e - languageName: node - linkType: hard - "@babel/preset-flow@npm:^7.12.1": version: 7.16.7 resolution: "@babel/preset-flow@npm:7.16.7" @@ -3845,7 +3845,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-react@npm:7.18.6, @babel/preset-react@npm:^7.18.6": +"@babel/preset-react@npm:7.18.6": version: 7.18.6 resolution: "@babel/preset-react@npm:7.18.6" dependencies: @@ -3877,7 +3877,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-typescript@npm:7.18.6, @babel/preset-typescript@npm:^7.18.6": +"@babel/preset-typescript@npm:7.18.6": version: 7.18.6 resolution: "@babel/preset-typescript@npm:7.18.6" dependencies: @@ -5371,22 +5371,6 @@ __metadata: languageName: node linkType: hard -"@grafana/eslint-config@npm:^4.0.0": - version: 4.0.0 - resolution: "@grafana/eslint-config@npm:4.0.0" - dependencies: - "@typescript-eslint/eslint-plugin": 5.16.0 - "@typescript-eslint/parser": 5.16.0 - eslint: 8.11.0 - eslint-config-prettier: 8.5.0 - eslint-plugin-jsdoc: 38.0.6 - eslint-plugin-react: 7.29.4 - eslint-plugin-react-hooks: 4.3.0 - typescript: 4.6.4 - checksum: ed7dbf3bff5ac38daf57dfd590696d3f48f31f216f2bbcac126c3a37619aea66e856c07677c6387531bb2ae007bdd09caf36b8f289bb88b1ac1c719b6a537ae9 - languageName: node - linkType: hard - "@grafana/experimental@npm:^0.0.2-canary.36": version: 0.0.2-canary.36 resolution: "@grafana/experimental@npm:0.0.2-canary.36" @@ -5485,7 +5469,7 @@ __metadata: version: 0.0.0-use.local resolution: "@grafana/toolkit@workspace:packages/grafana-toolkit" dependencies: - "@babel/core": ^7.18.9 + "@babel/core": 7.18.9 "@babel/plugin-proposal-class-properties": 7.18.6 "@babel/plugin-proposal-nullish-coalescing-operator": 7.18.6 "@babel/plugin-proposal-object-rest-spread": 7.18.9 @@ -5494,11 +5478,11 @@ __metadata: "@babel/plugin-transform-react-constant-elements": 7.18.9 "@babel/plugin-transform-runtime": 7.18.10 "@babel/plugin-transform-typescript": 7.19.0 - "@babel/preset-env": ^7.18.9 - "@babel/preset-react": ^7.18.6 - "@babel/preset-typescript": ^7.18.6 + "@babel/preset-env": 7.18.9 + "@babel/preset-react": 7.18.6 + "@babel/preset-typescript": 7.18.6 "@grafana/data": 9.3.0-pre - "@grafana/eslint-config": ^4.0.0 + "@grafana/eslint-config": 5.0.0 "@grafana/tsconfig": ^1.2.0-rc1 "@grafana/ui": 9.3.0-pre "@jest/core": 27.5.1 From b749cef78e9bd2fb5a1fab85aa853e45382e6966 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com> Date: Wed, 28 Sep 2022 18:50:19 +0300 Subject: [PATCH 09/62] Chore: Remove unnecessary transaction (#55819) --- pkg/services/dashboards/database/database.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 56ed0f0b458..f66e8bd96e0 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -574,7 +574,7 @@ func GetAlertsByDashboardId2(dashboardId int64, sess *sqlstore.DBSession) ([]*mo } func (d *DashboardStore) updateAlerts(ctx context.Context, existingAlerts []*models.Alert, alerts []*models.Alert, log log.Logger) error { - return d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + return d.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { for _, alert := range alerts { update := false var alertToUpdate *models.Alert From 5bd65db8a8363c165524a86c64ff279ae6ab47fa Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 28 Sep 2022 18:36:36 +0200 Subject: [PATCH 10/62] Loki: Add new range operation range_counter (#55936) * Loki: Add new range operation range_counter * Update yarn.lock --- package.json | 2 +- .../plugins/datasource/loki/querybuilder/operations.ts | 1 + .../app/plugins/datasource/loki/querybuilder/types.ts | 1 + yarn.lock | 10 +++++----- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b88f862983f..7dfec4ca718 100644 --- a/package.json +++ b/package.json @@ -257,7 +257,7 @@ "@grafana/e2e-selectors": "workspace:*", "@grafana/experimental": "^0.0.2-canary.36", "@grafana/google-sdk": "0.0.3", - "@grafana/lezer-logql": "0.1.0", + "@grafana/lezer-logql": "0.1.1", "@grafana/runtime": "workspace:*", "@grafana/schema": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/public/app/plugins/datasource/loki/querybuilder/operations.ts b/public/app/plugins/datasource/loki/querybuilder/operations.ts index 8d2a3424e51..f3f37697359 100644 --- a/public/app/plugins/datasource/loki/querybuilder/operations.ts +++ b/public/app/plugins/datasource/loki/querybuilder/operations.ts @@ -47,6 +47,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] { const list: QueryBuilderOperationDef[] = [ createRangeOperation(LokiOperationId.Rate), + createRangeOperation(LokiOperationId.RateCounter), createRangeOperation(LokiOperationId.CountOverTime), createRangeOperation(LokiOperationId.SumOverTime), createRangeOperation(LokiOperationId.BytesRate), diff --git a/public/app/plugins/datasource/loki/querybuilder/types.ts b/public/app/plugins/datasource/loki/querybuilder/types.ts index 3713eca688f..12a4a27e580 100644 --- a/public/app/plugins/datasource/loki/querybuilder/types.ts +++ b/public/app/plugins/datasource/loki/querybuilder/types.ts @@ -41,6 +41,7 @@ export enum LokiOperationId { LineFormat = 'line_format', LabelFormat = 'label_format', Rate = 'rate', + RateCounter = 'rate_counter', CountOverTime = 'count_over_time', SumOverTime = 'sum_over_time', AvgOverTime = 'avg_over_time', diff --git a/yarn.lock b/yarn.lock index 123189788c7..50e0bdf5d04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5392,12 +5392,12 @@ __metadata: languageName: node linkType: hard -"@grafana/lezer-logql@npm:0.1.0": - version: 0.1.0 - resolution: "@grafana/lezer-logql@npm:0.1.0" +"@grafana/lezer-logql@npm:0.1.1": + version: 0.1.1 + resolution: "@grafana/lezer-logql@npm:0.1.1" peerDependencies: "@lezer/lr": ^1.0.0 - checksum: 77c8a75c211cfdebe99eaf892d07673c52531fdb696c463a9dade74c25d919b87561b2f97c716d1d710c95da7c49edcffb6c98e0d4b635d0aa91d40cc5167f9e + checksum: b0d71e5670c54d92fbc4bd92fe72cfb27469cc9797d5595b284f9d2b86782584cb1a9b8bac51af07648b807a9287e7159dcdd0e1fc3e32a2e79d52b3a7700e4b languageName: node linkType: hard @@ -22418,7 +22418,7 @@ __metadata: "@grafana/eslint-config": 5.0.0 "@grafana/experimental": ^0.0.2-canary.36 "@grafana/google-sdk": 0.0.3 - "@grafana/lezer-logql": 0.1.0 + "@grafana/lezer-logql": 0.1.1 "@grafana/runtime": "workspace:*" "@grafana/schema": "workspace:*" "@grafana/toolkit": "workspace:*" From e8ac52ba0f849d1e515db5e449b221795926cb8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Giedrius=20Statkevi=C4=8Dius?= Date: Wed, 28 Sep 2022 20:05:52 +0300 Subject: [PATCH 11/62] Prometheus: fix proxy check (#54823) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The original check has been added ~3 years ago and since then a new variable has been added that says whether the mode is browser or proxied. Start using the new variable in this function so that proxy mode could be properly detected. It's important to us to get these headers and this check is currently broken for HTTP/HTTPS URLs. Signed-off-by: Giedrius Statkevičius Signed-off-by: Giedrius Statkevičius --- .../datasource/prometheus/datasource.test.ts | 28 +++++++++++++++++++ .../datasource/prometheus/datasource.tsx | 3 +- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index 4b8db23959a..1f64f577bd3 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -12,6 +12,8 @@ import { LoadingState, toDataFrame, } from '@grafana/data'; +import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { TemplateSrv } from 'app/features/templating/template_srv'; import { QueryOptions } from 'app/types'; import { VariableHide } from '../../../features/variables/types'; @@ -1846,6 +1848,23 @@ describe('PrometheusDatasource for POST', () => { const httpOptions = { headers: {} as { [key: string]: number | undefined }, }; + const instanceSettings = { + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + access: 'proxy', + jsonData: { httpMethod: 'POST' }, + } as unknown as DataSourceInstanceSettings; + + let ds: PrometheusDatasource; + beforeEach(() => { + ds = new PrometheusDatasource( + instanceSettings, + templateSrvStub as unknown as TemplateSrv, + timeSrvStub as unknown as TimeSrv + ); + }); it('with proxy access tracing headers should be added', () => { ds._addTracingHeaders(httpOptions as any, options as any); @@ -1855,6 +1874,14 @@ describe('PrometheusDatasource for POST', () => { }); it('with direct access tracing headers should not be added', () => { + const instanceSettings = { + url: 'proxied', + directUrl: 'direct', + user: 'test', + password: 'mupp', + jsonData: { httpMethod: 'POST' }, + } as unknown as DataSourceInstanceSettings; + const mockDs = new PrometheusDatasource( { ...instanceSettings, url: 'http://127.0.0.1:8000' }, templateSrvStub as any, @@ -1882,6 +1909,7 @@ function getPrepareTargetsContext({ const instanceSettings = { url: 'proxied', directUrl: 'direct', + access: 'proxy', user: 'test', password: 'mupp', jsonData: { httpMethod: 'POST' }, diff --git a/public/app/plugins/datasource/prometheus/datasource.tsx b/public/app/plugins/datasource/prometheus/datasource.tsx index 729d1aea0b8..9e35aaa9299 100644 --- a/public/app/plugins/datasource/prometheus/datasource.tsx +++ b/public/app/plugins/datasource/prometheus/datasource.tsx @@ -144,8 +144,7 @@ export class PrometheusDatasource _addTracingHeaders(httpOptions: PromQueryRequest, options: DataQueryRequest) { httpOptions.headers = {}; - const proxyMode = !this.url.match(/^http/); - if (proxyMode) { + if (this.access === 'proxy') { httpOptions.headers['X-Dashboard-Id'] = options.dashboardId; httpOptions.headers['X-Dashboard-UID'] = options.dashboardUID; httpOptions.headers['X-Panel-Id'] = options.panelId; From a281c26580e69db4cca75a9b8fc7f01acbfe9a9d Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Wed, 28 Sep 2022 14:00:46 -0400 Subject: [PATCH 12/62] GRPC Server: Move reflection to a separate gRPC service (#55872) --- .../backgroundsvcs/background_services.go | 1 + pkg/server/wire.go | 1 + pkg/services/grpcserver/reflection.go | 35 +++++++++++++++++++ pkg/services/grpcserver/service.go | 5 +-- 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 pkg/services/grpcserver/reflection.go diff --git a/pkg/server/backgroundsvcs/background_services.go b/pkg/server/backgroundsvcs/background_services.go index 5862715661e..03aa3837eef 100644 --- a/pkg/server/backgroundsvcs/background_services.go +++ b/pkg/server/backgroundsvcs/background_services.go @@ -51,6 +51,7 @@ func ProvideBackgroundServiceRegistry( _ serviceaccounts.Service, _ *guardian.Provider, _ *plugindashboardsservice.DashboardUpdater, _ *sanitizer.Provider, _ *grpcserver.HealthService, + _ *grpcserver.ReflectionService, ) *BackgroundServiceRegistry { return NewBackgroundServiceRegistry( httpServer, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 36b61905181..109d1619b9a 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -344,6 +344,7 @@ var wireBasicSet = wire.NewSet( orgimpl.ProvideService, grpcserver.ProvideService, grpcserver.ProvideHealthService, + grpcserver.ProvideReflectionService, teamimpl.ProvideService, tempuserimpl.ProvideService, loginattemptimpl.ProvideService, diff --git a/pkg/services/grpcserver/reflection.go b/pkg/services/grpcserver/reflection.go new file mode 100644 index 00000000000..3a4ecd09fe4 --- /dev/null +++ b/pkg/services/grpcserver/reflection.go @@ -0,0 +1,35 @@ +package grpcserver + +import ( + "context" + + "github.com/grafana/grafana/pkg/setting" + + "google.golang.org/grpc/reflection" + "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" +) + +// ReflectionService implements the gRPC Server Reflection Protocol: +// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md +type ReflectionService struct { + cfg *setting.Cfg + reflectionServer *reflectionServer +} + +type reflectionServer struct { + grpc_reflection_v1alpha.ServerReflectionServer +} + +// AuthFuncOverride no auth for reflection service. +func (s *reflectionServer) AuthFuncOverride(ctx context.Context, _ string) (context.Context, error) { + return ctx, nil +} + +func ProvideReflectionService(cfg *setting.Cfg, grpcServerProvider Provider) (*ReflectionService, error) { + re := &reflectionServer{reflection.NewServer(reflection.ServerOptions{Services: grpcServerProvider.GetServer()})} + grpc_reflection_v1alpha.RegisterServerReflectionServer(grpcServerProvider.GetServer(), re) + return &ReflectionService{ + cfg: cfg, + reflectionServer: re, + }, nil +} diff --git a/pkg/services/grpcserver/service.go b/pkg/services/grpcserver/service.go index e207bd3fa4f..8294a287f0b 100644 --- a/pkg/services/grpcserver/service.go +++ b/pkg/services/grpcserver/service.go @@ -16,7 +16,6 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials" - "google.golang.org/grpc/reflection" ) type Provider interface { @@ -51,9 +50,7 @@ func ProvideService(cfg *setting.Cfg, apiKey apikey.Service, userService user.Se opts = append(opts, grpc.Creds(credentials.NewTLS(cfg.GRPCServerTLSConfig))) } - grpcServer := grpc.NewServer(opts...) - reflection.Register(grpcServer) - s.server = grpcServer + s.server = grpc.NewServer(opts...) return s, nil } From b370a8e366c40c9a7e0594653a0cd4def1479232 Mon Sep 17 00:00:00 2001 From: Ezequiel Victorero Date: Wed, 28 Sep 2022 15:34:53 -0300 Subject: [PATCH 13/62] PublicDashboards: Allow disabling an existent public dashboard if it has template variables (#55778) --- .../publicdashboards/service/service.go | 10 ++++----- .../SharePublicDashboard/Configuration.tsx | 3 --- .../SharePublicDashboard.tsx | 22 ++++++++----------- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index 377489f1247..848a69d6654 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -96,16 +96,12 @@ func (pd *PublicDashboardServiceImpl) GetPublicDashboardConfig(ctx context.Conte // SavePublicDashboardConfig is a helper method to persist the sharing config // to the database. It handles validations for sharing config and persistence func (pd *PublicDashboardServiceImpl) SavePublicDashboardConfig(ctx context.Context, u *user.SignedInUser, dto *SavePublicDashboardConfigDTO) (*PublicDashboard, error) { + // validate if the dashboard exists dashboard, err := pd.GetDashboard(ctx, dto.DashboardUid) if err != nil { return nil, err } - err = validation.ValidateSavePublicDashboard(dto, dashboard) - if err != nil { - return nil, err - } - // set default value for time settings if dto.PublicDashboard.TimeSettings == nil { dto.PublicDashboard.TimeSettings = &TimeSettings{} @@ -120,6 +116,10 @@ func (pd *PublicDashboardServiceImpl) SavePublicDashboardConfig(ctx context.Cont // save changes var pubdashUid string if existingPubdash == nil { + err = validation.ValidateSavePublicDashboard(dto, dashboard) + if err != nil { + return nil, err + } pubdashUid, err = pd.savePublicDashboardConfig(ctx, dto) } else { pubdashUid, err = pd.updatePublicDashboardConfig(ctx, dto) diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/Configuration.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/Configuration.tsx index 46cf7fc7eca..e09f0564954 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/Configuration.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/Configuration.tsx @@ -13,14 +13,12 @@ import { getTimeRange } from 'app/features/dashboard/utils/timeRange'; export const Configuration = ({ disabled, isPubDashEnabled, - hasTemplateVariables, onToggleEnabled, dashboard, }: { disabled: boolean; isPubDashEnabled?: boolean; onToggleEnabled: () => void; - hasTemplateVariables: boolean; dashboard: DashboardModel; }) => { const selectors = e2eSelectors.pages.ShareDashboardModal.PublicDashboard; @@ -41,7 +39,6 @@ export const Configuration = ({ { diff --git a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx index fab54be0e4f..bd17032e58c 100644 --- a/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx +++ b/public/app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboard.tsx @@ -5,8 +5,6 @@ import { GrafanaTheme2 } from '@grafana/data/src'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src'; import { reportInteraction } from '@grafana/runtime/src'; import { Alert, Button, ClipboardButton, Field, HorizontalGroup, Input, useStyles2, Spinner } from '@grafana/ui/src'; -import { notifyApp } from 'app/core/actions'; -import { createErrorNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/services/context_srv'; import { useGetConfigQuery, useSaveConfigMutation } from 'app/features/dashboard/api/publicDashboardApi'; import { AcknowledgeCheckboxes } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/AcknowledgeCheckboxes'; @@ -20,7 +18,6 @@ import { } from 'app/features/dashboard/components/ShareModal/SharePublicDashboard/SharePublicDashboardUtils'; import { ShareModalTabProps } from 'app/features/dashboard/components/ShareModal/types'; import { isOrgAdmin } from 'app/features/plugins/admin/permissions'; -import { dispatch } from 'app/store/store'; import { AccessControlAction } from 'app/types'; interface Props extends ShareModalTabProps {} @@ -81,13 +78,6 @@ export const SharePublicDashboard = (props: Props) => { const onSavePublicConfig = () => { reportInteraction('grafana_dashboards_public_create_clicked'); - if (dashboardHasTemplateVariables(dashboardVariables)) { - dispatch( - notifyApp(createErrorNotification('This dashboard cannot be made public because it has template variables')) - ); - return; - } - saveConfig({ dashboard: props.dashboard, payload: { ...publicDashboard!, isEnabled: enabledSwitch.isEnabled }, @@ -111,7 +101,7 @@ export const SharePublicDashboard = (props: Props) => { {isFetchingLoading && }
- {dashboardHasTemplateVariables(dashboardVariables) ? ( + {dashboardHasTemplateVariables(dashboardVariables) && !publicDashboardPersisted(publicDashboard) ? ( { onToggleEnabled={() => setEnabledSwitch((prevState) => ({ isEnabled: !prevState.isEnabled, wasTouched: true })) } - hasTemplateVariables={dashboardHasTemplateVariables(dashboardVariables)} /> {publicDashboardPersisted(publicDashboard) && enabledSwitch.isEnabled && ( @@ -163,11 +152,18 @@ export const SharePublicDashboard = (props: Props) => { )} {hasWritePermissions ? ( - props.dashboard.hasUnsavedChanges() && ( + props.dashboard.hasUnsavedChanges() ? ( + ) : ( + dashboardHasTemplateVariables(dashboardVariables) && ( + + ) ) ) : ( From fc09d5a8a7c10df8b5053790e4b6fc5d3e03546b Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Wed, 28 Sep 2022 21:35:46 +0200 Subject: [PATCH 14/62] GrafanaUI: Expose data links context types externally (#55905) --- .../src/components/DataLinks/DataLinksContextMenu.tsx | 2 +- packages/grafana-ui/src/components/index.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx index 1bb1236eee8..2823c7c77c2 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx @@ -9,7 +9,7 @@ import { WithContextMenu } from '../ContextMenu/WithContextMenu'; import { MenuGroup, MenuItemsGroup } from '../Menu/MenuGroup'; import { MenuItem } from '../Menu/MenuItem'; -interface DataLinksContextMenuProps { +export interface DataLinksContextMenuProps { children: (props: DataLinksContextMenuApi) => JSX.Element; links: () => LinkModel[]; style?: CSSProperties; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 5e5f3db4781..5db63c342c7 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -156,7 +156,11 @@ export { MenuItem, type MenuItemProps } from './Menu/MenuItem'; export { WithContextMenu } from './ContextMenu/WithContextMenu'; export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor'; export { DataLinkInput } from './DataLinks/DataLinkInput'; -export { DataLinksContextMenu } from './DataLinks/DataLinksContextMenu'; +export { + DataLinksContextMenu, + type DataLinksContextMenuProps, + type DataLinksContextMenuApi, +} from './DataLinks/DataLinksContextMenu'; export { SeriesIcon } from './VizLegend/SeriesIcon'; export { InfoBox } from './InfoBox/InfoBox'; export { FeatureBadge, FeatureInfoBox } from './InfoBox/FeatureInfoBox'; From 16c9c858b8c9dd048328390f855f28ca545f71c0 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Wed, 28 Sep 2022 15:32:37 -0500 Subject: [PATCH 15/62] BarChart: add negY transform to fieldConfig overrides (#55930) --- public/app/plugins/panel/barchart/bars.ts | 14 ++++++++++-- public/app/plugins/panel/barchart/module.tsx | 24 +++++++++++++++++++- public/app/plugins/panel/barchart/utils.ts | 2 ++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/barchart/bars.ts b/public/app/plugins/panel/barchart/bars.ts index 7416ba55b7b..1e2282ef82b 100644 --- a/public/app/plugins/panel/barchart/bars.ts +++ b/public/app/plugins/panel/barchart/bars.ts @@ -57,6 +57,7 @@ export interface BarsOptions { legend?: VizLegendOptions; xSpacing?: number; xTimeAuto?: boolean; + negY?: boolean[]; } /** @@ -352,6 +353,10 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { let middleShift = isXHorizontal ? 0 : -Math.round(MIDDLE_BASELINE_SHIFT * fontSize); let value = rawValue(seriesIdx, dataIdx); + if (opts.negY?.[seriesIdx] && value != null) { + value *= -1; + } + if (value != null) { // Calculate final co-ordinates for text position const x = @@ -380,7 +385,7 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { // Adjust for baseline which is "top" in this case xAdjust = (textMetrics.width * scaleFactor) / 2; - // yAdjust only matters when when the value isn't negative + // yAdjust only matters when the value isn't negative yAdjust = value > 0 ? (textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent) * scaleFactor @@ -516,7 +521,12 @@ export function getConfig(opts: BarsOptions, theme: GrafanaTheme2) { for (const sidx in labels[didx]) { const label = labels[didx][sidx]; - const { text, value, x = 0, y = 0 } = label; + const { text, x = 0, y = 0 } = label; + let { value } = label; + + if (opts.negY?.[sidx] && value != null) { + value *= -1; + } let align: CanvasTextAlign = isXHorizontal ? 'center' : value !== null && value < 0 ? 'right' : 'left'; let baseline: CanvasTextBaseline = isXHorizontal diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index a371f146499..9a199b7e328 100644 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -8,7 +8,7 @@ import { VizOrientation, } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { StackingMode, VisibilityMode } from '@grafana/schema'; +import { GraphTransform, StackingMode, VisibilityMode } from '@grafana/schema'; import { graphFieldOptions, commonOptionsBuilder } from '@grafana/ui'; import { BarChartPanel } from './BarChartPanel'; @@ -63,6 +63,28 @@ export const plugin = new PanelPlugin(BarChartPa }, }); + builder.addSelect({ + category: ['Graph styles'], + name: 'Transform', + path: 'transform', + settings: { + options: [ + { + label: 'Constant', + value: GraphTransform.Constant, + description: 'The first value will be shown as a constant line', + }, + { + label: 'Negative Y', + value: GraphTransform.NegativeY, + description: 'Flip the results to negative values on the y axis', + }, + ], + isClearable: true, + }, + hideFromDefaults: true, + }); + commonOptionsBuilder.addAxisConfig(builder, cfg, false); commonOptionsBuilder.addHideFrom(builder); }, diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index 56c07cb04b3..dfe5222d2dc 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -18,6 +18,7 @@ import { import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames'; import { AxisPlacement, + GraphTransform, ScaleDirection, ScaleDistribution, ScaleOrientation, @@ -107,6 +108,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ legend, xSpacing: xTickLabelSpacing, xTimeAuto: frame.fields[0]?.type === FieldType.time && !frame.fields[0].config.unit?.startsWith('time:'), + negY: frame.fields.map((f) => f.config.custom?.transform === GraphTransform.NegativeY), }; const config = getConfig(opts, theme); From b0cd511ecce5da93689a332dd72f2aab97b2f20c Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Wed, 28 Sep 2022 15:21:21 -0700 Subject: [PATCH 16/62] Geomap: Fix tooltip display option (#55956) --- public/app/core/components/OptionsUI/registry.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/OptionsUI/registry.tsx b/public/app/core/components/OptionsUI/registry.tsx index d9ece7c8451..817287f524d 100644 --- a/public/app/core/components/OptionsUI/registry.tsx +++ b/public/app/core/components/OptionsUI/registry.tsx @@ -81,7 +81,8 @@ export const getAllOptionEditors = () => { name: 'Boolean', description: 'Allows boolean values input', editor(props) { - return props.onChange(e.currentTarget.checked)} />; + const { id, ...rest } = props; // Remove id from properties passed into switch + return props.onChange(e.currentTarget.checked)} />; }, }; From 5a9dfd717307b770cdf5f1535263c1b5fc541b19 Mon Sep 17 00:00:00 2001 From: Nathan Marrs Date: Wed, 28 Sep 2022 15:31:30 -0700 Subject: [PATCH 17/62] Canvas: Fix inline edit toggle crash (#55961) --- public/app/plugins/panel/canvas/CanvasPanel.tsx | 9 +++++---- public/app/plugins/panel/canvas/editor/layerEditor.tsx | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/panel/canvas/CanvasPanel.tsx b/public/app/plugins/panel/canvas/CanvasPanel.tsx index 298a668c126..06350f379cd 100644 --- a/public/app/plugins/panel/canvas/CanvasPanel.tsx +++ b/public/app/plugins/panel/canvas/CanvasPanel.tsx @@ -168,15 +168,16 @@ export class CanvasPanel extends Component { const shouldShowAdvancedTypesSwitched = this.props.options.showAdvancedTypes !== nextProps.options.showAdvancedTypes; if (shouldUpdateSceneAndPanel || inlineEditingSwitched || shouldShowAdvancedTypesSwitched) { + if (inlineEditingSwitched) { + // Replace scene div to prevent selecto instance leaks + this.scene.revId++; + } + this.needsReload = false; this.scene.load(nextProps.options.root, nextProps.options.inlineEditing, nextProps.options.showAdvancedTypes); this.scene.updateSize(nextProps.width, nextProps.height); this.scene.updateData(nextProps.data); changed = true; - - if (inlineEditingSwitched && this.props.options.inlineEditing) { - this.scene.selecto?.destroy(); - } } return changed; diff --git a/public/app/plugins/panel/canvas/editor/layerEditor.tsx b/public/app/plugins/panel/canvas/editor/layerEditor.tsx index 32858b59511..28cf61c56e8 100644 --- a/public/app/plugins/panel/canvas/editor/layerEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/layerEditor.tsx @@ -32,7 +32,7 @@ export function getLayerEditor(opts: InstanceState): NestedPanelOptions Date: Thu, 29 Sep 2022 15:00:49 +0800 Subject: [PATCH 18/62] Fix showWhen failure (#55337) When a contact point configuration field has `showWhen` set on it, the contact point form was not correctly displaying. This change fixes that so that forms display correctly. --- .betterer.results | 8 +++----- .../unified/components/receivers/form/ChannelOptions.tsx | 7 +++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.betterer.results b/.betterer.results index 5dd551c925b..04f8011111b 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3337,11 +3337,9 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "1"] ], "public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx:5381": [ - [0, 0, 0, "Do not use any type assertions.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Unexpected any. Specify a different type.", "4"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"] ], "public/app/features/alerting/unified/components/receivers/form/ReceiverForm.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"], diff --git a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx index 9ca8b98f086..9cdfc9747ea 100644 --- a/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx +++ b/public/app/features/alerting/unified/components/receivers/form/ChannelOptions.tsx @@ -29,14 +29,17 @@ export function ChannelOptions({ readOnly = false, }: Props): JSX.Element { const { watch } = useFormContext>(); - const currentFormValues = watch() as Record; // react hook form types ARE LYING! + const currentFormValues = watch(); // react hook form types ARE LYING! return ( <> {selectedChannelOptions.map((option: NotificationChannelOption, index: number) => { const key = `${option.label}-${index}`; // Some options can be dependent on other options, this determines what is selected in the dependency options // I think this needs more thought. - const selectedOptionValue = currentFormValues[`${pathPrefix}settings.${option.showWhen.field}`]; + // pathPrefix = items.index. + const paths = pathPrefix.split('.'); + const selectedOptionValue = + paths.length >= 2 ? currentFormValues.items[Number(paths[1])].settings[option.showWhen.field] : undefined; if (option.showWhen.field && selectedOptionValue !== option.showWhen.is) { return null; From 998a368c69843c23ea51b2ab747a4556e8f9d32a Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Thu, 29 Sep 2022 10:00:01 +0200 Subject: [PATCH 19/62] Logs: Show copy button independently from context (#55934) --- public/app/features/explore/Logs.tsx | 2 + .../app/features/logs/components/LogRow.tsx | 9 +++++ .../logs/components/LogRowMessage.tsx | 37 ++++++++++--------- .../app/features/logs/components/LogRows.tsx | 34 ++++++++++++++++- .../logs/components/getLogRowStyles.ts | 13 +------ public/app/plugins/panel/logs/LogsPanel.tsx | 2 + 6 files changed, 66 insertions(+), 31 deletions(-) diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 716294a979e..2bd7b3308d3 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -22,6 +22,7 @@ import { LoadingState, SplitOpen, DataQueryResponse, + CoreApp, } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { @@ -475,6 +476,7 @@ class UnthemedLogs extends PureComponent { showDetectedFields={showDetectedFields} onClickShowDetectedField={this.showDetectedField} onClickHideDetectedField={this.hideDetectedField} + app={CoreApp.Explore} />
LogRowModel[]; onClickFilterLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void; @@ -52,6 +55,7 @@ interface Props extends Themeable2 { onClickShowDetectedField?: (key: string) => void; onClickHideDetectedField?: (key: string) => void; onLogRowHover?: (row?: LogRowModel) => void; + toggleContextIsOpen?: () => void; } interface State { @@ -91,6 +95,7 @@ class UnThemedLogRow extends PureComponent { }; toggleContext = () => { + this.props.toggleContextIsOpen?.(); this.setState((state) => { return { showContext: !state.showContext, @@ -132,6 +137,7 @@ class UnThemedLogRow extends PureComponent { row, showDuplicates, showContextToggle, + showRowMenu, showLabels, showTime, showDetectedFields, @@ -141,6 +147,7 @@ class UnThemedLogRow extends PureComponent { getFieldLinks, forceEscape, onLogRowHover, + app, } = this.props; const { showDetails, showContext } = this.state; const style = getLogRowStyles(theme, row.logLevel); @@ -207,9 +214,11 @@ class UnThemedLogRow extends PureComponent { context={context} contextIsOpen={showContext} showContextToggle={showContextToggle} + showRowMenu={showRowMenu} wrapLogMessage={wrapLogMessage} prettifyLogMessage={prettifyLogMessage} onToggleContext={this.toggleContext} + app={app} logsSortOrder={logsSortOrder} /> )} diff --git a/public/app/features/logs/components/LogRowMessage.tsx b/public/app/features/logs/components/LogRowMessage.tsx index f4faff3d214..59ffe09e263 100644 --- a/public/app/features/logs/components/LogRowMessage.tsx +++ b/public/app/features/logs/components/LogRowMessage.tsx @@ -4,7 +4,7 @@ import React, { PureComponent } from 'react'; import Highlighter from 'react-highlight-words'; import tinycolor from 'tinycolor2'; -import { LogRowModel, findHighlightChunksInText, GrafanaTheme2, LogsSortOrder } from '@grafana/data'; +import { LogRowModel, findHighlightChunksInText, GrafanaTheme2, LogsSortOrder, CoreApp } from '@grafana/data'; import { withTheme2, Themeable2, IconButton, Tooltip } from '@grafana/ui'; import { LogMessageAnsi } from './LogMessageAnsi'; @@ -12,8 +12,6 @@ import { LogRowContext } from './LogRowContext'; import { LogRowContextQueryErrors, HasMoreContextRows, LogRowContextRows } from './LogRowContextProvider'; import { getLogRowStyles } from './getLogRowStyles'; -//Components - export const MAX_CHARACTERS = 100000; interface Props extends Themeable2 { @@ -24,6 +22,8 @@ interface Props extends Themeable2 { prettifyLogMessage: boolean; errors?: LogRowContextQueryErrors; context?: LogRowContextRows; + showRowMenu?: boolean; + app?: CoreApp; showContextToggle?: (row?: LogRowModel) => boolean; getRows: () => LogRowModel[]; onToggleContext: () => void; @@ -31,7 +31,7 @@ interface Props extends Themeable2 { logsSortOrder?: LogsSortOrder | null; } -const getStyles = (theme: GrafanaTheme2) => { +const getStyles = (theme: GrafanaTheme2, showContextButton: boolean, isInDashboard: boolean | undefined) => { const outlineColor = tinycolor(theme.components.dashboard.background).setAlpha(0.7).toRgbString(); return { @@ -52,7 +52,7 @@ const getStyles = (theme: GrafanaTheme2) => { display: block; margin-left: 0px; `, - contextButton: css` + rowMenu: css` display: flex; flex-wrap: nowrap; flex-direction: row; @@ -60,15 +60,16 @@ const getStyles = (theme: GrafanaTheme2) => { justify-content: space-evenly; align-items: center; position: absolute; - right: -8px; + right: ${isInDashboard ? '0px' : '-8px'}; top: 0; bottom: auto; - width: 80px; height: 36px; background: ${theme.colors.background.primary}; box-shadow: ${theme.shadows.z3}; padding: ${theme.spacing(0, 0, 0, 0.5)}; z-index: 100; + visibility: hidden; + width: ${showContextButton ? '80px' : '40px'}; `, }; }; @@ -125,17 +126,20 @@ class UnThemedLogRowMessage extends PureComponent { updateLimit, context, contextIsOpen, - showContextToggle, + showRowMenu, wrapLogMessage, prettifyLogMessage, onToggleContext, + app, logsSortOrder, + showContextToggle, } = this.props; const style = getLogRowStyles(theme, row.logLevel); const { hasAnsi, raw } = row; const restructuredEntry = restructureLog(raw, prettifyLogMessage); - const styles = getStyles(theme); + const shouldShowContextToggle = showContextToggle ? showContextToggle(row) : false; + const styles = getStyles(theme, shouldShowContextToggle, app === CoreApp.Dashboard); return ( // When context is open, the position has to be NOT relative. @@ -163,14 +167,13 @@ class UnThemedLogRowMessage extends PureComponent { {renderLogMessage(hasAnsi, restructuredEntry, row.searchWords, style.logsRowMatchHighLight)} - {!contextIsOpen && showContextToggle?.(row) && ( - e.stopPropagation()} - > - - - + {showRowMenu && ( + e.stopPropagation()}> + {shouldShowContextToggle && ( + + + + )} boolean; onClickFilterLabel?: (key: string, value: string) => void; onClickFilterOutLabel?: (key: string, value: string) => void; @@ -37,6 +47,7 @@ export interface Props extends Themeable2 { interface State { renderAll: boolean; + contextIsOpen: boolean; } class UnThemedLogRows extends PureComponent { @@ -48,6 +59,18 @@ class UnThemedLogRows extends PureComponent { state: State = { renderAll: false, + contextIsOpen: false, + }; + + /** + * Toggle the `contextIsOpen` state when a context of one LogRow is opened in order to not show the menu of the other log rows. + */ + toggleContextIsOpen = (): void => { + this.setState((state) => { + return { + contextIsOpen: !state.contextIsOpen, + }; + }); }; componentDidMount() { @@ -100,8 +123,9 @@ class UnThemedLogRows extends PureComponent { onClickHideDetectedField, forceEscape, onLogRowHover, + app, } = this.props; - const { renderAll } = this.state; + const { renderAll, contextIsOpen } = this.state; const { logsRowsTable } = getLogRowStyles(theme); const dedupedRows = deduplicatedRows ? deduplicatedRows : logRows; const hasData = logRows && logRows.length > 0; @@ -130,6 +154,7 @@ class UnThemedLogRows extends PureComponent { getRowContext={getRowContext} row={row} showContextToggle={showContextToggle} + showRowMenu={!contextIsOpen} showDuplicates={showDuplicates} showLabels={showLabels} showTime={showTime} @@ -145,7 +170,9 @@ class UnThemedLogRows extends PureComponent { getFieldLinks={getFieldLinks} logsSortOrder={logsSortOrder} forceEscape={forceEscape} + toggleContextIsOpen={this.toggleContextIsOpen} onLogRowHover={onLogRowHover} + app={app} /> ))} {hasData && @@ -157,6 +184,7 @@ class UnThemedLogRows extends PureComponent { getRowContext={getRowContext} row={row} showContextToggle={showContextToggle} + showRowMenu={!contextIsOpen} showDuplicates={showDuplicates} showLabels={showLabels} showTime={showTime} @@ -172,7 +200,9 @@ class UnThemedLogRows extends PureComponent { getFieldLinks={getFieldLinks} logsSortOrder={logsSortOrder} forceEscape={forceEscape} + toggleContextIsOpen={this.toggleContextIsOpen} onLogRowHover={onLogRowHover} + app={app} /> ))} {hasData && !renderAll && ( diff --git a/public/app/features/logs/components/getLogRowStyles.ts b/public/app/features/logs/components/getLogRowStyles.ts index eccdc8bb892..b6a2b734bd0 100644 --- a/public/app/features/logs/components/getLogRowStyles.ts +++ b/public/app/features/logs/components/getLogRowStyles.ts @@ -45,13 +45,6 @@ export const getLogRowStyles = (theme: GrafanaTheme2, logLevel?: LogLevel) => { font-size: ${theme.typography.bodySmall.fontSize}; width: 100%; `, - context: css` - label: context; - visibility: hidden; - white-space: nowrap; - position: relative; - margin-left: 10px; - `, logsRow: css` label: logs-row; width: 100%; @@ -59,13 +52,9 @@ export const getLogRowStyles = (theme: GrafanaTheme2, logLevel?: LogLevel) => { vertical-align: top; &:hover { - .log-row-context { + .log-row-menu { visibility: visible; z-index: 1; - text-decoration: underline; - &:hover { - color: ${theme.colors.warning.main}; - } } } diff --git a/public/app/plugins/panel/logs/LogsPanel.tsx b/public/app/plugins/panel/logs/LogsPanel.tsx index 8ec083829ea..0a970f00f26 100644 --- a/public/app/plugins/panel/logs/LogsPanel.tsx +++ b/public/app/plugins/panel/logs/LogsPanel.tsx @@ -10,6 +10,7 @@ import { LogRowModel, DataHoverClearEvent, DataHoverEvent, + CoreApp, } from '@grafana/data'; import { CustomScrollbar, useStyles2, usePanelContext } from '@grafana/ui'; import { dataFrameToLogsModel, dedupLogRows, COMMON_LABELS } from 'app/core/logsModel'; @@ -116,6 +117,7 @@ export const LogsPanel: React.FunctionComponent = ({ enableLogDetails={enableLogDetails} previewLimit={isAscending ? logRows.length : undefined} onLogRowHover={onLogRowHover} + app={CoreApp.Dashboard} /> {showCommonLabels && isAscending && renderCommonLabels()} From ebdc13ed861203f4929a75c7f2d3e935355f63c1 Mon Sep 17 00:00:00 2001 From: brendamuir <100768211+brendamuir@users.noreply.github.com> Date: Thu, 29 Sep 2022 09:19:16 +0100 Subject: [PATCH 20/62] Docs alerting: copy edit terraform provisioning (#55978) * Docs alerting: copy edit terraform provisioning * changes intro sentence for explore --- docs/sources/alerting/fundamentals/_index.md | 2 +- .../terraform-provisioning/index.md | 32 +++++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/docs/sources/alerting/fundamentals/_index.md b/docs/sources/alerting/fundamentals/_index.md index 77ae41cb259..56e87c448fa 100644 --- a/docs/sources/alerting/fundamentals/_index.md +++ b/docs/sources/alerting/fundamentals/_index.md @@ -9,7 +9,7 @@ weight: 105 # Explore Grafana Alerting -Whether you're starting or expanding your implementation of Grafana Alerting, learn more about the key concepts and available features that help you create, manage, and take action on your alerts and improve your team’s ability to resolve issues quickly. +Learn about the key concepts and features that help you create, manage, and take action on your alerts and improve your team's ability to resolve issues quickly. - [Data sources](https://grafana.com/docs/grafana/latest/alerting/fundamentals/data-source-alerting/) - [Alert rules](https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/) diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md index 4401baedfeb..a448cd1fb26 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md @@ -96,19 +96,17 @@ EOT } ``` -1. Enter text for your notification in the text field. +2. Enter text for your notification in the text field. The `text` field supports [Go-style templating](https://pkg.go.dev/text/template). This enables you to manage your Grafana Alerting message templates directly in Terraform. -1. Run the command ‘terraform apply’. +3. Run the command ‘terraform apply’. -1. Go to the Grafana UI and check the details of your contact point. - -**Note:** +4. Go to the Grafana UI and check the details of your contact point. You cannot edit resources provisioned via Terraform from the UI. This ensures that your alerting stack always stays in sync with your code. -1. Click **Test** to verify that the contact point works correctly. +5. Click **Test** to verify that the contact point works correctly. **Note:** @@ -172,17 +170,17 @@ contact_point = grafana_contact_point.my_slack_contact_point.name } -1. In the mute_timings field, link a mute timing to your notification policy. +2. In the mute_timings field, link a mute timing to your notification policy. -1. Run the command ‘terraform apply’. +3. Run the command ‘terraform apply’. -1. Go to the Grafana UI and check the details of your notification policy. +4. Go to the Grafana UI and check the details of your notification policy. **Note:** You cannot edit resources provisioned from Terraform from the UI. This ensures that your alerting stack always stays in sync with your code. -1. Click **Test** to verify that the notification point is working correctly. +5. Click **Test** to verify that the notification point is working correctly. ## Provision mute timings @@ -209,16 +207,16 @@ name = "My Mute Timing" } -1. Run the command ‘terraform apply’. -1. Go to the Grafana UI and check the details of your mute timing. -1. Reference your newly created mute timing in a notification policy using the `mute_timings` field. +2. Run the command ‘terraform apply’. +3. Go to the Grafana UI and check the details of your mute timing. +4. Reference your newly created mute timing in a notification policy using the `mute_timings` field. This will apply your mute timing to some or all of your notifications. **Note:** You cannot edit resources provisioned from Terraform from the UI. This ensures that your alerting stack always stays in sync with your code. -1. Click **Test** to verify that the mute timing is working correctly. +5. Click **Test** to verify that the mute timing is working correctly. ## Provision alert rules @@ -243,11 +241,11 @@ resource "grafana_folder" "rule_folder" { } ``` -1. Define an alert rule. +2. Define an alert rule. For more information on alert rules, refer to [how to create Grafana-managed alerts](https://grafana.com/blog/2022/08/01/grafana-alerting-video-how-to-create-alerts-in-grafana-9/). -1. Create a rule group containing one or more rules. +3. Create a rule group containing one or more rules. In this example, the `grafana_rule_group` resource group is used. @@ -314,7 +312,7 @@ EOT } ``` -1. Go to the Grafana UI and check your alert rule. +4. Go to the Grafana UI and check your alert rule. You can see whether or not the alert rule is firing. You can also see a visualization of each of the alert rule’s query stages From 5c5228da617829a4a9861461cd40bc460480df21 Mon Sep 17 00:00:00 2001 From: Jack Westbrook Date: Thu, 29 Sep 2022 10:31:52 +0200 Subject: [PATCH 21/62] Toolkit: Put back publishConfig access public (#55983) --- packages/grafana-toolkit/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/grafana-toolkit/package.json b/packages/grafana-toolkit/package.json index bd42b3a5c25..b67d92b072b 100644 --- a/packages/grafana-toolkit/package.json +++ b/packages/grafana-toolkit/package.json @@ -18,6 +18,9 @@ "bin": { "grafana-toolkit": "./bin/grafana-toolkit.js" }, + "publishConfig": { + "access": "public" + }, "files": [ "config", "src", From 9cf1f2224c7331021634a70e3822e8bd38a81be2 Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Thu, 29 Sep 2022 10:44:15 +0200 Subject: [PATCH 22/62] LogsContext: Added button to load 10 more log lines (#55923) * added load-more button * added missing px --- .../logs/components/LogRowContext.tsx | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/public/app/features/logs/components/LogRowContext.tsx b/public/app/features/logs/components/LogRowContext.tsx index 27ab94d4080..fbdd259aa25 100644 --- a/public/app/features/logs/components/LogRowContext.tsx +++ b/public/app/features/logs/components/LogRowContext.tsx @@ -2,7 +2,7 @@ import { css, cx } from '@emotion/css'; import React, { useRef, useState, useLayoutEffect, useEffect } from 'react'; import { GrafanaTheme2, DataQueryError, LogRowModel, textUtil, LogsSortOrder } from '@grafana/data'; -import { useStyles2, Alert, ClickOutsideWrapper, CustomScrollbar, List } from '@grafana/ui'; +import { useStyles2, Alert, ClickOutsideWrapper, CustomScrollbar, List, Button } from '@grafana/ui'; import { LogMessageAnsi } from './LogMessageAnsi'; import { LogRowContextRows, LogRowContextQueryErrors, HasMoreContextRows } from './LogRowContextProvider'; @@ -31,12 +31,15 @@ const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean) * We also adjust width to 75%. */ + const headerHeight = 40; + const logsHeight = 220; + const contextHeight = headerHeight + logsHeight; const afterContext = wrapLogMessage ? css` - top: -250px; + top: -${contextHeight}px; ` : css` - margin-top: -250px; + margin-top: -${contextHeight}px; width: 75%; `; @@ -51,7 +54,7 @@ const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean) return { commonStyles: css` position: absolute; - height: 250px; + height: ${contextHeight}px; z-index: ${theme.zIndex.dropdown}; overflow: hidden; background: ${theme.colors.background.primary}; @@ -61,14 +64,17 @@ const getLogRowContextStyles = (theme: GrafanaTheme2, wrapLogMessage?: boolean) width: 100%; `, header: css` - height: 30px; + height: ${headerHeight}px; padding: 0 10px; display: flex; align-items: center; background: ${theme.colors.background.secondary}; `, + headerButton: css` + margin-left: 8px; + `, logs: css` - height: 220px; + height: ${logsHeight}px; padding: 10px; `, afterContext, @@ -101,7 +107,7 @@ const LogRowContextGroupHeader: React.FunctionComponent { - const { header } = useStyles2(getLogRowContextStyles); + const { header, headerButton } = useStyles2(getLogRowContextStyles); // determine the position in time for this LogGroup by taking the ordering of // logs and position of the component itself into account. @@ -124,18 +130,9 @@ const LogRowContextGroupHeader: React.FunctionComponent {(rows.length >= 10 || (rows.length > 10 && rows.length % 10 !== 0)) && canLoadMoreRows && ( - - Load 10 more - + )} ); From 21a99fba7e64408d71f62ac5d73b196e136b3ecf Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:17:41 +0200 Subject: [PATCH 23/62] Loki/Prometheus: Fix adding of ad hoc filters when jumping from dashboard to explore (#55915) * Loki, Prometheus: Fix adding of ad hoc filters when jumping from dashbaord to explore * Update --- .../plugins/datasource/loki/datasource.test.ts | 16 ++++++++++++++++ public/app/plugins/datasource/loki/datasource.ts | 2 +- .../datasource/prometheus/datasource.test.ts | 12 ++++++++++++ .../plugins/datasource/prometheus/datasource.tsx | 4 +++- 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index d1db8834080..eda90cbf0b6 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -252,6 +252,22 @@ describe('LokiDatasource', () => { }); }); + describe('when running interpolateVariablesInQueries', () => { + it('should call addAdHocFilters', () => { + const ds = createLokiDatasource(templateSrvStub); + ds.addAdHocFilters = jest.fn(); + const expr = 'rate({bar="baz", job="foo"} [5m]'; + const queries = [ + { + refId: 'A', + expr, + }, + ]; + ds.interpolateVariablesInQueries(queries, {}); + expect(ds.addAdHocFilters).toHaveBeenCalledWith(expr); + }); + }); + describe('when performing testDataSource', () => { let ds: LokiDatasource; beforeEach(() => { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 8aa6b459bb6..dce94ed4b98 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -272,7 +272,7 @@ export class LokiDatasource expandedQueries = queries.map((query) => ({ ...query, datasource: this.getRef(), - expr: this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr), + expr: this.addAdHocFilters(this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr)), })); } diff --git a/public/app/plugins/datasource/prometheus/datasource.test.ts b/public/app/plugins/datasource/prometheus/datasource.test.ts index 1f64f577bd3..9fd9929b08f 100644 --- a/public/app/plugins/datasource/prometheus/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/datasource.test.ts @@ -624,6 +624,18 @@ describe('PrometheusDatasource', () => { expect(templateSrvStub.replace).toBeCalledTimes(2); expect(queries[0].interval).toBe(interval); }); + + it('should call enhanceExprWithAdHocFilters', () => { + ds.enhanceExprWithAdHocFilters = jest.fn(); + const queries = [ + { + refId: 'A', + expr: 'rate({bar="baz", job="foo"} [5m]', + }, + ]; + ds.interpolateVariablesInQueries(queries, {}); + expect(ds.enhanceExprWithAdHocFilters).toHaveBeenCalled(); + }); }); describe('applyTemplateVariables', () => { diff --git a/public/app/plugins/datasource/prometheus/datasource.tsx b/public/app/plugins/datasource/prometheus/datasource.tsx index 9e35aaa9299..41fa69a5faf 100644 --- a/public/app/plugins/datasource/prometheus/datasource.tsx +++ b/public/app/plugins/datasource/prometheus/datasource.tsx @@ -1001,7 +1001,9 @@ export class PrometheusDatasource const expandedQuery = { ...query, datasource: this.getRef(), - expr: this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr), + expr: this.enhanceExprWithAdHocFilters( + this.templateSrv.replace(query.expr, scopedVars, this.interpolateQueryExpr) + ), interval: this.templateSrv.replace(query.interval, scopedVars), }; return expandedQuery; From c4500438a46f4350374c8150fede8b5f75c07e1b Mon Sep 17 00:00:00 2001 From: Alexa V <239999+axelavargas@users.noreply.github.com> Date: Thu, 29 Sep 2022 12:13:07 +0200 Subject: [PATCH 24/62] Variable: Limit variable name to 50 characters (#55406) Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> Co-authored-by: kay delaney --- .../app/features/variables/editor/VariableEditorEditor.tsx | 6 ++++-- public/app/features/variables/editor/VariableTextField.tsx | 3 +++ public/app/features/variables/editor/types.ts | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/public/app/features/variables/editor/VariableEditorEditor.tsx b/public/app/features/variables/editor/VariableEditorEditor.tsx index c427b483630..f1887042616 100644 --- a/public/app/features/variables/editor/VariableEditorEditor.tsx +++ b/public/app/features/variables/editor/VariableEditorEditor.tsx @@ -26,7 +26,7 @@ import { VariableTextField } from './VariableTextField'; import { VariableTypeSelect } from './VariableTypeSelect'; import { VariableValuesPreview } from './VariableValuesPreview'; import { changeVariableName, variableEditorMount, variableEditorUnMount } from './actions'; -import { OnPropChangeArguments } from './types'; +import { OnPropChangeArguments, VariableNameConstraints } from './types'; const mapStateToProps = (state: StoreState, ownProps: OwnProps) => ({ editor: getVariablesState(ownProps.identifier.rootStateKey, state).editor, @@ -149,8 +149,10 @@ export class VariableEditorEditorUnConnected extends PureComponent { onChange={this.onNameChange} name="Name" placeholder="name" - required testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2} + maxLength={VariableNameConstraints.MaxSize} + required + tooltip="Variable name cannot be longer than 50 characters" /> diff --git a/public/app/features/variables/editor/VariableTextField.tsx b/public/app/features/variables/editor/VariableTextField.tsx index 05b3a4a92a2..782c96502fa 100644 --- a/public/app/features/variables/editor/VariableTextField.tsx +++ b/public/app/features/variables/editor/VariableTextField.tsx @@ -15,6 +15,7 @@ interface VariableTextFieldProps { grow?: boolean; onBlur?: (event: FormEvent) => void; interactive?: boolean; + maxLength?: number; } export function VariableTextField({ @@ -30,6 +31,7 @@ export function VariableTextField({ tooltip, grow, interactive, + maxLength, }: PropsWithChildren): ReactElement { return ( @@ -43,6 +45,7 @@ export function VariableTextField({ onBlur={onBlur} width={grow ? undefined : width ?? 25} data-testid={testId} + maxLength={maxLength} required={required} /> diff --git a/public/app/features/variables/editor/types.ts b/public/app/features/variables/editor/types.ts index 419ab1be7db..fa86b3111ae 100644 --- a/public/app/features/variables/editor/types.ts +++ b/public/app/features/variables/editor/types.ts @@ -1,5 +1,9 @@ import { VariableModel } from '../types'; +export enum VariableNameConstraints { + MaxSize = 50, +} + export interface OnPropChangeArguments { propName: keyof Model; propValue: any; From 836b3b4dd62834f439df4b6940a69b7bc102f7da Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Sep 2022 11:22:01 +0100 Subject: [PATCH 25/62] Update dependency rc-tree to v5.7.0 (#55902) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 7dfec4ca718..cf3a72318f5 100644 --- a/package.json +++ b/package.json @@ -353,7 +353,7 @@ "rc-drawer": "4.4.3", "rc-slider": "9.7.5", "rc-time-picker": "3.7.3", - "rc-tree": "5.6.6", + "rc-tree": "5.7.0", "re-resizable": "6.9.9", "react": "17.0.2", "react-awesome-query-builder": "5.3.1", diff --git a/yarn.lock b/yarn.lock index 50e0bdf5d04..6e6ece2d192 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22638,7 +22638,7 @@ __metadata: rc-drawer: 4.4.3 rc-slider: 9.7.5 rc-time-picker: 3.7.3 - rc-tree: 5.6.6 + rc-tree: 5.7.0 re-resizable: 6.9.9 react: 17.0.2 react-awesome-query-builder: 5.3.1 @@ -32220,9 +32220,9 @@ __metadata: languageName: node linkType: hard -"rc-tree@npm:5.6.6": - version: 5.6.6 - resolution: "rc-tree@npm:5.6.6" +"rc-tree@npm:5.7.0": + version: 5.7.0 + resolution: "rc-tree@npm:5.7.0" dependencies: "@babel/runtime": ^7.10.1 classnames: 2.x @@ -32232,7 +32232,7 @@ __metadata: peerDependencies: react: "*" react-dom: "*" - checksum: 9584413b9b22d845fde70723db4994fe01982b6753bd02f154947c18e8187e3c276533b0c888dc41d813315dc91a40b0cf4dba9b10067f914f1deecdb7219677 + checksum: 0bcab3a11615ca69386077841d63337206ddb6dcb7284b6cfbc53d39b450e74f5e3e0f43b190e445182321bb6052a9b46dacc566a6a564e8ba2d65e83b99ffcb languageName: node linkType: hard From 27abc32608a9ac8235b85601482feadb7751bfbc Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 29 Sep 2022 11:52:33 +0100 Subject: [PATCH 26/62] fix lockfile (#56003) --- yarn.lock | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6e6ece2d192..e52481b8ec6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -32220,23 +32220,7 @@ __metadata: languageName: node linkType: hard -"rc-tree@npm:5.7.0": - version: 5.7.0 - resolution: "rc-tree@npm:5.7.0" - dependencies: - "@babel/runtime": ^7.10.1 - classnames: 2.x - rc-motion: ^2.0.1 - rc-util: ^5.16.1 - rc-virtual-list: ^3.4.8 - peerDependencies: - react: "*" - react-dom: "*" - checksum: 0bcab3a11615ca69386077841d63337206ddb6dcb7284b6cfbc53d39b450e74f5e3e0f43b190e445182321bb6052a9b46dacc566a6a564e8ba2d65e83b99ffcb - languageName: node - linkType: hard - -"rc-tree@npm:~5.7.0": +"rc-tree@npm:5.7.0, rc-tree@npm:~5.7.0": version: 5.7.0 resolution: "rc-tree@npm:5.7.0" dependencies: From 34f18aacd6fbfefe3758769d7769029395832895 Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Thu, 29 Sep 2022 15:11:53 +0400 Subject: [PATCH 27/62] Chore: add more input parameters to `serviceaccounts/test/common.go` (#55994) --- pkg/services/serviceaccounts/tests/common.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index 788d6675629..a82dbcb0016 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -20,14 +20,16 @@ type TestUser struct { Role string Login string IsServiceAccount bool + OrgID int64 } type TestApiKey struct { - Name string - Role org.RoleType - OrgId int64 - Key string - IsExpired bool + Name string + Role org.RoleType + OrgId int64 + Key string + IsExpired bool + ServiceAccountID *int64 } func SetupUserServiceAccount(t *testing.T, sqlStore *sqlstore.SQLStore, testUser TestUser) *user.User { @@ -41,6 +43,7 @@ func SetupUserServiceAccount(t *testing.T, sqlStore *sqlstore.SQLStore, testUser IsServiceAccount: testUser.IsServiceAccount, DefaultOrgRole: role, Name: testUser.Name, + OrgID: testUser.OrgID, }) require.NoError(t, err) return u1 @@ -53,9 +56,10 @@ func SetupApiKey(t *testing.T, sqlStore *sqlstore.SQLStore, testKey TestApiKey) } addKeyCmd := &apikey.AddCommand{ - Name: testKey.Name, - Role: role, - OrgId: testKey.OrgId, + Name: testKey.Name, + Role: role, + OrgId: testKey.OrgId, + ServiceAccountID: testKey.ServiceAccountID, } if testKey.Key != "" { From b4f73c9f09e738265c3ac9ef961fee4ed7b9b2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 29 Sep 2022 13:27:51 +0200 Subject: [PATCH 28/62] PluginPages: Support plugin pages that don't belong to a section (#55904) * Fixing pages that don't exist in navtree * Fix test * fix lint warning * Fixes --- pkg/services/navtree/navtreeimpl/applinks.go | 2 +- public/app/core/components/PageNew/Page.tsx | 2 +- .../core/components/PageNew/SectionNav.tsx | 4 ++ .../plugins/components/AppRootPage.tsx | 15 +----- .../plugins/components/PluginPageContext.tsx | 2 +- public/app/features/plugins/utils.test.ts | 10 ++-- public/app/features/plugins/utils.ts | 54 +++++++++++-------- 7 files changed, 44 insertions(+), 45 deletions(-) diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go index d6a8978f759..88b52e62b5b 100644 --- a/pkg/services/navtree/navtreeimpl/applinks.go +++ b/pkg/services/navtree/navtreeimpl/applinks.go @@ -85,7 +85,7 @@ func (s *ServiceImpl) processAppPlugin(plugin plugins.PluginDTO, c *models.ReqCo SortWeight: navtree.WeightPlugin, } - if s.features.IsEnabled(featuremgmt.FlagTopnav) { + if topNavEnabled { appLink.Url = s.cfg.AppSubURL + "/a/" + plugin.ID } else { appLink.Url = path.Join(s.cfg.AppSubURL, plugin.DefaultNavURL) diff --git a/public/app/core/components/PageNew/Page.tsx b/public/app/core/components/PageNew/Page.tsx index 1ad41a503b4..4e39f9a8701 100644 --- a/public/app/core/components/PageNew/Page.tsx +++ b/public/app/core/components/PageNew/Page.tsx @@ -50,7 +50,7 @@ export const Page: PageType = ({
{layout === PageLayoutType.Standard && (
- {navModel && navModel.main.children && } + {navModel && }
diff --git a/public/app/core/components/PageNew/SectionNav.tsx b/public/app/core/components/PageNew/SectionNav.tsx index 59c31a5bde9..58d2fc57669 100644 --- a/public/app/core/components/PageNew/SectionNav.tsx +++ b/public/app/core/components/PageNew/SectionNav.tsx @@ -13,6 +13,10 @@ export interface Props { export function SectionNav({ model }: Props) { const styles = useStyles2(getStyles); + if (!Boolean(model.main?.children?.length)) { + return null; + } + return (