From 2eaeff8ea7cd1133c1cfbbb012a30ad70fb6ecbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Jim=C3=A9nez=20S=C3=A1nchez?= Date: Thu, 10 Apr 2025 19:57:42 +0200 Subject: [PATCH 01/37] Provisioning: Fix issue with double prefix for github repository (#103781) --- .../provisioning/jobs/sync/incremental.go | 2 +- .../jobs/sync/incremental_test.go | 2 +- .../apis/provisioning/repository/github.go | 76 +- .../provisioning/repository/github/client.go | 3 + .../repository/github/mock_client.go | 1216 +++++++++++++++++ .../repository/github/mock_commit_file.go | 212 +++ .../github/mock_repository_content.go | 312 +++++ .../provisioning/repository/github_test.go | 101 ++ .../provisioning/repository/local_test.go | 3 + .../apis/provisioning/safepath/dir.go | 27 + .../apis/provisioning/safepath/dir_test.go | 98 ++ 11 files changed, 2038 insertions(+), 14 deletions(-) create mode 100644 pkg/registry/apis/provisioning/repository/github/mock_client.go create mode 100644 pkg/registry/apis/provisioning/repository/github/mock_commit_file.go create mode 100644 pkg/registry/apis/provisioning/repository/github/mock_repository_content.go diff --git a/pkg/registry/apis/provisioning/jobs/sync/incremental.go b/pkg/registry/apis/provisioning/jobs/sync/incremental.go index 43aafada339..9bbfdb43921 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/incremental.go +++ b/pkg/registry/apis/provisioning/jobs/sync/incremental.go @@ -92,7 +92,7 @@ func IncrementalSync(ctx context.Context, repo repository.Versioned, previousRef result.Resource = gvk.Kind result.Group = gvk.Group case repository.FileActionRenamed: - name, gvk, err := repositoryResources.RenameResourceFile(ctx, change.Path, change.PreviousRef, change.Path, change.Ref) + name, gvk, err := repositoryResources.RenameResourceFile(ctx, change.PreviousPath, change.PreviousRef, change.Path, change.Ref) if err != nil { result.Error = err } diff --git a/pkg/registry/apis/provisioning/jobs/sync/incremental_test.go b/pkg/registry/apis/provisioning/jobs/sync/incremental_test.go index 03c75d898e5..8cf8906470b 100644 --- a/pkg/registry/apis/provisioning/jobs/sync/incremental_test.go +++ b/pkg/registry/apis/provisioning/jobs/sync/incremental_test.go @@ -220,7 +220,7 @@ func TestIncrementalSync(t *testing.T) { progress.On("SetMessage", mock.Anything, "versioned changes replicated").Return() // Mock resource rename - repoResources.On("RenameResourceFile", mock.Anything, "dashboards/new.json", "old-ref", "dashboards/new.json", "new-ref"). + repoResources.On("RenameResourceFile", mock.Anything, "dashboards/old.json", "old-ref", "dashboards/new.json", "new-ref"). Return("renamed-dashboard", schema.GroupVersionKind{Kind: "Dashboard", Group: "dashboards"}, nil) // Mock progress recording diff --git a/pkg/registry/apis/provisioning/repository/github.go b/pkg/registry/apis/provisioning/repository/github.go index 4e8e75ba472..7da5d961409 100644 --- a/pkg/registry/apis/provisioning/repository/github.go +++ b/pkg/registry/apis/provisioning/repository/github.go @@ -262,8 +262,15 @@ func (r *githubRepository) ReadTree(ctx context.Context, ref string) ([]FileTree entries := make([]FileTreeEntry, 0, len(tree)) for _, entry := range tree { + isBlob := !entry.IsDirectory() + // FIXME: this we could potentially do somewhere else on in a different way + filePath := entry.GetPath() + if !isBlob && !safepath.IsDir(filePath) { + filePath = filePath + "/" + } + converted := FileTreeEntry{ - Path: entry.GetPath(), + Path: filePath, Size: entry.GetSize(), Hash: entry.GetSHA(), Blob: !entry.IsDirectory(), @@ -672,30 +679,75 @@ func (r *githubRepository) CompareFiles(ctx context.Context, base, ref string) ( // reference: https://docs.github.com/en/rest/commits/commits?apiVersion=2022-11-28#get-a-commit switch f.GetStatus() { case "added", "copied": + currentPath, err := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path) + if err != nil { + // do nothing as it's outside of configured path + continue + } + changes = append(changes, VersionedFileChange{ - Path: f.GetFilename(), + Path: currentPath, Ref: ref, Action: FileActionCreated, }) case "modified", "changed": + currentPath, err := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path) + if err != nil { + // do nothing as it's outside of configured path + continue + } + changes = append(changes, VersionedFileChange{ - Path: f.GetFilename(), + Path: currentPath, Ref: ref, Action: FileActionUpdated, }) case "renamed": + previousPath, previousErr := safepath.RelativeTo(f.GetPreviousFilename(), r.config.Spec.GitHub.Path) + currentPath, currentErr := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path) + + // Handle all possible combinations of path validation results: + // 1. Both paths outside configured path, do nothing + // 2. Both paths inside configured path, rename + // 3. Moving out of configured path, delete previous file + // 4. Moving into configured path, create new file + switch { + case previousErr != nil && currentErr != nil: + // do nothing as it's outside of configured path + case previousErr == nil && currentErr == nil: + changes = append(changes, VersionedFileChange{ + Path: currentPath, + PreviousPath: previousPath, + Ref: ref, + PreviousRef: base, + Action: FileActionRenamed, + }) + case previousErr == nil && currentErr != nil: + changes = append(changes, VersionedFileChange{ + Path: currentPath, + Ref: ref, + Action: FileActionDeleted, + }) + case previousErr != nil && currentErr == nil: + changes = append(changes, VersionedFileChange{ + Path: currentPath, + Ref: ref, + Action: FileActionCreated, + }) + } + case "removed": + currentPath, err := safepath.RelativeTo(f.GetFilename(), r.config.Spec.GitHub.Path) + if err != nil { + // do nothing as it's outside of configured path + continue + } + changes = append(changes, VersionedFileChange{ - Path: f.GetFilename(), - PreviousPath: f.GetPreviousFilename(), Ref: ref, PreviousRef: base, - Action: FileActionRenamed, - }) - case "removed": - changes = append(changes, VersionedFileChange{ - Ref: base, - Path: f.GetFilename(), - Action: FileActionDeleted, + Path: currentPath, + PreviousPath: currentPath, + Action: FileActionDeleted, }) case "unchanged": // do nothing diff --git a/pkg/registry/apis/provisioning/repository/github/client.go b/pkg/registry/apis/provisioning/repository/github/client.go index 3afe76b3172..943057463cc 100644 --- a/pkg/registry/apis/provisioning/repository/github/client.go +++ b/pkg/registry/apis/provisioning/repository/github/client.go @@ -29,6 +29,7 @@ const MaxFileSize = 10 * 1024 * 1024 // 10MB in bytes type ErrRateLimited = github.RateLimitError +//go:generate mockery --name Client --structname MockClient --inpackage --filename mock_client.go --with-expecter type Client interface { // IsAuthenticated checks if the client is authenticated. IsAuthenticated(ctx context.Context) error @@ -103,6 +104,7 @@ type Client interface { ClearAllPullRequestFileComments(ctx context.Context, owner, repository string, number int) error } +//go:generate mockery --name RepositoryContent --structname MockRepositoryContent --inpackage --filename mock_repository_content.go --with-expecter type RepositoryContent interface { // Returns true if this is a directory, false if it is a file. IsDirectory() bool @@ -142,6 +144,7 @@ type Commit struct { CreatedAt time.Time } +//go:generate mockery --name CommitFile --structname MockCommitFile --inpackage --filename mock_commit_file.go --with-expecter type CommitFile interface { GetSHA() string GetFilename() string diff --git a/pkg/registry/apis/provisioning/repository/github/mock_client.go b/pkg/registry/apis/provisioning/repository/github/mock_client.go new file mode 100644 index 00000000000..a564d1c4d52 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/mock_client.go @@ -0,0 +1,1216 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package github + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// MockClient is an autogenerated mock type for the Client type +type MockClient struct { + mock.Mock +} + +type MockClient_Expecter struct { + mock *mock.Mock +} + +func (_m *MockClient) EXPECT() *MockClient_Expecter { + return &MockClient_Expecter{mock: &_m.Mock} +} + +// BranchExists provides a mock function with given fields: ctx, owner, repository, branchName +func (_m *MockClient) BranchExists(ctx context.Context, owner string, repository string, branchName string) (bool, error) { + ret := _m.Called(ctx, owner, repository, branchName) + + if len(ret) == 0 { + panic("no return value specified for BranchExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (bool, error)); ok { + return rf(ctx, owner, repository, branchName) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) bool); ok { + r0 = rf(ctx, owner, repository, branchName) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { + r1 = rf(ctx, owner, repository, branchName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_BranchExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BranchExists' +type MockClient_BranchExists_Call struct { + *mock.Call +} + +// BranchExists is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - branchName string +func (_e *MockClient_Expecter) BranchExists(ctx interface{}, owner interface{}, repository interface{}, branchName interface{}) *MockClient_BranchExists_Call { + return &MockClient_BranchExists_Call{Call: _e.mock.On("BranchExists", ctx, owner, repository, branchName)} +} + +func (_c *MockClient_BranchExists_Call) Run(run func(ctx context.Context, owner string, repository string, branchName string)) *MockClient_BranchExists_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) + }) + return _c +} + +func (_c *MockClient_BranchExists_Call) Return(_a0 bool, _a1 error) *MockClient_BranchExists_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_BranchExists_Call) RunAndReturn(run func(context.Context, string, string, string) (bool, error)) *MockClient_BranchExists_Call { + _c.Call.Return(run) + return _c +} + +// ClearAllPullRequestFileComments provides a mock function with given fields: ctx, owner, repository, number +func (_m *MockClient) ClearAllPullRequestFileComments(ctx context.Context, owner string, repository string, number int) error { + ret := _m.Called(ctx, owner, repository, number) + + if len(ret) == 0 { + panic("no return value specified for ClearAllPullRequestFileComments") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, int) error); ok { + r0 = rf(ctx, owner, repository, number) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_ClearAllPullRequestFileComments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ClearAllPullRequestFileComments' +type MockClient_ClearAllPullRequestFileComments_Call struct { + *mock.Call +} + +// ClearAllPullRequestFileComments is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - number int +func (_e *MockClient_Expecter) ClearAllPullRequestFileComments(ctx interface{}, owner interface{}, repository interface{}, number interface{}) *MockClient_ClearAllPullRequestFileComments_Call { + return &MockClient_ClearAllPullRequestFileComments_Call{Call: _e.mock.On("ClearAllPullRequestFileComments", ctx, owner, repository, number)} +} + +func (_c *MockClient_ClearAllPullRequestFileComments_Call) Run(run func(ctx context.Context, owner string, repository string, number int)) *MockClient_ClearAllPullRequestFileComments_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(int)) + }) + return _c +} + +func (_c *MockClient_ClearAllPullRequestFileComments_Call) Return(_a0 error) *MockClient_ClearAllPullRequestFileComments_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_ClearAllPullRequestFileComments_Call) RunAndReturn(run func(context.Context, string, string, int) error) *MockClient_ClearAllPullRequestFileComments_Call { + _c.Call.Return(run) + return _c +} + +// Commits provides a mock function with given fields: ctx, owner, repository, path, branch +func (_m *MockClient) Commits(ctx context.Context, owner string, repository string, path string, branch string) ([]Commit, error) { + ret := _m.Called(ctx, owner, repository, path, branch) + + if len(ret) == 0 { + panic("no return value specified for Commits") + } + + var r0 []Commit + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) ([]Commit, error)); ok { + return rf(ctx, owner, repository, path, branch) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) []Commit); ok { + r0 = rf(ctx, owner, repository, path, branch) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Commit) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok { + r1 = rf(ctx, owner, repository, path, branch) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_Commits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Commits' +type MockClient_Commits_Call struct { + *mock.Call +} + +// Commits is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - path string +// - branch string +func (_e *MockClient_Expecter) Commits(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}) *MockClient_Commits_Call { + return &MockClient_Commits_Call{Call: _e.mock.On("Commits", ctx, owner, repository, path, branch)} +} + +func (_c *MockClient_Commits_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string)) *MockClient_Commits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) + }) + return _c +} + +func (_c *MockClient_Commits_Call) Return(_a0 []Commit, _a1 error) *MockClient_Commits_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_Commits_Call) RunAndReturn(run func(context.Context, string, string, string, string) ([]Commit, error)) *MockClient_Commits_Call { + _c.Call.Return(run) + return _c +} + +// CompareCommits provides a mock function with given fields: ctx, owner, repository, base, head +func (_m *MockClient) CompareCommits(ctx context.Context, owner string, repository string, base string, head string) ([]CommitFile, error) { + ret := _m.Called(ctx, owner, repository, base, head) + + if len(ret) == 0 { + panic("no return value specified for CompareCommits") + } + + var r0 []CommitFile + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) ([]CommitFile, error)); ok { + return rf(ctx, owner, repository, base, head) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) []CommitFile); ok { + r0 = rf(ctx, owner, repository, base, head) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]CommitFile) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok { + r1 = rf(ctx, owner, repository, base, head) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_CompareCommits_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CompareCommits' +type MockClient_CompareCommits_Call struct { + *mock.Call +} + +// CompareCommits is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - base string +// - head string +func (_e *MockClient_Expecter) CompareCommits(ctx interface{}, owner interface{}, repository interface{}, base interface{}, head interface{}) *MockClient_CompareCommits_Call { + return &MockClient_CompareCommits_Call{Call: _e.mock.On("CompareCommits", ctx, owner, repository, base, head)} +} + +func (_c *MockClient_CompareCommits_Call) Run(run func(ctx context.Context, owner string, repository string, base string, head string)) *MockClient_CompareCommits_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) + }) + return _c +} + +func (_c *MockClient_CompareCommits_Call) Return(_a0 []CommitFile, _a1 error) *MockClient_CompareCommits_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_CompareCommits_Call) RunAndReturn(run func(context.Context, string, string, string, string) ([]CommitFile, error)) *MockClient_CompareCommits_Call { + _c.Call.Return(run) + return _c +} + +// CreateBranch provides a mock function with given fields: ctx, owner, repository, sourceBranch, branchName +func (_m *MockClient) CreateBranch(ctx context.Context, owner string, repository string, sourceBranch string, branchName string) error { + ret := _m.Called(ctx, owner, repository, sourceBranch, branchName) + + if len(ret) == 0 { + panic("no return value specified for CreateBranch") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) error); ok { + r0 = rf(ctx, owner, repository, sourceBranch, branchName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_CreateBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBranch' +type MockClient_CreateBranch_Call struct { + *mock.Call +} + +// CreateBranch is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - sourceBranch string +// - branchName string +func (_e *MockClient_Expecter) CreateBranch(ctx interface{}, owner interface{}, repository interface{}, sourceBranch interface{}, branchName interface{}) *MockClient_CreateBranch_Call { + return &MockClient_CreateBranch_Call{Call: _e.mock.On("CreateBranch", ctx, owner, repository, sourceBranch, branchName)} +} + +func (_c *MockClient_CreateBranch_Call) Run(run func(ctx context.Context, owner string, repository string, sourceBranch string, branchName string)) *MockClient_CreateBranch_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) + }) + return _c +} + +func (_c *MockClient_CreateBranch_Call) Return(_a0 error) *MockClient_CreateBranch_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_CreateBranch_Call) RunAndReturn(run func(context.Context, string, string, string, string) error) *MockClient_CreateBranch_Call { + _c.Call.Return(run) + return _c +} + +// CreateFile provides a mock function with given fields: ctx, owner, repository, path, branch, message, content +func (_m *MockClient) CreateFile(ctx context.Context, owner string, repository string, path string, branch string, message string, content []byte) error { + ret := _m.Called(ctx, owner, repository, path, branch, message, content) + + if len(ret) == 0 { + panic("no return value specified for CreateFile") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, []byte) error); ok { + r0 = rf(ctx, owner, repository, path, branch, message, content) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_CreateFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateFile' +type MockClient_CreateFile_Call struct { + *mock.Call +} + +// CreateFile is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - path string +// - branch string +// - message string +// - content []byte +func (_e *MockClient_Expecter) CreateFile(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}, message interface{}, content interface{}) *MockClient_CreateFile_Call { + return &MockClient_CreateFile_Call{Call: _e.mock.On("CreateFile", ctx, owner, repository, path, branch, message, content)} +} + +func (_c *MockClient_CreateFile_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string, message string, content []byte)) *MockClient_CreateFile_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].([]byte)) + }) + return _c +} + +func (_c *MockClient_CreateFile_Call) Return(_a0 error) *MockClient_CreateFile_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_CreateFile_Call) RunAndReturn(run func(context.Context, string, string, string, string, string, []byte) error) *MockClient_CreateFile_Call { + _c.Call.Return(run) + return _c +} + +// CreatePullRequestComment provides a mock function with given fields: ctx, owner, repository, number, body +func (_m *MockClient) CreatePullRequestComment(ctx context.Context, owner string, repository string, number int, body string) error { + ret := _m.Called(ctx, owner, repository, number, body) + + if len(ret) == 0 { + panic("no return value specified for CreatePullRequestComment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, int, string) error); ok { + r0 = rf(ctx, owner, repository, number, body) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_CreatePullRequestComment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePullRequestComment' +type MockClient_CreatePullRequestComment_Call struct { + *mock.Call +} + +// CreatePullRequestComment is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - number int +// - body string +func (_e *MockClient_Expecter) CreatePullRequestComment(ctx interface{}, owner interface{}, repository interface{}, number interface{}, body interface{}) *MockClient_CreatePullRequestComment_Call { + return &MockClient_CreatePullRequestComment_Call{Call: _e.mock.On("CreatePullRequestComment", ctx, owner, repository, number, body)} +} + +func (_c *MockClient_CreatePullRequestComment_Call) Run(run func(ctx context.Context, owner string, repository string, number int, body string)) *MockClient_CreatePullRequestComment_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(int), args[4].(string)) + }) + return _c +} + +func (_c *MockClient_CreatePullRequestComment_Call) Return(_a0 error) *MockClient_CreatePullRequestComment_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_CreatePullRequestComment_Call) RunAndReturn(run func(context.Context, string, string, int, string) error) *MockClient_CreatePullRequestComment_Call { + _c.Call.Return(run) + return _c +} + +// CreatePullRequestFileComment provides a mock function with given fields: ctx, owner, repository, number, comment +func (_m *MockClient) CreatePullRequestFileComment(ctx context.Context, owner string, repository string, number int, comment FileComment) error { + ret := _m.Called(ctx, owner, repository, number, comment) + + if len(ret) == 0 { + panic("no return value specified for CreatePullRequestFileComment") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, int, FileComment) error); ok { + r0 = rf(ctx, owner, repository, number, comment) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_CreatePullRequestFileComment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreatePullRequestFileComment' +type MockClient_CreatePullRequestFileComment_Call struct { + *mock.Call +} + +// CreatePullRequestFileComment is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - number int +// - comment FileComment +func (_e *MockClient_Expecter) CreatePullRequestFileComment(ctx interface{}, owner interface{}, repository interface{}, number interface{}, comment interface{}) *MockClient_CreatePullRequestFileComment_Call { + return &MockClient_CreatePullRequestFileComment_Call{Call: _e.mock.On("CreatePullRequestFileComment", ctx, owner, repository, number, comment)} +} + +func (_c *MockClient_CreatePullRequestFileComment_Call) Run(run func(ctx context.Context, owner string, repository string, number int, comment FileComment)) *MockClient_CreatePullRequestFileComment_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(int), args[4].(FileComment)) + }) + return _c +} + +func (_c *MockClient_CreatePullRequestFileComment_Call) Return(_a0 error) *MockClient_CreatePullRequestFileComment_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_CreatePullRequestFileComment_Call) RunAndReturn(run func(context.Context, string, string, int, FileComment) error) *MockClient_CreatePullRequestFileComment_Call { + _c.Call.Return(run) + return _c +} + +// CreateWebhook provides a mock function with given fields: ctx, owner, repository, cfg +func (_m *MockClient) CreateWebhook(ctx context.Context, owner string, repository string, cfg WebhookConfig) (WebhookConfig, error) { + ret := _m.Called(ctx, owner, repository, cfg) + + if len(ret) == 0 { + panic("no return value specified for CreateWebhook") + } + + var r0 WebhookConfig + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, WebhookConfig) (WebhookConfig, error)); ok { + return rf(ctx, owner, repository, cfg) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, WebhookConfig) WebhookConfig); ok { + r0 = rf(ctx, owner, repository, cfg) + } else { + r0 = ret.Get(0).(WebhookConfig) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, WebhookConfig) error); ok { + r1 = rf(ctx, owner, repository, cfg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_CreateWebhook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateWebhook' +type MockClient_CreateWebhook_Call struct { + *mock.Call +} + +// CreateWebhook is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - cfg WebhookConfig +func (_e *MockClient_Expecter) CreateWebhook(ctx interface{}, owner interface{}, repository interface{}, cfg interface{}) *MockClient_CreateWebhook_Call { + return &MockClient_CreateWebhook_Call{Call: _e.mock.On("CreateWebhook", ctx, owner, repository, cfg)} +} + +func (_c *MockClient_CreateWebhook_Call) Run(run func(ctx context.Context, owner string, repository string, cfg WebhookConfig)) *MockClient_CreateWebhook_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(WebhookConfig)) + }) + return _c +} + +func (_c *MockClient_CreateWebhook_Call) Return(_a0 WebhookConfig, _a1 error) *MockClient_CreateWebhook_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_CreateWebhook_Call) RunAndReturn(run func(context.Context, string, string, WebhookConfig) (WebhookConfig, error)) *MockClient_CreateWebhook_Call { + _c.Call.Return(run) + return _c +} + +// DeleteFile provides a mock function with given fields: ctx, owner, repository, path, branch, message, hash +func (_m *MockClient) DeleteFile(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string) error { + ret := _m.Called(ctx, owner, repository, path, branch, message, hash) + + if len(ret) == 0 { + panic("no return value specified for DeleteFile") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, string) error); ok { + r0 = rf(ctx, owner, repository, path, branch, message, hash) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_DeleteFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteFile' +type MockClient_DeleteFile_Call struct { + *mock.Call +} + +// DeleteFile is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - path string +// - branch string +// - message string +// - hash string +func (_e *MockClient_Expecter) DeleteFile(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}, message interface{}, hash interface{}) *MockClient_DeleteFile_Call { + return &MockClient_DeleteFile_Call{Call: _e.mock.On("DeleteFile", ctx, owner, repository, path, branch, message, hash)} +} + +func (_c *MockClient_DeleteFile_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string)) *MockClient_DeleteFile_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].(string)) + }) + return _c +} + +func (_c *MockClient_DeleteFile_Call) Return(_a0 error) *MockClient_DeleteFile_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_DeleteFile_Call) RunAndReturn(run func(context.Context, string, string, string, string, string, string) error) *MockClient_DeleteFile_Call { + _c.Call.Return(run) + return _c +} + +// DeleteWebhook provides a mock function with given fields: ctx, owner, repository, webhookID +func (_m *MockClient) DeleteWebhook(ctx context.Context, owner string, repository string, webhookID int64) error { + ret := _m.Called(ctx, owner, repository, webhookID) + + if len(ret) == 0 { + panic("no return value specified for DeleteWebhook") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) error); ok { + r0 = rf(ctx, owner, repository, webhookID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_DeleteWebhook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteWebhook' +type MockClient_DeleteWebhook_Call struct { + *mock.Call +} + +// DeleteWebhook is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - webhookID int64 +func (_e *MockClient_Expecter) DeleteWebhook(ctx interface{}, owner interface{}, repository interface{}, webhookID interface{}) *MockClient_DeleteWebhook_Call { + return &MockClient_DeleteWebhook_Call{Call: _e.mock.On("DeleteWebhook", ctx, owner, repository, webhookID)} +} + +func (_c *MockClient_DeleteWebhook_Call) Run(run func(ctx context.Context, owner string, repository string, webhookID int64)) *MockClient_DeleteWebhook_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(int64)) + }) + return _c +} + +func (_c *MockClient_DeleteWebhook_Call) Return(_a0 error) *MockClient_DeleteWebhook_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_DeleteWebhook_Call) RunAndReturn(run func(context.Context, string, string, int64) error) *MockClient_DeleteWebhook_Call { + _c.Call.Return(run) + return _c +} + +// EditWebhook provides a mock function with given fields: ctx, owner, repository, cfg +func (_m *MockClient) EditWebhook(ctx context.Context, owner string, repository string, cfg WebhookConfig) error { + ret := _m.Called(ctx, owner, repository, cfg) + + if len(ret) == 0 { + panic("no return value specified for EditWebhook") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, WebhookConfig) error); ok { + r0 = rf(ctx, owner, repository, cfg) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_EditWebhook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EditWebhook' +type MockClient_EditWebhook_Call struct { + *mock.Call +} + +// EditWebhook is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - cfg WebhookConfig +func (_e *MockClient_Expecter) EditWebhook(ctx interface{}, owner interface{}, repository interface{}, cfg interface{}) *MockClient_EditWebhook_Call { + return &MockClient_EditWebhook_Call{Call: _e.mock.On("EditWebhook", ctx, owner, repository, cfg)} +} + +func (_c *MockClient_EditWebhook_Call) Run(run func(ctx context.Context, owner string, repository string, cfg WebhookConfig)) *MockClient_EditWebhook_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(WebhookConfig)) + }) + return _c +} + +func (_c *MockClient_EditWebhook_Call) Return(_a0 error) *MockClient_EditWebhook_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_EditWebhook_Call) RunAndReturn(run func(context.Context, string, string, WebhookConfig) error) *MockClient_EditWebhook_Call { + _c.Call.Return(run) + return _c +} + +// GetBranch provides a mock function with given fields: ctx, owner, repository, branchName +func (_m *MockClient) GetBranch(ctx context.Context, owner string, repository string, branchName string) (Branch, error) { + ret := _m.Called(ctx, owner, repository, branchName) + + if len(ret) == 0 { + panic("no return value specified for GetBranch") + } + + var r0 Branch + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (Branch, error)); ok { + return rf(ctx, owner, repository, branchName) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) Branch); ok { + r0 = rf(ctx, owner, repository, branchName) + } else { + r0 = ret.Get(0).(Branch) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { + r1 = rf(ctx, owner, repository, branchName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_GetBranch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBranch' +type MockClient_GetBranch_Call struct { + *mock.Call +} + +// GetBranch is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - branchName string +func (_e *MockClient_Expecter) GetBranch(ctx interface{}, owner interface{}, repository interface{}, branchName interface{}) *MockClient_GetBranch_Call { + return &MockClient_GetBranch_Call{Call: _e.mock.On("GetBranch", ctx, owner, repository, branchName)} +} + +func (_c *MockClient_GetBranch_Call) Run(run func(ctx context.Context, owner string, repository string, branchName string)) *MockClient_GetBranch_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) + }) + return _c +} + +func (_c *MockClient_GetBranch_Call) Return(_a0 Branch, _a1 error) *MockClient_GetBranch_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_GetBranch_Call) RunAndReturn(run func(context.Context, string, string, string) (Branch, error)) *MockClient_GetBranch_Call { + _c.Call.Return(run) + return _c +} + +// GetContents provides a mock function with given fields: ctx, owner, repository, path, ref +func (_m *MockClient) GetContents(ctx context.Context, owner string, repository string, path string, ref string) (RepositoryContent, []RepositoryContent, error) { + ret := _m.Called(ctx, owner, repository, path, ref) + + if len(ret) == 0 { + panic("no return value specified for GetContents") + } + + var r0 RepositoryContent + var r1 []RepositoryContent + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) (RepositoryContent, []RepositoryContent, error)); ok { + return rf(ctx, owner, repository, path, ref) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) RepositoryContent); ok { + r0 = rf(ctx, owner, repository, path, ref) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(RepositoryContent) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) []RepositoryContent); ok { + r1 = rf(ctx, owner, repository, path, ref) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).([]RepositoryContent) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, string, string, string, string) error); ok { + r2 = rf(ctx, owner, repository, path, ref) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// MockClient_GetContents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContents' +type MockClient_GetContents_Call struct { + *mock.Call +} + +// GetContents is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - path string +// - ref string +func (_e *MockClient_Expecter) GetContents(ctx interface{}, owner interface{}, repository interface{}, path interface{}, ref interface{}) *MockClient_GetContents_Call { + return &MockClient_GetContents_Call{Call: _e.mock.On("GetContents", ctx, owner, repository, path, ref)} +} + +func (_c *MockClient_GetContents_Call) Run(run func(ctx context.Context, owner string, repository string, path string, ref string)) *MockClient_GetContents_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) + }) + return _c +} + +func (_c *MockClient_GetContents_Call) Return(fileContents RepositoryContent, dirContents []RepositoryContent, err error) *MockClient_GetContents_Call { + _c.Call.Return(fileContents, dirContents, err) + return _c +} + +func (_c *MockClient_GetContents_Call) RunAndReturn(run func(context.Context, string, string, string, string) (RepositoryContent, []RepositoryContent, error)) *MockClient_GetContents_Call { + _c.Call.Return(run) + return _c +} + +// GetTree provides a mock function with given fields: ctx, owner, repository, basePath, ref, recursive +func (_m *MockClient) GetTree(ctx context.Context, owner string, repository string, basePath string, ref string, recursive bool) ([]RepositoryContent, bool, error) { + ret := _m.Called(ctx, owner, repository, basePath, ref, recursive) + + if len(ret) == 0 { + panic("no return value specified for GetTree") + } + + var r0 []RepositoryContent + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool) ([]RepositoryContent, bool, error)); ok { + return rf(ctx, owner, repository, basePath, ref, recursive) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool) []RepositoryContent); ok { + r0 = rf(ctx, owner, repository, basePath, ref, recursive) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]RepositoryContent) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string, bool) bool); ok { + r1 = rf(ctx, owner, repository, basePath, ref, recursive) + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func(context.Context, string, string, string, string, bool) error); ok { + r2 = rf(ctx, owner, repository, basePath, ref, recursive) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// MockClient_GetTree_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTree' +type MockClient_GetTree_Call struct { + *mock.Call +} + +// GetTree is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - basePath string +// - ref string +// - recursive bool +func (_e *MockClient_Expecter) GetTree(ctx interface{}, owner interface{}, repository interface{}, basePath interface{}, ref interface{}, recursive interface{}) *MockClient_GetTree_Call { + return &MockClient_GetTree_Call{Call: _e.mock.On("GetTree", ctx, owner, repository, basePath, ref, recursive)} +} + +func (_c *MockClient_GetTree_Call) Run(run func(ctx context.Context, owner string, repository string, basePath string, ref string, recursive bool)) *MockClient_GetTree_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(bool)) + }) + return _c +} + +func (_c *MockClient_GetTree_Call) Return(entries []RepositoryContent, truncated bool, err error) *MockClient_GetTree_Call { + _c.Call.Return(entries, truncated, err) + return _c +} + +func (_c *MockClient_GetTree_Call) RunAndReturn(run func(context.Context, string, string, string, string, bool) ([]RepositoryContent, bool, error)) *MockClient_GetTree_Call { + _c.Call.Return(run) + return _c +} + +// GetWebhook provides a mock function with given fields: ctx, owner, repository, webhookID +func (_m *MockClient) GetWebhook(ctx context.Context, owner string, repository string, webhookID int64) (WebhookConfig, error) { + ret := _m.Called(ctx, owner, repository, webhookID) + + if len(ret) == 0 { + panic("no return value specified for GetWebhook") + } + + var r0 WebhookConfig + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) (WebhookConfig, error)); ok { + return rf(ctx, owner, repository, webhookID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, int64) WebhookConfig); ok { + r0 = rf(ctx, owner, repository, webhookID) + } else { + r0 = ret.Get(0).(WebhookConfig) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, int64) error); ok { + r1 = rf(ctx, owner, repository, webhookID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_GetWebhook_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWebhook' +type MockClient_GetWebhook_Call struct { + *mock.Call +} + +// GetWebhook is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - webhookID int64 +func (_e *MockClient_Expecter) GetWebhook(ctx interface{}, owner interface{}, repository interface{}, webhookID interface{}) *MockClient_GetWebhook_Call { + return &MockClient_GetWebhook_Call{Call: _e.mock.On("GetWebhook", ctx, owner, repository, webhookID)} +} + +func (_c *MockClient_GetWebhook_Call) Run(run func(ctx context.Context, owner string, repository string, webhookID int64)) *MockClient_GetWebhook_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(int64)) + }) + return _c +} + +func (_c *MockClient_GetWebhook_Call) Return(_a0 WebhookConfig, _a1 error) *MockClient_GetWebhook_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_GetWebhook_Call) RunAndReturn(run func(context.Context, string, string, int64) (WebhookConfig, error)) *MockClient_GetWebhook_Call { + _c.Call.Return(run) + return _c +} + +// IsAuthenticated provides a mock function with given fields: ctx +func (_m *MockClient) IsAuthenticated(ctx context.Context) error { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for IsAuthenticated") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(ctx) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_IsAuthenticated_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsAuthenticated' +type MockClient_IsAuthenticated_Call struct { + *mock.Call +} + +// IsAuthenticated is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockClient_Expecter) IsAuthenticated(ctx interface{}) *MockClient_IsAuthenticated_Call { + return &MockClient_IsAuthenticated_Call{Call: _e.mock.On("IsAuthenticated", ctx)} +} + +func (_c *MockClient_IsAuthenticated_Call) Run(run func(ctx context.Context)) *MockClient_IsAuthenticated_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockClient_IsAuthenticated_Call) Return(_a0 error) *MockClient_IsAuthenticated_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_IsAuthenticated_Call) RunAndReturn(run func(context.Context) error) *MockClient_IsAuthenticated_Call { + _c.Call.Return(run) + return _c +} + +// ListPullRequestFiles provides a mock function with given fields: ctx, owner, repository, number +func (_m *MockClient) ListPullRequestFiles(ctx context.Context, owner string, repository string, number int) ([]CommitFile, error) { + ret := _m.Called(ctx, owner, repository, number) + + if len(ret) == 0 { + panic("no return value specified for ListPullRequestFiles") + } + + var r0 []CommitFile + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, int) ([]CommitFile, error)); ok { + return rf(ctx, owner, repository, number) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, int) []CommitFile); ok { + r0 = rf(ctx, owner, repository, number) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]CommitFile) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, int) error); ok { + r1 = rf(ctx, owner, repository, number) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_ListPullRequestFiles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPullRequestFiles' +type MockClient_ListPullRequestFiles_Call struct { + *mock.Call +} + +// ListPullRequestFiles is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - number int +func (_e *MockClient_Expecter) ListPullRequestFiles(ctx interface{}, owner interface{}, repository interface{}, number interface{}) *MockClient_ListPullRequestFiles_Call { + return &MockClient_ListPullRequestFiles_Call{Call: _e.mock.On("ListPullRequestFiles", ctx, owner, repository, number)} +} + +func (_c *MockClient_ListPullRequestFiles_Call) Run(run func(ctx context.Context, owner string, repository string, number int)) *MockClient_ListPullRequestFiles_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(int)) + }) + return _c +} + +func (_c *MockClient_ListPullRequestFiles_Call) Return(_a0 []CommitFile, _a1 error) *MockClient_ListPullRequestFiles_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_ListPullRequestFiles_Call) RunAndReturn(run func(context.Context, string, string, int) ([]CommitFile, error)) *MockClient_ListPullRequestFiles_Call { + _c.Call.Return(run) + return _c +} + +// ListWebhooks provides a mock function with given fields: ctx, owner, repository +func (_m *MockClient) ListWebhooks(ctx context.Context, owner string, repository string) ([]WebhookConfig, error) { + ret := _m.Called(ctx, owner, repository) + + if len(ret) == 0 { + panic("no return value specified for ListWebhooks") + } + + var r0 []WebhookConfig + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]WebhookConfig, error)); ok { + return rf(ctx, owner, repository) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) []WebhookConfig); ok { + r0 = rf(ctx, owner, repository) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]WebhookConfig) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, owner, repository) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_ListWebhooks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListWebhooks' +type MockClient_ListWebhooks_Call struct { + *mock.Call +} + +// ListWebhooks is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +func (_e *MockClient_Expecter) ListWebhooks(ctx interface{}, owner interface{}, repository interface{}) *MockClient_ListWebhooks_Call { + return &MockClient_ListWebhooks_Call{Call: _e.mock.On("ListWebhooks", ctx, owner, repository)} +} + +func (_c *MockClient_ListWebhooks_Call) Run(run func(ctx context.Context, owner string, repository string)) *MockClient_ListWebhooks_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockClient_ListWebhooks_Call) Return(_a0 []WebhookConfig, _a1 error) *MockClient_ListWebhooks_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_ListWebhooks_Call) RunAndReturn(run func(context.Context, string, string) ([]WebhookConfig, error)) *MockClient_ListWebhooks_Call { + _c.Call.Return(run) + return _c +} + +// RepoExists provides a mock function with given fields: ctx, owner, repository +func (_m *MockClient) RepoExists(ctx context.Context, owner string, repository string) (bool, error) { + ret := _m.Called(ctx, owner, repository) + + if len(ret) == 0 { + panic("no return value specified for RepoExists") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (bool, error)); ok { + return rf(ctx, owner, repository) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok { + r0 = rf(ctx, owner, repository) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, owner, repository) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClient_RepoExists_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RepoExists' +type MockClient_RepoExists_Call struct { + *mock.Call +} + +// RepoExists is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +func (_e *MockClient_Expecter) RepoExists(ctx interface{}, owner interface{}, repository interface{}) *MockClient_RepoExists_Call { + return &MockClient_RepoExists_Call{Call: _e.mock.On("RepoExists", ctx, owner, repository)} +} + +func (_c *MockClient_RepoExists_Call) Run(run func(ctx context.Context, owner string, repository string)) *MockClient_RepoExists_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockClient_RepoExists_Call) Return(_a0 bool, _a1 error) *MockClient_RepoExists_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClient_RepoExists_Call) RunAndReturn(run func(context.Context, string, string) (bool, error)) *MockClient_RepoExists_Call { + _c.Call.Return(run) + return _c +} + +// UpdateFile provides a mock function with given fields: ctx, owner, repository, path, branch, message, hash, content +func (_m *MockClient) UpdateFile(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string, content []byte) error { + ret := _m.Called(ctx, owner, repository, path, branch, message, hash, content) + + if len(ret) == 0 { + panic("no return value specified for UpdateFile") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, string, string, []byte) error); ok { + r0 = rf(ctx, owner, repository, path, branch, message, hash, content) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClient_UpdateFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateFile' +type MockClient_UpdateFile_Call struct { + *mock.Call +} + +// UpdateFile is a helper method to define mock.On call +// - ctx context.Context +// - owner string +// - repository string +// - path string +// - branch string +// - message string +// - hash string +// - content []byte +func (_e *MockClient_Expecter) UpdateFile(ctx interface{}, owner interface{}, repository interface{}, path interface{}, branch interface{}, message interface{}, hash interface{}, content interface{}) *MockClient_UpdateFile_Call { + return &MockClient_UpdateFile_Call{Call: _e.mock.On("UpdateFile", ctx, owner, repository, path, branch, message, hash, content)} +} + +func (_c *MockClient_UpdateFile_Call) Run(run func(ctx context.Context, owner string, repository string, path string, branch string, message string, hash string, content []byte)) *MockClient_UpdateFile_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].(string), args[7].([]byte)) + }) + return _c +} + +func (_c *MockClient_UpdateFile_Call) Return(_a0 error) *MockClient_UpdateFile_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClient_UpdateFile_Call) RunAndReturn(run func(context.Context, string, string, string, string, string, string, []byte) error) *MockClient_UpdateFile_Call { + _c.Call.Return(run) + return _c +} + +// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockClient(t interface { + mock.TestingT + Cleanup(func()) +}) *MockClient { + mock := &MockClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/repository/github/mock_commit_file.go b/pkg/registry/apis/provisioning/repository/github/mock_commit_file.go new file mode 100644 index 00000000000..68d3d2044f0 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/mock_commit_file.go @@ -0,0 +1,212 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package github + +import mock "github.com/stretchr/testify/mock" + +// MockCommitFile is an autogenerated mock type for the CommitFile type +type MockCommitFile struct { + mock.Mock +} + +type MockCommitFile_Expecter struct { + mock *mock.Mock +} + +func (_m *MockCommitFile) EXPECT() *MockCommitFile_Expecter { + return &MockCommitFile_Expecter{mock: &_m.Mock} +} + +// GetFilename provides a mock function with no fields +func (_m *MockCommitFile) GetFilename() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetFilename") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockCommitFile_GetFilename_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFilename' +type MockCommitFile_GetFilename_Call struct { + *mock.Call +} + +// GetFilename is a helper method to define mock.On call +func (_e *MockCommitFile_Expecter) GetFilename() *MockCommitFile_GetFilename_Call { + return &MockCommitFile_GetFilename_Call{Call: _e.mock.On("GetFilename")} +} + +func (_c *MockCommitFile_GetFilename_Call) Run(run func()) *MockCommitFile_GetFilename_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockCommitFile_GetFilename_Call) Return(_a0 string) *MockCommitFile_GetFilename_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCommitFile_GetFilename_Call) RunAndReturn(run func() string) *MockCommitFile_GetFilename_Call { + _c.Call.Return(run) + return _c +} + +// GetPreviousFilename provides a mock function with no fields +func (_m *MockCommitFile) GetPreviousFilename() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetPreviousFilename") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockCommitFile_GetPreviousFilename_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPreviousFilename' +type MockCommitFile_GetPreviousFilename_Call struct { + *mock.Call +} + +// GetPreviousFilename is a helper method to define mock.On call +func (_e *MockCommitFile_Expecter) GetPreviousFilename() *MockCommitFile_GetPreviousFilename_Call { + return &MockCommitFile_GetPreviousFilename_Call{Call: _e.mock.On("GetPreviousFilename")} +} + +func (_c *MockCommitFile_GetPreviousFilename_Call) Run(run func()) *MockCommitFile_GetPreviousFilename_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockCommitFile_GetPreviousFilename_Call) Return(_a0 string) *MockCommitFile_GetPreviousFilename_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCommitFile_GetPreviousFilename_Call) RunAndReturn(run func() string) *MockCommitFile_GetPreviousFilename_Call { + _c.Call.Return(run) + return _c +} + +// GetSHA provides a mock function with no fields +func (_m *MockCommitFile) GetSHA() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetSHA") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockCommitFile_GetSHA_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSHA' +type MockCommitFile_GetSHA_Call struct { + *mock.Call +} + +// GetSHA is a helper method to define mock.On call +func (_e *MockCommitFile_Expecter) GetSHA() *MockCommitFile_GetSHA_Call { + return &MockCommitFile_GetSHA_Call{Call: _e.mock.On("GetSHA")} +} + +func (_c *MockCommitFile_GetSHA_Call) Run(run func()) *MockCommitFile_GetSHA_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockCommitFile_GetSHA_Call) Return(_a0 string) *MockCommitFile_GetSHA_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCommitFile_GetSHA_Call) RunAndReturn(run func() string) *MockCommitFile_GetSHA_Call { + _c.Call.Return(run) + return _c +} + +// GetStatus provides a mock function with no fields +func (_m *MockCommitFile) GetStatus() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetStatus") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockCommitFile_GetStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetStatus' +type MockCommitFile_GetStatus_Call struct { + *mock.Call +} + +// GetStatus is a helper method to define mock.On call +func (_e *MockCommitFile_Expecter) GetStatus() *MockCommitFile_GetStatus_Call { + return &MockCommitFile_GetStatus_Call{Call: _e.mock.On("GetStatus")} +} + +func (_c *MockCommitFile_GetStatus_Call) Run(run func()) *MockCommitFile_GetStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockCommitFile_GetStatus_Call) Return(_a0 string) *MockCommitFile_GetStatus_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockCommitFile_GetStatus_Call) RunAndReturn(run func() string) *MockCommitFile_GetStatus_Call { + _c.Call.Return(run) + return _c +} + +// NewMockCommitFile creates a new instance of MockCommitFile. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockCommitFile(t interface { + mock.TestingT + Cleanup(func()) +}) *MockCommitFile { + mock := &MockCommitFile{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/repository/github/mock_repository_content.go b/pkg/registry/apis/provisioning/repository/github/mock_repository_content.go new file mode 100644 index 00000000000..bddbaaf2558 --- /dev/null +++ b/pkg/registry/apis/provisioning/repository/github/mock_repository_content.go @@ -0,0 +1,312 @@ +// Code generated by mockery v2.52.4. DO NOT EDIT. + +package github + +import mock "github.com/stretchr/testify/mock" + +// MockRepositoryContent is an autogenerated mock type for the RepositoryContent type +type MockRepositoryContent struct { + mock.Mock +} + +type MockRepositoryContent_Expecter struct { + mock *mock.Mock +} + +func (_m *MockRepositoryContent) EXPECT() *MockRepositoryContent_Expecter { + return &MockRepositoryContent_Expecter{mock: &_m.Mock} +} + +// GetFileContent provides a mock function with no fields +func (_m *MockRepositoryContent) GetFileContent() (string, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetFileContent") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockRepositoryContent_GetFileContent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFileContent' +type MockRepositoryContent_GetFileContent_Call struct { + *mock.Call +} + +// GetFileContent is a helper method to define mock.On call +func (_e *MockRepositoryContent_Expecter) GetFileContent() *MockRepositoryContent_GetFileContent_Call { + return &MockRepositoryContent_GetFileContent_Call{Call: _e.mock.On("GetFileContent")} +} + +func (_c *MockRepositoryContent_GetFileContent_Call) Run(run func()) *MockRepositoryContent_GetFileContent_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRepositoryContent_GetFileContent_Call) Return(_a0 string, _a1 error) *MockRepositoryContent_GetFileContent_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockRepositoryContent_GetFileContent_Call) RunAndReturn(run func() (string, error)) *MockRepositoryContent_GetFileContent_Call { + _c.Call.Return(run) + return _c +} + +// GetPath provides a mock function with no fields +func (_m *MockRepositoryContent) GetPath() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetPath") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockRepositoryContent_GetPath_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPath' +type MockRepositoryContent_GetPath_Call struct { + *mock.Call +} + +// GetPath is a helper method to define mock.On call +func (_e *MockRepositoryContent_Expecter) GetPath() *MockRepositoryContent_GetPath_Call { + return &MockRepositoryContent_GetPath_Call{Call: _e.mock.On("GetPath")} +} + +func (_c *MockRepositoryContent_GetPath_Call) Run(run func()) *MockRepositoryContent_GetPath_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRepositoryContent_GetPath_Call) Return(_a0 string) *MockRepositoryContent_GetPath_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRepositoryContent_GetPath_Call) RunAndReturn(run func() string) *MockRepositoryContent_GetPath_Call { + _c.Call.Return(run) + return _c +} + +// GetSHA provides a mock function with no fields +func (_m *MockRepositoryContent) GetSHA() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetSHA") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// MockRepositoryContent_GetSHA_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSHA' +type MockRepositoryContent_GetSHA_Call struct { + *mock.Call +} + +// GetSHA is a helper method to define mock.On call +func (_e *MockRepositoryContent_Expecter) GetSHA() *MockRepositoryContent_GetSHA_Call { + return &MockRepositoryContent_GetSHA_Call{Call: _e.mock.On("GetSHA")} +} + +func (_c *MockRepositoryContent_GetSHA_Call) Run(run func()) *MockRepositoryContent_GetSHA_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRepositoryContent_GetSHA_Call) Return(_a0 string) *MockRepositoryContent_GetSHA_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRepositoryContent_GetSHA_Call) RunAndReturn(run func() string) *MockRepositoryContent_GetSHA_Call { + _c.Call.Return(run) + return _c +} + +// GetSize provides a mock function with no fields +func (_m *MockRepositoryContent) GetSize() int64 { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetSize") + } + + var r0 int64 + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + +// MockRepositoryContent_GetSize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSize' +type MockRepositoryContent_GetSize_Call struct { + *mock.Call +} + +// GetSize is a helper method to define mock.On call +func (_e *MockRepositoryContent_Expecter) GetSize() *MockRepositoryContent_GetSize_Call { + return &MockRepositoryContent_GetSize_Call{Call: _e.mock.On("GetSize")} +} + +func (_c *MockRepositoryContent_GetSize_Call) Run(run func()) *MockRepositoryContent_GetSize_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRepositoryContent_GetSize_Call) Return(_a0 int64) *MockRepositoryContent_GetSize_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRepositoryContent_GetSize_Call) RunAndReturn(run func() int64) *MockRepositoryContent_GetSize_Call { + _c.Call.Return(run) + return _c +} + +// IsDirectory provides a mock function with no fields +func (_m *MockRepositoryContent) IsDirectory() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for IsDirectory") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// MockRepositoryContent_IsDirectory_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsDirectory' +type MockRepositoryContent_IsDirectory_Call struct { + *mock.Call +} + +// IsDirectory is a helper method to define mock.On call +func (_e *MockRepositoryContent_Expecter) IsDirectory() *MockRepositoryContent_IsDirectory_Call { + return &MockRepositoryContent_IsDirectory_Call{Call: _e.mock.On("IsDirectory")} +} + +func (_c *MockRepositoryContent_IsDirectory_Call) Run(run func()) *MockRepositoryContent_IsDirectory_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRepositoryContent_IsDirectory_Call) Return(_a0 bool) *MockRepositoryContent_IsDirectory_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRepositoryContent_IsDirectory_Call) RunAndReturn(run func() bool) *MockRepositoryContent_IsDirectory_Call { + _c.Call.Return(run) + return _c +} + +// IsSymlink provides a mock function with no fields +func (_m *MockRepositoryContent) IsSymlink() bool { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for IsSymlink") + } + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// MockRepositoryContent_IsSymlink_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsSymlink' +type MockRepositoryContent_IsSymlink_Call struct { + *mock.Call +} + +// IsSymlink is a helper method to define mock.On call +func (_e *MockRepositoryContent_Expecter) IsSymlink() *MockRepositoryContent_IsSymlink_Call { + return &MockRepositoryContent_IsSymlink_Call{Call: _e.mock.On("IsSymlink")} +} + +func (_c *MockRepositoryContent_IsSymlink_Call) Run(run func()) *MockRepositoryContent_IsSymlink_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockRepositoryContent_IsSymlink_Call) Return(_a0 bool) *MockRepositoryContent_IsSymlink_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockRepositoryContent_IsSymlink_Call) RunAndReturn(run func() bool) *MockRepositoryContent_IsSymlink_Call { + _c.Call.Return(run) + return _c +} + +// NewMockRepositoryContent creates a new instance of MockRepositoryContent. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockRepositoryContent(t interface { + mock.TestingT + Cleanup(func()) +}) *MockRepositoryContent { + mock := &MockRepositoryContent{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/repository/github_test.go b/pkg/registry/apis/provisioning/repository/github_test.go index 6ee181429d5..f6939510f87 100644 --- a/pkg/registry/apis/provisioning/repository/github_test.go +++ b/pkg/registry/apis/provisioning/repository/github_test.go @@ -1,6 +1,7 @@ package repository import ( + context "context" "fmt" "net/http" "os" @@ -8,10 +9,12 @@ import ( "testing" "github.com/stretchr/testify/assert" + mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github" ) func TestIsValidGitBranchName(t *testing.T) { @@ -147,3 +150,101 @@ func TestParseWebhooks(t *testing.T) { }) } } + +func TestReadTree(t *testing.T) { + tests := []struct { + name string + path string + tree []pgh.RepositoryContent + expected []FileTreeEntry + }{ + {name: "empty tree", tree: []pgh.RepositoryContent{}, expected: []FileTreeEntry{}}, + {name: "single file", tree: func() []pgh.RepositoryContent { + content := pgh.NewMockRepositoryContent(t) + content.EXPECT().GetPath().Return("file.txt") + content.EXPECT().GetSize().Return(int64(100)) + content.EXPECT().GetSHA().Return("abc123") + content.EXPECT().IsDirectory().Return(false) + return []pgh.RepositoryContent{content} + }(), expected: []FileTreeEntry{ + {Path: "file.txt", Size: 100, Hash: "abc123", Blob: true}, + }}, + {name: "single directory", tree: func() []pgh.RepositoryContent { + content := pgh.NewMockRepositoryContent(t) + content.EXPECT().GetPath().Return("dir") + content.EXPECT().IsDirectory().Return(true) + content.EXPECT().GetSize().Return(int64(0)) + content.EXPECT().GetSHA().Return("") + + return []pgh.RepositoryContent{content} + }(), expected: []FileTreeEntry{ + {Path: "dir/", Blob: false}, + }}, + {name: "mixed content", tree: func() []pgh.RepositoryContent { + file1 := pgh.NewMockRepositoryContent(t) + file1.EXPECT().GetPath().Return("file1.txt") + file1.EXPECT().GetSize().Return(int64(100)) + file1.EXPECT().GetSHA().Return("abc123") + file1.EXPECT().IsDirectory().Return(false) + + dir := pgh.NewMockRepositoryContent(t) + dir.EXPECT().GetPath().Return("dir") + dir.EXPECT().IsDirectory().Return(true) + dir.EXPECT().GetSize().Return(int64(0)) + dir.EXPECT().GetSHA().Return("") + + file2 := pgh.NewMockRepositoryContent(t) + file2.EXPECT().GetPath().Return("file2.txt") + file2.EXPECT().GetSize().Return(int64(200)) + file2.EXPECT().GetSHA().Return("def456") + file2.EXPECT().IsDirectory().Return(false) + + return []pgh.RepositoryContent{file1, dir, file2} + }(), expected: []FileTreeEntry{ + {Path: "file1.txt", Size: 100, Hash: "abc123", Blob: true}, + {Path: "dir/", Blob: false}, + {Path: "file2.txt", Size: 200, Hash: "def456", Blob: true}, + }}, + {name: "with path prefix", path: "prefix", tree: func() []pgh.RepositoryContent { + file := pgh.NewMockRepositoryContent(t) + file.EXPECT().GetPath().Return("file.txt") + file.EXPECT().GetSize().Return(int64(100)) + file.EXPECT().GetSHA().Return("abc123") + file.EXPECT().IsDirectory().Return(false) + + dir := pgh.NewMockRepositoryContent(t) + dir.EXPECT().GetPath().Return("dir") + dir.EXPECT().GetSize().Return(int64(0)) + dir.EXPECT().GetSHA().Return("") + dir.EXPECT().IsDirectory().Return(true) + + return []pgh.RepositoryContent{file, dir} + }(), expected: []FileTreeEntry{ + {Path: "file.txt", Size: 100, Hash: "abc123", Blob: true}, + {Path: "dir/", Blob: false}, + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ghMock := pgh.NewMockClient(t) + gh := &githubRepository{ + owner: "owner", + repo: "repo", + config: &provisioning.Repository{ + Spec: provisioning.RepositorySpec{ + GitHub: &provisioning.GitHubRepositoryConfig{ + Path: tt.path, + }, + }, + }, + gh: ghMock, + } + + ghMock.On("GetTree", mock.Anything, "owner", "repo", tt.path, "some-ref", true).Return(tt.tree, false, nil) + tree, err := gh.ReadTree(context.Background(), "some-ref") + require.NoError(t, err) + require.Equal(t, tt.expected, tree) + }) + } +} diff --git a/pkg/registry/apis/provisioning/repository/local_test.go b/pkg/registry/apis/provisioning/repository/local_test.go index 42e09e725f4..c2825ff4f84 100644 --- a/pkg/registry/apis/provisioning/repository/local_test.go +++ b/pkg/registry/apis/provisioning/repository/local_test.go @@ -46,6 +46,9 @@ func TestLocalResolver(t *testing.T) { "client.go", "factory.go", "impl.go", + "mock_client.go", + "mock_commit_file.go", + "mock_repository_content.go", "testdata", "testdata/webhook-issue_comment-created.json", "testdata/webhook-ping-check.json", diff --git a/pkg/registry/apis/provisioning/safepath/dir.go b/pkg/registry/apis/provisioning/safepath/dir.go index c6d0bbbe727..d156229df6c 100644 --- a/pkg/registry/apis/provisioning/safepath/dir.go +++ b/pkg/registry/apis/provisioning/safepath/dir.go @@ -1,6 +1,7 @@ package safepath import ( + "fmt" "path" "strings" ) @@ -36,3 +37,29 @@ func Dir(filePath string) string { func InDir(filePath, dir string) bool { return strings.HasPrefix(filePath, dir) } + +// RelativeTo returns the relative path of the filePath to the given directory. +// It handles cases where either filePath or dir have leading or trailing slashes. +func RelativeTo(filePath, dir string) (string, error) { + if dir == "/" || dir == "" { + return filePath, nil + } + + // Normalize paths by trimming leading and trailing slashes + normalizedDir := strings.Trim(dir, "/") + if normalizedDir != "" { + normalizedDir += "/" + } + + normalizedPath := strings.TrimPrefix(filePath, "/") + + // Check if the normalized path is in the normalized directory + if !strings.HasPrefix(normalizedPath, normalizedDir) { + return "", fmt.Errorf("filePath is not a subdirectory of dir") + } + + // Get the relative path by trimming the directory prefix + relativePath := strings.TrimPrefix(normalizedPath, normalizedDir) + + return relativePath, nil +} diff --git a/pkg/registry/apis/provisioning/safepath/dir_test.go b/pkg/registry/apis/provisioning/safepath/dir_test.go index bf55b160660..2d88614243b 100644 --- a/pkg/registry/apis/provisioning/safepath/dir_test.go +++ b/pkg/registry/apis/provisioning/safepath/dir_test.go @@ -160,3 +160,101 @@ func TestInDir(t *testing.T) { }) } } + +func TestRelativeTo(t *testing.T) { + tests := []struct { + name string + filePath string + dir string + want string + expectError bool + }{ + { + name: "simple relative path", + filePath: "folder/subfolder/file.txt", + dir: "folder", + want: "subfolder/file.txt", + }, + { + name: "relative path with leading slash", + filePath: "/prefix/folder/subfolder/file.txt", + dir: "/prefix/folder", + want: "subfolder/file.txt", + }, + { + name: "relative path with leading slash in dir but in filePath", + filePath: "prefix/folder/subfolder/file.txt", + dir: "/prefix/folder", + want: "subfolder/file.txt", + }, + { + name: "with trailing slash in dir", + filePath: "folder/subfolder/file.txt", + dir: "folder/", + want: "subfolder/file.txt", + }, + { + name: "with trailing slash in both", + filePath: "folder/subfolder/", + dir: "folder/", + want: "subfolder/", + }, + { + name: "empty directory", + filePath: "file.txt", + dir: "", + want: "file.txt", + }, + { + name: "directory is root", + filePath: "folder/file.txt", + dir: "/", + want: "folder/file.txt", + }, + { + name: "nested directories", + filePath: "a/b/c/d/file.txt", + dir: "a/b", + want: "c/d/file.txt", + }, + { + name: "file not in directory", + filePath: "other/file.txt", + dir: "folder", + want: "", + expectError: true, + }, + { + name: "file path shorter than directory", + filePath: "file.txt", + dir: "folder/subfolder", + want: "", + expectError: true, + }, + { + name: "same directory", + filePath: "folder/file.txt", + dir: "folder", + want: "file.txt", + }, + { + name: "directory with similar prefix", + filePath: "folder2/file.txt", + dir: "folder", + want: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := RelativeTo(tt.filePath, tt.dir) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} From 828cd05f18cbabbbbe2147ff3a8f0763308f1481 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Thu, 10 Apr 2025 19:27:31 +0100 Subject: [PATCH 02/37] ConditionalRendering: Adjust naming and delete button alignment (#103677) adjust naming and delete button alignment --- .../conditional-rendering/ConditionalRenderingBase.tsx | 2 +- .../ConditionalRenderingGroupCondition.tsx | 4 ++-- public/locales/en-US/grafana.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingBase.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingBase.tsx index f71e4be541b..d078e68da18 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingBase.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingBase.tsx @@ -100,7 +100,7 @@ function ConditionalRenderingBaseRenderer({ )} - + {comp} {!model.isItemSupported() && ( diff --git a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupCondition.tsx b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupCondition.tsx index 642c4a22b9b..7f7e098d757 100644 --- a/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupCondition.tsx +++ b/public/app/features/dashboard-scene/conditional-rendering/ConditionalRenderingGroupCondition.tsx @@ -17,8 +17,8 @@ export const ConditionalRenderingGroupCondition = ({ value, onChange }: Props) = const options: Array> = useMemo( () => [ - { label: t('dashboard.conditional-rendering.conditions.group.condition.all', 'Match all (AND)'), value: 'and' }, - { label: t('dashboard.conditional-rendering.conditions.group.condition.any', 'Match any (OR)'), value: 'or' }, + { label: t('dashboard.conditional-rendering.conditions.group.condition.all', 'Match all'), value: 'and' }, + { label: t('dashboard.conditional-rendering.conditions.group.condition.any', 'Match any'), value: 'or' }, ], [] ); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index be613569e0c..de8e8e8e608 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -2672,8 +2672,8 @@ "variable": "Template variable" }, "condition": { - "all": "Match all (AND)", - "any": "Match any (OR)", + "all": "Match all", + "any": "Match any", "label": "Match rules" }, "label": "Group", From 778d0b2da49b53e8278e8eb09c25f47509df0c5d Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Thu, 10 Apr 2025 20:47:17 +0200 Subject: [PATCH 03/37] Logs: Clean up Explore meta information (#103801) * LogLabels: add displayMax property * MetaInfoText: prevent label wrapping * LogsMetaRow: remove unescaped content and max characters * LogsMetaRow: use font weight normal * LogLabels: add button to collapse back * LogsMetaRow: set line height * LogsMetaRow: save common logs state * LogsMetaRow: update unit tests * LogLabels: update unit test * LogListContext: add forceEscape option * LogListContext: move forceEscape and hasUnescapedContents * LogListControls: move forceEscape to controls * LogList: read forceEscape from context * i18n * LogListControls: test new control * Update betterer * logsModel: update test * LogLabels: replace characters with icons * Prettier * LogLabels: add aria labels * i18n * Update tests * Update test --- public/app/features/explore/Logs/Logs.tsx | 12 +- .../explore/Logs/LogsMetaRow.test.tsx | 115 ++++++++++------- .../app/features/explore/Logs/LogsMetaRow.tsx | 69 ++++------ .../app/features/explore/Logs/utils/logs.ts | 1 + public/app/features/explore/MetaInfoText.tsx | 1 + .../logs/components/ControlledLogRows.tsx | 17 ++- .../logs/components/LogLabels.test.tsx | 54 ++++++++ .../features/logs/components/LogLabels.tsx | 121 ++++++++++++------ .../logs/components/panel/LogList.tsx | 10 +- .../logs/components/panel/LogListContext.tsx | 30 ++++- .../components/panel/LogListControls.test.tsx | 19 +++ .../logs/components/panel/LogListControls.tsx | 25 ++++ .../panel/__mocks__/LogListContext.tsx | 3 + public/app/features/logs/logsModel.test.ts | 76 +++++------ public/app/features/logs/logsModel.ts | 15 ++- public/locales/en-US/grafana.json | 6 + 16 files changed, 382 insertions(+), 192 deletions(-) diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx index 8cf6381aa6a..3c1b6c997b0 100644 --- a/public/app/features/explore/Logs/Logs.tsx +++ b/public/app/features/explore/Logs/Logs.tsx @@ -209,7 +209,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { ); const [isFlipping, setIsFlipping] = useState(false); const [displayedFields, setDisplayedFields] = useState([]); - const [forceEscape, setForceEscape] = useState(false); const [contextOpen, setContextOpen] = useState(false); const [contextRow, setContextRow] = useState(undefined); const [pinLineButtonTooltipTitle, setPinLineButtonTooltipTitle] = useState(PINNED_LOGS_MESSAGE); @@ -497,10 +496,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { [sortOrderChanged] ); - const onEscapeNewlines = useCallback(() => { - setForceEscape(!forceEscape); - }, [forceEscape]); - const onChangeVisualisation = useCallback( (visualisation: LogsVisualisationType) => { setVisualisationType(visualisation); @@ -1000,10 +995,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { meta={logsMeta || []} dedupStrategy={dedupStrategy} dedupCount={dedupCount} - hasUnescapedContent={hasUnescapedContent} - forceEscape={forceEscape} displayedFields={displayedFields} - onEscapeNewlines={onEscapeNewlines} clearDetectedFields={clearDetectedFields} /> @@ -1051,7 +1043,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { showLabels={showLabels} showTime={showTime} enableLogDetails={true} - forceEscape={forceEscape} wrapLogMessage={wrapLogMessage} prettifyLogMessage={prettifyLogMessage} timeZone={timeZone} @@ -1075,6 +1066,7 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { logsMeta={logsMeta} logOptionsStorageKey={SETTING_KEY_ROOT} onLogOptionsChange={onLogOptionsChange} + hasUnescapedContent={hasUnescapedContent} /> )} @@ -1110,7 +1102,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { showLabels={showLabels} showTime={showTime} enableLogDetails={true} - forceEscape={forceEscape} wrapLogMessage={wrapLogMessage} prettifyLogMessage={prettifyLogMessage} timeZone={timeZone} @@ -1159,7 +1150,6 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => { dedupStrategy={dedupStrategy} displayedFields={displayedFields} filterLevels={filterLevels} - forceEscape={forceEscape} getFieldLinks={getFieldLinks} getRowContextQuery={getRowContextQuery} loadMore={loadMoreLogs} diff --git a/public/app/features/explore/Logs/LogsMetaRow.test.tsx b/public/app/features/explore/Logs/LogsMetaRow.test.tsx index da0277fbcf4..618be846511 100644 --- a/public/app/features/explore/Logs/LogsMetaRow.test.tsx +++ b/public/app/features/explore/Logs/LogsMetaRow.test.tsx @@ -3,11 +3,19 @@ import userEvent from '@testing-library/user-event'; import saveAs from 'file-saver'; import { ComponentProps } from 'react'; -import { FieldType, LogLevel, LogsDedupStrategy, standardTransformersRegistry, toDataFrame } from '@grafana/data'; +import { + FieldType, + LogLevel, + LogsDedupStrategy, + LogsMetaItem, + LogsMetaKind, + standardTransformersRegistry, + store, + toDataFrame, +} from '@grafana/data'; import { organizeFieldsTransformer } from '@grafana/data/internal'; import { config } from '@grafana/runtime'; -import { MAX_CHARACTERS } from '../../logs/components/LogRowMessage'; import { logRowsToReadableJson } from '../../logs/utils'; import { extractFieldsTransformer } from '../../transformers/extractFields/extractFields'; @@ -26,10 +34,7 @@ const defaultProps: LogsMetaRowProps = { dedupStrategy: LogsDedupStrategy.none, dedupCount: 0, displayedFields: [], - hasUnescapedContent: false, - forceEscape: false, logRows: [], - onEscapeNewlines: jest.fn(), clearDetectedFields: jest.fn(), }; @@ -49,13 +54,6 @@ describe('LogsMetaRow', () => { expect(await screen.findByText('1234')).toBeInTheDocument(); }); - it('renders a highlighting warning', async () => { - setup({ logRows: [{ entry: 'A'.repeat(MAX_CHARACTERS + 1) }] }); - expect( - await screen.findByText('Logs with more than 100,000 characters could not be parsed and highlighted') - ).toBeInTheDocument(); - }); - it('renders the show original line button', () => { setup({ displayedFields: ['test'] }); expect( @@ -65,7 +63,7 @@ describe('LogsMetaRow', () => { ).toBeInTheDocument(); }); - it('renders the displayedfield', async () => { + it('renders the displayed fields', async () => { setup({ displayedFields: ['testField1234'] }); expect(await screen.findByText('testField1234')).toBeInTheDocument(); }); @@ -85,39 +83,6 @@ describe('LogsMetaRow', () => { expect(clearSpy).toBeCalled(); }); - it('renders a button to remove escaping', () => { - setup({ hasUnescapedContent: true, forceEscape: true }); - expect( - screen.getByRole('button', { - name: 'Remove escaping', - }) - ).toBeInTheDocument(); - }); - - it('renders a button to remove escaping', () => { - setup({ hasUnescapedContent: true, forceEscape: false }); - expect( - screen.getByRole('button', { - name: 'Escape newlines', - }) - ).toBeInTheDocument(); - }); - - it('renders a button to remove escaping', () => { - const escapeSpy = jest.fn(); - setup({ hasUnescapedContent: true, forceEscape: false, onEscapeNewlines: escapeSpy }); - fireEvent( - screen.getByRole('button', { - name: 'Escape newlines', - }), - new MouseEvent('click', { - bubbles: true, - cancelable: true, - }) - ); - expect(escapeSpy).toBeCalled(); - }); - it('renders a button to show the download menu', () => { setup(); expect(screen.getByText('Download').closest('button')).toBeInTheDocument(); @@ -317,4 +282,62 @@ describe('LogsMetaRow', () => { const text = await blob.text(); expect(text).toBe(`"Date","time","message bar"\r\n1970-01-01T00:00:00.001Z,1,INFO 1`); }); + + it('renders common labels', async () => { + const meta: LogsMetaItem[] = [ + { + label: 'Common labels', + value: { + exporter: 'OTLP', + job: 'cicd-o11y/grafana-deployment-tools', + }, + kind: LogsMetaKind.LabelsMap, + }, + ]; + setup({ meta }); + expect(await screen.findByText(/Common labels/)).toBeInTheDocument(); + expect(await screen.findByText('exporter=OTLP')).toBeInTheDocument(); + expect(await screen.findByText('job=cicd-o11y/grafana-deployment-tools')).toBeInTheDocument(); + }); + + it('renders collapsed common labels', async () => { + const meta: LogsMetaItem[] = [ + { + label: 'Common labels', + value: { + exporter: 'OTLP', + job: 'cicd-o11y/grafana-deployment-tools', + service_name: 'grafana', + service_namespace: 'cicd-o11y', + }, + kind: LogsMetaKind.LabelsMap, + }, + ]; + setup({ meta }); + expect(await screen.findByText(/Common labels/)).toBeInTheDocument(); + expect(await screen.findByText('exporter=OTLP')).toBeInTheDocument(); + expect(await screen.findByText('job=cicd-o11y/grafana-deployment-tools')).toBeInTheDocument(); + expect(await screen.findByLabelText('Expand labels')).toBeInTheDocument(); + }); + + it('renders expanded common labels', async () => { + jest.spyOn(store, 'getBool').mockReturnValue(true); + const meta: LogsMetaItem[] = [ + { + label: 'Common labels', + value: { + exporter: 'OTLP', + job: 'cicd-o11y/grafana-deployment-tools', + service_name: 'grafana', + service_namespace: 'cicd-o11y', + }, + kind: LogsMetaKind.LabelsMap, + }, + ]; + setup({ meta }); + expect(await screen.findByText(/Common labels/)).toBeInTheDocument(); + expect(await screen.findByText('exporter=OTLP')).toBeInTheDocument(); + expect(await screen.findByText('job=cicd-o11y/grafana-deployment-tools')).toBeInTheDocument(); + expect(await screen.findByLabelText('Collapse labels')).toBeInTheDocument(); + }); }); diff --git a/public/app/features/explore/Logs/LogsMetaRow.tsx b/public/app/features/explore/Logs/LogsMetaRow.tsx index 96aa09fa933..ab893d02933 100644 --- a/public/app/features/explore/Logs/LogsMetaRow.tsx +++ b/public/app/features/explore/Logs/LogsMetaRow.tsx @@ -1,21 +1,26 @@ import { css } from '@emotion/css'; import { memo } from 'react'; -import { LogsDedupStrategy, LogsMetaItem, LogsMetaKind, LogRowModel, CoreApp, Labels } from '@grafana/data'; +import { LogsDedupStrategy, LogsMetaItem, LogsMetaKind, LogRowModel, CoreApp, Labels, store } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; -import { Button, Dropdown, Menu, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui'; +import { Button, Dropdown, Menu, ToolbarButton, useStyles2 } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; -import { LogLabels, LogLabelsList } from '../../logs/components/LogLabels'; -import { MAX_CHARACTERS } from '../../logs/components/LogRowMessage'; +import { LogLabels, LogLabelsList, Props as LogLabelsProps } from '../../logs/components/LogLabels'; import { DownloadFormat, downloadLogs } from '../../logs/utils'; import { MetaInfoText, MetaItemProps } from '../MetaInfoText'; +import { SETTINGS_KEYS } from './utils/logs'; + const getStyles = () => ({ metaContainer: css({ flex: 1, display: 'flex', flexWrap: 'wrap', + '& span': { + fontWeight: 'normal', + lineHeight: '1.25em', + }, }), }); @@ -24,25 +29,12 @@ export type Props = { dedupStrategy: LogsDedupStrategy; dedupCount: number; displayedFields: string[]; - hasUnescapedContent: boolean; - forceEscape: boolean; logRows: LogRowModel[]; - onEscapeNewlines: () => void; clearDetectedFields: () => void; }; export const LogsMetaRow = memo( - ({ - meta, - dedupStrategy, - dedupCount, - displayedFields, - clearDetectedFields, - hasUnescapedContent, - forceEscape, - onEscapeNewlines, - logRows, - }: Props) => { + ({ meta, dedupStrategy, dedupCount, displayedFields, clearDetectedFields, logRows }: Props) => { const style = useStyles2(getStyles); const logsMetaItem: Array = [...meta]; @@ -55,14 +47,6 @@ export const LogsMetaRow = memo( kind: LogsMetaKind.Number, }); } - // Add info about limit for highlighting - if (logRows.some((r) => r.entry.length > MAX_CHARACTERS)) { - logsMetaItem.push({ - label: 'Info', - value: 'Logs with more than 100,000 characters could not be parsed and highlighted', - kind: LogsMetaKind.String, - }); - } // Add detected fields info if (displayedFields?.length > 0) { @@ -91,22 +75,6 @@ export const LogsMetaRow = memo( downloadLogs(format, logRows, meta); } - // Add unescaped content info - if (hasUnescapedContent) { - logsMetaItem.push({ - label: 'Your logs might have incorrectly escaped content', - value: ( - - - - ), - }); - } const downloadMenu = ( {/* eslint-disable-next-line @grafana/no-untranslated-strings */} @@ -117,6 +85,17 @@ export const LogsMetaRow = memo( download(DownloadFormat.CSV)} /> ); + + const onCommonLabelsToggle = (state: boolean) => { + store.set(SETTINGS_KEYS.commonLabels, state); + }; + + const commonLabelsProps = { + onDisplayMaxToggle: onCommonLabelsToggle, + displayMax: 3, + displayAll: store.getBool(SETTINGS_KEYS.commonLabels, false), + }; + return ( <> {logsMetaItem && ( @@ -125,7 +104,7 @@ export const LogsMetaRow = memo( metaItems={logsMetaItem.map((item) => { return { label: item.label, - value: 'kind' in item ? renderMetaItem(item.value, item.kind) : item.value, + value: 'kind' in item ? renderMetaItem(item.value, item.kind, commonLabelsProps) : item.value, }; })} /> @@ -145,12 +124,12 @@ export const LogsMetaRow = memo( LogsMetaRow.displayName = 'LogsMetaRow'; -function renderMetaItem(value: string | number | Labels, kind: LogsMetaKind) { +function renderMetaItem(value: string | number | Labels, kind: LogsMetaKind, logLabelsProps: Partial) { if (typeof value === 'string' || typeof value === 'number') { return <>{value}; } if (kind === LogsMetaKind.LabelsMap) { - return ; + return ; } if (kind === LogsMetaKind.Error) { return {value.toString()}; diff --git a/public/app/features/explore/Logs/utils/logs.ts b/public/app/features/explore/Logs/utils/logs.ts index 1e8cd037436..51926b33c87 100644 --- a/public/app/features/explore/Logs/utils/logs.ts +++ b/public/app/features/explore/Logs/utils/logs.ts @@ -5,6 +5,7 @@ export const SETTINGS_KEYS = { prettifyLogMessage: 'grafana.explore.logs.prettifyLogMessage', logsSortOrder: 'grafana.explore.logs.sortOrder', logContextWrapLogMessage: 'grafana.explore.logs.logContext.wrapLogMessage', + commonLabels: 'grafana.explore.logs.commonLabels', }; export const SETTING_KEY_ROOT = 'grafana.explore.logs'; diff --git a/public/app/features/explore/MetaInfoText.tsx b/public/app/features/explore/MetaInfoText.tsx index 906ff0c2c9c..d2106cb155d 100644 --- a/public/app/features/explore/MetaInfoText.tsx +++ b/public/app/features/explore/MetaInfoText.tsx @@ -26,6 +26,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ marginRight: `calc(${theme.spacing(2)} / 2)`, fontSize: theme.typography.bodySmall.fontSize, fontWeight: theme.typography.fontWeightMedium, + whiteSpace: 'nowrap', }), metaValue: css({ fontFamily: theme.typography.fontFamilyMonospace, diff --git a/public/app/features/logs/components/ControlledLogRows.tsx b/public/app/features/logs/components/ControlledLogRows.tsx index dba49b9df09..a08a7955f95 100644 --- a/public/app/features/logs/components/ControlledLogRows.tsx +++ b/public/app/features/logs/components/ControlledLogRows.tsx @@ -25,6 +25,7 @@ import { LogListControls } from './panel/LogListControls'; import { ScrollToLogsEvent } from './panel/virtualization'; export interface ControlledLogRowsProps extends Omit { + hasUnescapedContent?: boolean; loading: boolean; logsMeta?: LogsMetaItem[]; loadMoreLogs?: (range: AbsoluteTimeRange) => void; @@ -50,6 +51,7 @@ export type LogRowsComponentProps = Omit< export const ControlledLogRows = ({ deduplicatedRows, dedupStrategy, + hasUnescapedContent, showLabels, showTime, logsMeta, @@ -65,6 +67,7 @@ export const ControlledLogRows = ({ app={rest.app || CoreApp.Unknown} displayedFields={[]} dedupStrategy={dedupStrategy} + hasUnescapedContent={hasUnescapedContent} logOptionsStorageKey={logOptionsStorageKey} logs={deduplicatedRows ?? []} logsMeta={logsMeta} @@ -83,8 +86,17 @@ export const ControlledLogRows = ({ }; const LogRowsComponent = ({ loading, loadMoreLogs, deduplicatedRows = [], range, ...rest }: LogRowsComponentProps) => { - const { app, dedupStrategy, filterLevels, prettifyJSON, sortOrder, showTime, showUniqueLabels, wrapLogMessage } = - useLogListContext(); + const { + app, + dedupStrategy, + filterLevels, + forceEscape, + prettifyJSON, + sortOrder, + showTime, + showUniqueLabels, + wrapLogMessage, + } = useLogListContext(); const eventBus = useMemo(() => new EventBusSrv(), []); const scrollElementRef = useRef(null); @@ -124,6 +136,7 @@ const LogRowsComponent = ({ loading, loadMoreLogs, deduplicatedRows = [], range, app={app} dedupStrategy={dedupStrategy} deduplicatedRows={filteredLogs} + forceEscape={forceEscape} logRows={filteredLogs} logsSortOrder={sortOrder} scrollElement={scrollElementRef.current} diff --git a/public/app/features/logs/components/LogLabels.test.tsx b/public/app/features/logs/components/LogLabels.test.tsx index f736b0a595c..b70af572021 100644 --- a/public/app/features/logs/components/LogLabels.test.tsx +++ b/public/app/features/logs/components/LogLabels.test.tsx @@ -35,6 +35,60 @@ describe('', () => { await userEvent.hover(screen.getByText('foo=bar')); expect(screen.getAllByText('foo=bar')).toHaveLength(1); }); + describe('displayMax', () => { + it('renders up to displayMax labels', () => { + render(); + expect(screen.getByText('foo=bar')).toBeInTheDocument(); + expect(screen.getByLabelText('Expand labels')).toBeInTheDocument(); + expect(screen.queryByText('baz=42')).not.toBeInTheDocument(); + }); + + it('allows to render all labels', async () => { + const onDisplayMaxToggle = jest.fn(); + const { rerender } = render( + + ); + + await userEvent.click(screen.getByLabelText('Expand labels')); + expect(onDisplayMaxToggle).toHaveBeenCalledTimes(1); + expect(onDisplayMaxToggle).toHaveBeenCalledWith(true); + + rerender(); + + expect(screen.getByText('foo=bar')).toBeInTheDocument(); + expect(screen.getByText('baz=42')).toBeInTheDocument(); + expect(screen.getByLabelText('Collapse labels')).toBeInTheDocument(); + }); + + it('allows to collapse labels', async () => { + const onDisplayMaxToggle = jest.fn(); + const { rerender } = render( + + ); + + await userEvent.click(screen.getByLabelText('Collapse labels')); + expect(onDisplayMaxToggle).toHaveBeenCalledTimes(1); + expect(onDisplayMaxToggle).toHaveBeenCalledWith(false); + + rerender( + + ); + + expect(screen.getByText('foo=bar')).toBeInTheDocument(); + expect(screen.getByLabelText('Expand labels')).toBeInTheDocument(); + expect(screen.queryByText('baz=42')).not.toBeInTheDocument(); + }); + }); }); describe('', () => { diff --git a/public/app/features/logs/components/LogLabels.tsx b/public/app/features/logs/components/LogLabels.tsx index fa675287a72..6b363383906 100644 --- a/public/app/features/logs/components/LogLabels.tsx +++ b/public/app/features/logs/components/LogLabels.tsx @@ -1,54 +1,101 @@ -import { css, cx } from '@emotion/css'; -import { memo, forwardRef, useMemo } from 'react'; +import { css } from '@emotion/css'; +import { memo, forwardRef, useMemo, useState } from 'react'; import { GrafanaTheme2, Labels } from '@grafana/data'; -import { Tooltip, useStyles2 } from '@grafana/ui'; +import { Button, Icon, Tooltip, useStyles2 } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; import { LOG_LINE_BODY_FIELD_NAME } from './LogDetailsBody'; // Levels are already encoded in color, filename is a Loki-ism const HIDDEN_LABELS = ['detected_level', 'level', 'lvl', 'filename']; -interface Props { +export interface Props { labels: Labels; emptyMessage?: string; addTooltip?: boolean; + displayMax?: number; + displayAll?: boolean; + onDisplayMaxToggle?(state: boolean): void; } -export const LogLabels = memo(({ labels, emptyMessage, addTooltip = true }: Props) => { - const styles = useStyles2(getStyles); - const displayLabels = useMemo( - () => - Object.keys(labels) - .filter((label) => !label.startsWith('_') && !HIDDEN_LABELS.includes(label) && labels[label]) - .map((label) => `${label}=${labels[label]}`), - [labels] - ); +export const LogLabels = memo( + ({ + labels, + emptyMessage, + addTooltip = true, + displayMax, + onDisplayMaxToggle, + displayAll: initialDisplayAll = false, + }: Props) => { + const [displayAll, setDisplayAll] = useState(displayMax ? initialDisplayAll : undefined); + const styles = useStyles2(getStyles); + const allLabels = useMemo( + () => + Object.keys(labels) + .filter((label) => !label.startsWith('_') && !HIDDEN_LABELS.includes(label) && labels[label]) + .map((label) => `${label}=${labels[label]}`), + [labels] + ); + const displayLabels = useMemo( + () => allLabels.slice(0, !displayAll && displayMax ? displayMax : Infinity), + [allLabels, displayAll, displayMax] + ); + + if (displayLabels.length === 0 && emptyMessage) { + return ( + + {emptyMessage} + + ); + } - if (displayLabels.length === 0 && emptyMessage) { return ( - - {emptyMessage} + + {displayLabels.map((labelValue) => { + return addTooltip ? ( + + {labelValue} + + ) : ( + + {labelValue} + + ); + })} + {displayLabels.length < allLabels.length && !displayAll && ( + + )} + {displayAll === true && ( + + )} ); } - - return ( - - {displayLabels.map((labelValue) => { - return addTooltip ? ( - - {labelValue} - - ) : ( - - {labelValue} - - ); - })} - - ); -}); +); LogLabels.displayName = 'LogLabels'; interface LogLabelsArrayProps { @@ -58,7 +105,7 @@ interface LogLabelsArrayProps { export const LogLabelsList = memo(({ labels }: LogLabelsArrayProps) => { const styles = useStyles2(getStyles); return ( - + {labels.map((label) => ( {label === LOG_LINE_BODY_FIELD_NAME ? 'log line' : label} @@ -77,8 +124,8 @@ interface LogLabelProps { const LogLabel = forwardRef(({ styles, tooltip, children }: LogLabelProps, ref) => { return ( - - + + {children} @@ -92,6 +139,7 @@ const getStyles = (theme: GrafanaTheme2) => { display: 'flex', flexWrap: 'wrap', fontSize: theme.typography.size.xs, + alignItems: 'center', }), logsLabel: css({ label: 'logs-label', @@ -103,6 +151,7 @@ const getStyles = (theme: GrafanaTheme2) => { textOverflow: 'ellipsis', whiteSpace: 'nowrap', overflow: 'hidden', + maxHeight: theme.spacing(2), }), logsLabelValue: css({ label: 'logs-label__value', diff --git a/public/app/features/logs/components/panel/LogList.tsx b/public/app/features/logs/components/panel/LogList.tsx index 366293af4ae..f74acfdf181 100644 --- a/public/app/features/logs/components/panel/LogList.tsx +++ b/public/app/features/logs/components/panel/LogList.tsx @@ -47,7 +47,6 @@ interface Props { displayedFields: string[]; eventBus?: EventBus; filterLevels?: LogLevel[]; - forceEscape?: boolean; getFieldLinks?: GetFieldLinksFn; getRowContextQuery?: GetRowContextQueryFn; grammar?: Grammar; @@ -89,7 +88,6 @@ export const LogList = ({ dedupStrategy, eventBus, filterLevels, - forceEscape = false, getFieldLinks, getRowContextQuery, grammar, @@ -143,7 +141,6 @@ export const LogList = ({ { - const { app, displayedFields, filterLevels, showTime, sortOrder, wrapLogMessage } = useLogListContext(); + const { app, displayedFields, filterLevels, forceEscape, showTime, sortOrder, wrapLogMessage } = useLogListContext(); const [processedLogs, setProcessedLogs] = useState([]); const [listHeight, setListHeight] = useState( app === CoreApp.Explore ? window.innerHeight * 0.75 : containerElement.clientHeight @@ -197,7 +193,9 @@ const LogListComponent = ({ }, [eventBus, logs.length]); useEffect(() => { - setProcessedLogs(preProcessLogs(logs, { getFieldLinks, escape: forceEscape, order: sortOrder, timeZone }, grammar)); + setProcessedLogs( + preProcessLogs(logs, { getFieldLinks, escape: forceEscape ?? false, order: sortOrder, timeZone }, grammar) + ); }, [forceEscape, getFieldLinks, grammar, logs, sortOrder, timeZone]); useEffect(() => { diff --git a/public/app/features/logs/components/panel/LogListContext.tsx b/public/app/features/logs/components/panel/LogListContext.tsx index 38de4bfab75..7a1269d28be 100644 --- a/public/app/features/logs/components/panel/LogListContext.tsx +++ b/public/app/features/logs/components/panel/LogListContext.tsx @@ -28,9 +28,11 @@ import { GetRowContextQueryFn } from './LogLineMenu'; export interface LogListContextData extends Omit { downloadLogs: (format: DownloadFormat) => void; filterLevels: LogLevel[]; + hasUnescapedContent?: boolean; setDedupStrategy: (dedupStrategy: LogsDedupStrategy) => void; setDisplayedFields: (displayedFields: string[]) => void; setFilterLevels: (filterLevels: LogLevel[]) => void; + setForceEscape: (forceEscape: boolean) => void; setLogListState: Dispatch>; setPinnedLogs: (pinnedlogs: string[]) => void; setPrettifyJSON: (prettifyJSON: boolean) => void; @@ -47,9 +49,11 @@ export const LogListContext = createContext({ displayedFields: [], downloadLogs: () => {}, filterLevels: [], + hasUnescapedContent: false, setDedupStrategy: () => {}, setDisplayedFields: () => {}, setFilterLevels: () => {}, + setForceEscape: () => {}, setLogListState: () => {}, setPinnedLogs: () => {}, setPrettifyJSON: () => {}, @@ -82,7 +86,9 @@ export type LogListState = Pick< LogListContextData, | 'dedupStrategy' | 'displayedFields' + | 'forceEscape' | 'filterLevels' + | 'hasUnescapedContent' | 'pinnedLogs' | 'prettifyJSON' | 'showUniqueLabels' @@ -98,6 +104,8 @@ export interface Props { dedupStrategy: LogsDedupStrategy; displayedFields: string[]; filterLevels?: LogLevel[]; + forceEscape?: boolean; + hasUnescapedContent?: boolean; getRowContextQuery?: GetRowContextQueryFn; logs: LogRowModel[]; logsMeta?: LogsMetaItem[]; @@ -125,11 +133,13 @@ export const LogListContextProvider = ({ children, dedupStrategy, displayedFields, + filterLevels, + forceEscape = false, + hasUnescapedContent, getRowContextQuery, logs, logsMeta, logOptionsStorageKey, - filterLevels, logSupportsContext, onLogOptionsChange, onLogLineHover, @@ -152,6 +162,8 @@ export const LogListContextProvider = ({ displayedFields, filterLevels: filterLevels ?? (logOptionsStorageKey ? store.getObject(`${logOptionsStorageKey}.filterLevels`, []) : []), + forceEscape, + hasUnescapedContent, pinnedLogs, prettifyJSON, showTime, @@ -205,6 +217,12 @@ export const LogListContextProvider = ({ } }, [filterLevels, logListState]); + useEffect(() => { + if (logListState.hasUnescapedContent !== hasUnescapedContent) { + setLogListState({ ...logListState, hasUnescapedContent }); + } + }, [hasUnescapedContent, logListState]); + const setDedupStrategy = useCallback( (dedupStrategy: LogsDedupStrategy) => { setLogListState({ ...logListState, dedupStrategy }); @@ -221,6 +239,13 @@ export const LogListContextProvider = ({ [logListState, onLogOptionsChange] ); + const setForceEscape = useCallback( + (forceEscape: boolean) => { + setLogListState({ ...logListState, forceEscape }); + }, + [logListState] + ); + const setFilterLevels = useCallback( (filterLevels: LogLevel[]) => { setLogListState({ ...logListState, filterLevels }); @@ -322,6 +347,8 @@ export const LogListContextProvider = ({ displayedFields: logListState.displayedFields, downloadLogs, filterLevels: logListState.filterLevels, + forceEscape: logListState.forceEscape, + hasUnescapedContent: logListState.hasUnescapedContent, getRowContextQuery, logSupportsContext, onLogLineHover, @@ -335,6 +362,7 @@ export const LogListContextProvider = ({ setDedupStrategy, setDisplayedFields, setFilterLevels, + setForceEscape, setLogListState, setPinnedLogs, setPrettifyJSON, diff --git a/public/app/features/logs/components/panel/LogListControls.test.tsx b/public/app/features/logs/components/panel/LogListControls.test.tsx index ec35cf0fa84..2471506d8d5 100644 --- a/public/app/features/logs/components/panel/LogListControls.test.tsx +++ b/public/app/features/logs/components/panel/LogListControls.test.tsx @@ -41,6 +41,10 @@ describe('LogListControls', () => { expect(screen.getByLabelText('Scroll to top')).toBeInTheDocument(); expect(screen.queryByLabelText('Show unique labels')).not.toBeInTheDocument(); expect(screen.queryByLabelText('Expand JSON logs')).not.toBeInTheDocument(); + expect( + screen.queryByLabelText('Fix incorrectly escaped newline and tab sequences in log lines') + ).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Remove escaping')).not.toBeInTheDocument(); }); test('Renders legacy controls', () => { @@ -242,4 +246,19 @@ describe('LogListControls', () => { await userEvent.click(await screen.findByText('txt')); expect(downloadLogs).toHaveBeenCalledWith('text', filteredLogs, undefined); }); + + test('Controls new lines', async () => { + const { rerender } = render( + + + + ); + await userEvent.click(screen.getByLabelText('Fix incorrectly escaped newline and tab sequences in log lines')); + rerender( + + + + ); + await userEvent.click(screen.getByLabelText('Remove escaping')); + }); }); diff --git a/public/app/features/logs/components/panel/LogListControls.tsx b/public/app/features/logs/components/panel/LogListControls.tsx index 2eccad4c4e3..23159176475 100644 --- a/public/app/features/logs/components/panel/LogListControls.tsx +++ b/public/app/features/logs/components/panel/LogListControls.tsx @@ -42,9 +42,12 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) dedupStrategy, downloadLogs, filterLevels, + forceEscape, + hasUnescapedContent, prettifyJSON, setDedupStrategy, setFilterLevels, + setForceEscape, setPrettifyJSON, setShowTime, setShowUniqueLabels, @@ -76,6 +79,11 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) ); }, [eventBus]); + const onForceEscapeClick = useCallback(() => { + reportInteraction('logs_log_list_controls_force_escape_clicked'); + setForceEscape(!forceEscape); + }, [forceEscape, setForceEscape]); + const onFilterLevelClick = useCallback( (level?: LogLevel) => { reportInteraction('logs_log_list_controls_level_clicked'); @@ -310,6 +318,23 @@ export const LogListControls = ({ eventBus, visualisationType = 'logs' }: Props) size="lg" /> )} + {hasUnescapedContent && ( + + )} )} {!config.exploreHideLogsDownload && ( diff --git a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx index e514463374c..9cff7a3367b 100644 --- a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx +++ b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx @@ -13,6 +13,7 @@ export const LogListContext = createContext({ setDedupStrategy: () => {}, setDisplayedFields: () => {}, setFilterLevels: () => {}, + setForceEscape: () => {}, setLogListState: () => {}, setPinnedLogs: () => {}, setPrettifyJSON: () => {}, @@ -57,6 +58,7 @@ export const defaultProps = { setDedupStrategy: jest.fn(), setDisplayedFields: jest.fn(), setFilterLevels: jest.fn(), + setForceEscape: jest.fn(), setLogListState: jest.fn(), setPinnedLogs: jest.fn(), setShowTime: jest.fn(), @@ -108,6 +110,7 @@ export const LogListContextProvider = ({ setDedupStrategy: jest.fn(), setDisplayedFields: jest.fn(), setFilterLevels: jest.fn(), + setForceEscape: jest.fn(), setLogListState: jest.fn(), setPinnedLogs: jest.fn(), setPrettifyJSON: jest.fn(), diff --git a/public/app/features/logs/logsModel.test.ts b/public/app/features/logs/logsModel.test.ts index 69bf270c891..3c7b599c7f6 100644 --- a/public/app/features/logs/logsModel.test.ts +++ b/public/app/features/logs/logsModel.test.ts @@ -348,6 +348,11 @@ describe('dataFrameToLogsModel', () => { ]); expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta![0]).toMatchObject({ + label: '', + value: `2 lines returned`, + kind: LogsMetaKind.String, + }); + expect(logsModel.meta![1]).toMatchObject({ label: COMMON_LABELS, value: { filename: '/var/log/grafana/grafana.log', @@ -355,11 +360,6 @@ describe('dataFrameToLogsModel', () => { }, kind: LogsMetaKind.LabelsMap, }); - expect(logsModel.meta![1]).toMatchObject({ - label: '', - value: `2 lines returned`, - kind: LogsMetaKind.String, - }); }); it('given one series should return expected logs model with detected_level', () => { @@ -430,17 +430,17 @@ describe('dataFrameToLogsModel', () => { ]); expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta![0]).toMatchObject({ + label: '', + value: `2 lines returned`, + kind: LogsMetaKind.String, + }); + expect(logsModel.meta![1]).toMatchObject({ label: COMMON_LABELS, value: { job: 'grafana', }, kind: LogsMetaKind.LabelsMap, }); - expect(logsModel.meta![1]).toMatchObject({ - label: '', - value: `2 lines returned`, - kind: LogsMetaKind.String, - }); }); it('with infinite scrolling enabled it should return expected logs model', () => { @@ -485,7 +485,7 @@ describe('dataFrameToLogsModel', () => { it('given one series with limit as custom meta property should return correct limit', () => { const series: DataFrame[] = getTestDataFrame(); const logsModel = dataFrameToLogsModel(series, 1); - expect(logsModel.meta![1]).toMatchObject({ + expect(logsModel.meta![0]).toMatchObject({ label: '', value: `2 lines returned`, kind: LogsMetaKind.String, @@ -516,16 +516,16 @@ describe('dataFrameToLogsModel', () => { queries ); expect(logsModel.meta).toEqual([ - { - label: 'Common labels', - value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' }, - kind: 2, - }, { label: '', value: '2 lines shown — 21.85% (5h 14min 40sec) of 24h', kind: 1, }, + { + label: 'Common labels', + value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' }, + kind: 2, + }, ]); }); @@ -554,16 +554,16 @@ describe('dataFrameToLogsModel', () => { queries ); expect(logsModel.meta).toEqual([ - { - label: 'Common labels', - value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' }, - kind: 2, - }, { label: '', value: '2 reached', kind: 1, }, + { + label: 'Common labels', + value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' }, + kind: 2, + }, ]); }); @@ -649,15 +649,15 @@ describe('dataFrameToLogsModel', () => { ]); expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta![0]).toMatchObject({ - label: COMMON_LABELS, - value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' }, - kind: LogsMetaKind.LabelsMap, - }); - expect(logsModel.meta![1]).toMatchObject({ label: '', value: `2 lines returned`, kind: LogsMetaKind.String, }); + expect(logsModel.meta![1]).toMatchObject({ + label: COMMON_LABELS, + value: { filename: '/var/log/grafana/grafana.log', job: 'grafana' }, + kind: LogsMetaKind.LabelsMap, + }); }); it('given one series with labels-field it should work regardless the label-fields position', () => { @@ -768,20 +768,20 @@ describe('dataFrameToLogsModel', () => { expect(logsModel.series).toHaveLength(2); expect(logsModel.meta).toHaveLength(3); expect(logsModel.meta![0]).toMatchObject({ - label: COMMON_LABELS, - value: series[0].fields[1].labels, - kind: LogsMetaKind.LabelsMap, - }); - expect(logsModel.meta![1]).toMatchObject({ label: '', value: `2 lines returned`, kind: LogsMetaKind.String, }); - expect(logsModel.meta![2]).toMatchObject({ + expect(logsModel.meta![1]).toMatchObject({ label: '', value: 'Error when parsing some of the logs', kind: LogsMetaKind.Error, }); + expect(logsModel.meta![2]).toMatchObject({ + label: COMMON_LABELS, + value: series[0].fields[1].labels, + kind: LogsMetaKind.LabelsMap, + }); }); it('given one series without labels should return expected logs model', () => { @@ -1110,15 +1110,15 @@ describe('dataFrameToLogsModel', () => { const logsModel = dataFrameToLogsModel(series, 1, { from: 1556270591353, to: 1556289770991 }); expect(logsModel.meta).toHaveLength(2); expect(logsModel.meta![0]).toMatchObject({ - label: COMMON_LABELS, - value: series[0].fields[1].labels, - kind: LogsMetaKind.LabelsMap, - }); - expect(logsModel.meta![1]).toMatchObject({ label: '', value: `2 lines shown — 98.44% (5h 14min 40sec) of 5h 19min 40sec`, kind: LogsMetaKind.String, }); + expect(logsModel.meta![1]).toMatchObject({ + label: COMMON_LABELS, + value: series[0].fields[1].labels, + kind: LogsMetaKind.LabelsMap, + }); }); it('should fallback to row index if no id', () => { @@ -1271,9 +1271,9 @@ describe('logSeriesToLogsModel', () => { const logsModel = dataFrameToLogsModel(logSeries, 0); expect(logsModel.meta).toMatchObject([ - { kind: 2, label: COMMON_LABELS, value: { foo: 'bar', level: 'dbug' } }, { kind: 0, label: LIMIT_LABEL, value: 2000 }, { kind: 1, label: 'Total bytes processed', value: '194 kB' }, + { kind: 2, label: COMMON_LABELS, value: { foo: 'bar', level: 'dbug' } }, ]); expect(logsModel.rows).toHaveLength(3); expect(logsModel.rows).toMatchObject([ diff --git a/public/app/features/logs/logsModel.ts b/public/app/features/logs/logsModel.ts index 12aebd4368e..7e75684d423 100644 --- a/public/app/features/logs/logsModel.ts +++ b/public/app/features/logs/logsModel.ts @@ -473,13 +473,6 @@ export function logSeriesToLogsModel( // Meta data to display in status const meta: LogsMetaItem[] = []; - if (size(commonLabels) > 0) { - meta.push({ - label: COMMON_LABELS, - value: commonLabels, - kind: LogsMetaKind.LabelsMap, - }); - } // Data sources that set up searchWords on backend use meta.custom.limit. // Data sources that set up searchWords through frontend can use meta.limit. const limits = logSeries.filter((series) => series?.meta?.custom?.limit ?? series?.meta?.limit); @@ -538,6 +531,14 @@ export function logSeriesToLogsModel( }); } + if (size(commonLabels) > 0) { + meta.push({ + label: COMMON_LABELS, + value: commonLabels, + kind: LogsMetaKind.LabelsMap, + }); + } + return { hasUniqueLabels, meta, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index de8e8e8e608..b40b2e2a119 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5269,6 +5269,10 @@ "tooltip-show-log-line": "Show log line" } }, + "log-labels": { + "collapse": "Collapse labels", + "expand": "Expand labels" + }, "log-line-menu": { "copy-link": "Copy link to log line", "copy-log": "Copy log line", @@ -5321,11 +5325,13 @@ "txt": "txt" }, "enable-highlighting": "Enable highlighting", + "escape-newlines": "Fix incorrectly escaped newline and tab sequences in log lines", "hide-timestamps": "Hide timestamps", "hide-unique-labels": "Hide unique labels", "newest-first": "Newest logs first", "oldest-first": "Oldest logs first", "prettify-json": "Expand JSON logs", + "remove-escaping": "Remove escaping", "scroll-bottom": "Scroll to bottom", "scroll-top": "Scroll to top", "show-timestamps": "Show timestamps", From acd843303ebb27c9423ea7bb4a77fa45258ad1d8 Mon Sep 17 00:00:00 2001 From: Sarah Zinger Date: Thu, 10 Apr 2025 14:51:44 -0400 Subject: [PATCH 04/37] SQL Expression: Add instrumentation for sql expressions (#103758) --- pkg/expr/classic/classic.go | 3 +- pkg/expr/classic/classic_test.go | 2 +- pkg/expr/commands.go | 10 ++- pkg/expr/commands_test.go | 12 +-- pkg/expr/converter_test.go | 3 +- pkg/expr/dataplane_test.go | 3 +- pkg/expr/hysteresis.go | 13 +-- pkg/expr/hysteresis_test.go | 2 +- pkg/expr/metrics.go | 47 ----------- pkg/expr/metrics/metrics.go | 125 ++++++++++++++++++++++++++++ pkg/expr/ml.go | 2 +- pkg/expr/ml_test.go | 5 +- pkg/expr/nodes.go | 6 +- pkg/expr/service.go | 5 +- pkg/expr/service_test.go | 3 +- pkg/expr/sql_command.go | 25 ++++-- pkg/expr/sql_command_test.go | 26 +++++- pkg/expr/threshold.go | 3 +- pkg/expr/threshold_bench_test.go | 10 +-- pkg/expr/threshold_test.go | 2 +- pkg/expr/transform.go | 2 +- pkg/registry/apis/query/metrics.go | 48 ----------- pkg/registry/apis/query/query.go | 4 +- pkg/registry/apis/query/register.go | 5 +- 24 files changed, 223 insertions(+), 143 deletions(-) delete mode 100644 pkg/expr/metrics.go create mode 100644 pkg/expr/metrics/metrics.go delete mode 100644 pkg/registry/apis/query/metrics.go diff --git a/pkg/expr/classic/classic.go b/pkg/expr/classic/classic.go index 0f42ac45d6f..63cd0cc6ef9 100644 --- a/pkg/expr/classic/classic.go +++ b/pkg/expr/classic/classic.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/expr/metrics" "github.com/grafana/grafana/pkg/infra/tracing" ) @@ -69,7 +70,7 @@ func (cmd *ConditionsCmd) NeedsVars() []string { // Execute runs the command and returns the results or an error if the command // failed to execute. -func (cmd *ConditionsCmd) Execute(ctx context.Context, t time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) { +func (cmd *ConditionsCmd) Execute(ctx context.Context, t time.Time, vars mathexp.Vars, tracer tracing.Tracer, _ *metrics.ExprMetrics) (mathexp.Results, error) { ctx, span := tracer.Start(ctx, "SSE.ExecuteClassicConditions") defer span.End() // isFiring and isNoData contains the outcome of ConditionsCmd, and is derived from the diff --git a/pkg/expr/classic/classic_test.go b/pkg/expr/classic/classic_test.go index 14db0059f0f..a05e63d4f50 100644 --- a/pkg/expr/classic/classic_test.go +++ b/pkg/expr/classic/classic_test.go @@ -639,7 +639,7 @@ func TestConditionsCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - res, err := tt.cmd.Execute(context.Background(), time.Now(), tt.vars, tracing.InitializeTracerForTest()) + res, err := tt.cmd.Execute(context.Background(), time.Now(), tt.vars, tracing.InitializeTracerForTest(), nil) require.NoError(t, err) require.Equal(t, tt.expected(), res) }) diff --git a/pkg/expr/commands.go b/pkg/expr/commands.go index cc8f1f9fda6..5b18ce1a104 100644 --- a/pkg/expr/commands.go +++ b/pkg/expr/commands.go @@ -12,13 +12,15 @@ import ( "go.opentelemetry.io/otel/attribute" "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/expr/metrics" + "github.com/grafana/grafana/pkg/infra/tracing" ) // Command is an interface for all expression commands. type Command interface { NeedsVars() []string - Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) + Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer, metrics *metrics.ExprMetrics) (mathexp.Results, error) Type() string } @@ -69,7 +71,7 @@ func (gm *MathCommand) NeedsVars() []string { // Execute runs the command and returns the results or an error if the command // failed to execute. -func (gm *MathCommand) Execute(ctx context.Context, _ time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) { +func (gm *MathCommand) Execute(ctx context.Context, _ time.Time, vars mathexp.Vars, tracer tracing.Tracer, _ *metrics.ExprMetrics) (mathexp.Results, error) { _, span := tracer.Start(ctx, "SSE.ExecuteMath") span.SetAttributes(attribute.String("expression", gm.RawExpression)) defer span.End() @@ -165,7 +167,7 @@ func (gr *ReduceCommand) NeedsVars() []string { // Execute runs the command and returns the results or an error if the command // failed to execute. -func (gr *ReduceCommand) Execute(ctx context.Context, _ time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) { +func (gr *ReduceCommand) Execute(ctx context.Context, _ time.Time, vars mathexp.Vars, tracer tracing.Tracer, _ *metrics.ExprMetrics) (mathexp.Results, error) { _, span := tracer.Start(ctx, "SSE.ExecuteReduce") defer span.End() @@ -295,7 +297,7 @@ func (gr *ResampleCommand) NeedsVars() []string { // Execute runs the command and returns the results or an error if the command // failed to execute. -func (gr *ResampleCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) { +func (gr *ResampleCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer, _ *metrics.ExprMetrics) (mathexp.Results, error) { _, span := tracer.Start(ctx, "SSE.ExecuteResample") defer span.End() newRes := mathexp.Results{} diff --git a/pkg/expr/commands_test.go b/pkg/expr/commands_test.go index 7fd58329171..dfe80046c90 100644 --- a/pkg/expr/commands_test.go +++ b/pkg/expr/commands_test.go @@ -119,7 +119,7 @@ func TestReduceExecute(t *testing.T) { }, } - execute, err := cmd.Execute(context.Background(), time.Now(), vars, tracing.InitializeTracerForTest()) + execute, err := cmd.Execute(context.Background(), time.Now(), vars, tracing.InitializeTracerForTest(), nil) require.NoError(t, err) require.Len(t, execute.Values, len(numbers)) @@ -163,7 +163,7 @@ func TestReduceExecute(t *testing.T) { t.Run("drop all non numbers if mapper is DropNonNumber", func(t *testing.T) { cmd, err := NewReduceCommand(util.GenerateShortUID(), randomReduceFunc(), varToReduce, &mathexp.DropNonNumber{}) require.NoError(t, err) - execute, err := cmd.Execute(context.Background(), time.Now(), vars, tracing.InitializeTracerForTest()) + execute, err := cmd.Execute(context.Background(), time.Now(), vars, tracing.InitializeTracerForTest(), nil) require.NoError(t, err) require.Len(t, execute.Values, 2) }) @@ -171,7 +171,7 @@ func TestReduceExecute(t *testing.T) { t.Run("replace all non numbers if mapper is ReplaceNonNumberWithValue", func(t *testing.T) { cmd, err := NewReduceCommand(util.GenerateShortUID(), randomReduceFunc(), varToReduce, &mathexp.ReplaceNonNumberWithValue{Value: 1}) require.NoError(t, err) - execute, err := cmd.Execute(context.Background(), time.Now(), vars, tracing.InitializeTracerForTest()) + execute, err := cmd.Execute(context.Background(), time.Now(), vars, tracing.InitializeTracerForTest(), nil) require.NoError(t, err) require.Len(t, execute.Values, len(numbers)) for _, value := range execute.Values[1 : len(numbers)-1] { @@ -194,7 +194,7 @@ func TestReduceExecute(t *testing.T) { } cmd, err := NewReduceCommand(util.GenerateShortUID(), randomReduceFunc(), varToReduce, nil) require.NoError(t, err) - results, err := cmd.Execute(context.Background(), time.Now(), vars, tracing.InitializeTracerForTest()) + results, err := cmd.Execute(context.Background(), time.Now(), vars, tracing.InitializeTracerForTest(), nil) require.NoError(t, err) require.Len(t, results.Values, 1) @@ -253,7 +253,7 @@ func TestResampleCommand_Execute(t *testing.T) { t.Run(test.name, func(t *testing.T) { result, err := cmd.Execute(context.Background(), time.Now(), mathexp.Vars{ varToReduce: mathexp.Results{Values: mathexp.Values{test.vals}}, - }, tracing.InitializeTracerForTest()) + }, tracing.InitializeTracerForTest(), nil) if test.isError { require.Error(t, err) } else { @@ -268,7 +268,7 @@ func TestResampleCommand_Execute(t *testing.T) { t.Run("should return empty result if input is nil Value", func(t *testing.T) { result, err := cmd.Execute(context.Background(), time.Now(), mathexp.Vars{ varToReduce: mathexp.Results{Values: mathexp.Values{nil}}, - }, tracing.InitializeTracerForTest()) + }, tracing.InitializeTracerForTest(), nil) require.Empty(t, result.Values) require.NoError(t, err) }) diff --git a/pkg/expr/converter_test.go b/pkg/expr/converter_test.go index d5bb8e64f74..45d04169c32 100644 --- a/pkg/expr/converter_test.go +++ b/pkg/expr/converter_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/expr/metrics" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -21,7 +22,7 @@ func TestConvertDataFramesToResults(t *testing.T) { cfg: setting.NewCfg(), features: featuremgmt.WithFeatures(), tracer: tracing.InitializeTracerForTest(), - metrics: newMetrics(nil), + metrics: metrics.NewSSEMetrics(nil), } converter := &ResultConverter{Features: s.features, Tracer: s.tracer} diff --git a/pkg/expr/dataplane_test.go b/pkg/expr/dataplane_test.go index 53094212f75..56039aa867c 100644 --- a/pkg/expr/dataplane_test.go +++ b/pkg/expr/dataplane_test.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/expr/metrics" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" @@ -64,7 +65,7 @@ func framesPassThroughService(t *testing.T, frames data.Frames) (data.Frames, er &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider()), tracer: tracing.InitializeTracerForTest(), - metrics: newMetrics(nil), + metrics: metrics.NewSSEMetrics(nil), converter: &ResultConverter{ Features: features, Tracer: tracing.InitializeTracerForTest(), diff --git a/pkg/expr/hysteresis.go b/pkg/expr/hysteresis.go index d55c78a801d..0911225dfdd 100644 --- a/pkg/expr/hysteresis.go +++ b/pkg/expr/hysteresis.go @@ -9,6 +9,7 @@ import ( "go.opentelemetry.io/otel/attribute" "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/expr/metrics" "github.com/grafana/grafana/pkg/infra/tracing" ) @@ -32,7 +33,7 @@ func (h *HysteresisCommand) NeedsVars() []string { return []string{h.ReferenceVar} } -func (h *HysteresisCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) { +func (h *HysteresisCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer, metrics *metrics.ExprMetrics) (mathexp.Results, error) { results := vars[h.ReferenceVar] logger := logger.FromContext(ctx) @@ -46,7 +47,7 @@ func (h *HysteresisCommand) Execute(ctx context.Context, now time.Time, vars mat return mathexp.Results{Values: mathexp.Values{mathexp.NewNoData()}}, nil } if len(h.LoadedDimensions) == 0 { - return h.LoadingThresholdFunc.Execute(traceCtx, now, vars, tracer) + return h.LoadingThresholdFunc.Execute(traceCtx, now, vars, tracer, metrics) } var loadedVals, unloadedVals mathexp.Values for _, value := range results.Values { @@ -62,10 +63,10 @@ func (h *HysteresisCommand) Execute(ctx context.Context, now time.Time, vars mat logger.Debug("Evaluating thresholds", "unloadingThresholdDimensions", len(loadedVals), "loadingThresholdDimensions", len(unloadedVals)) if len(loadedVals) == 0 { // if all values are unloaded - return h.LoadingThresholdFunc.Execute(traceCtx, now, vars, tracer) + return h.LoadingThresholdFunc.Execute(traceCtx, now, vars, tracer, metrics) } if len(unloadedVals) == 0 { // if all values are loaded - return h.UnloadingThresholdFunc.Execute(traceCtx, now, vars, tracer) + return h.UnloadingThresholdFunc.Execute(traceCtx, now, vars, tracer, metrics) } defer func() { @@ -74,12 +75,12 @@ func (h *HysteresisCommand) Execute(ctx context.Context, now time.Time, vars mat }() vars[h.ReferenceVar] = mathexp.Results{Values: unloadedVals} - loadingResults, err := h.LoadingThresholdFunc.Execute(traceCtx, now, vars, tracer) + loadingResults, err := h.LoadingThresholdFunc.Execute(traceCtx, now, vars, tracer, metrics) if err != nil { return mathexp.Results{}, fmt.Errorf("failed to execute loading threshold: %w", err) } vars[h.ReferenceVar] = mathexp.Results{Values: loadedVals} - unloadingResults, err := h.UnloadingThresholdFunc.Execute(traceCtx, now, vars, tracer) + unloadingResults, err := h.UnloadingThresholdFunc.Execute(traceCtx, now, vars, tracer, metrics) if err != nil { return mathexp.Results{}, fmt.Errorf("failed to execute unloading threshold: %w", err) } diff --git a/pkg/expr/hysteresis_test.go b/pkg/expr/hysteresis_test.go index 31fd875213a..2f4f3181a5d 100644 --- a/pkg/expr/hysteresis_test.go +++ b/pkg/expr/hysteresis_test.go @@ -108,7 +108,7 @@ func TestHysteresisExecute(t *testing.T) { result, err := cmd.Execute(context.Background(), time.Now(), mathexp.Vars{ "A": mathexp.Results{Values: tc.input}, - }, tracer) + }, tracer, nil) if tc.expectedError != nil { require.ErrorIs(t, err, tc.expectedError) return diff --git a/pkg/expr/metrics.go b/pkg/expr/metrics.go deleted file mode 100644 index 4cdbdbbd7cd..00000000000 --- a/pkg/expr/metrics.go +++ /dev/null @@ -1,47 +0,0 @@ -package expr - -import ( - "github.com/prometheus/client_golang/prometheus" -) - -const ( - metricsSubSystem = "sse" - metricsNamespace = "grafana" -) - -type metrics struct { - dsRequests *prometheus.CounterVec - - // older metric - expressionsQuerySummary *prometheus.SummaryVec -} - -func newMetrics(reg prometheus.Registerer) *metrics { - m := &metrics{ - dsRequests: prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: metricsNamespace, - Subsystem: metricsSubSystem, - Name: "ds_queries_total", - Help: "Number of datasource queries made via server side expression requests", - }, []string{"error", "dataplane", "datasource_type"}), - - // older (No Namespace or Subsystem) - expressionsQuerySummary: prometheus.NewSummaryVec( - prometheus.SummaryOpts{ - Name: "expressions_queries_duration_milliseconds", - Help: "Expressions query summary", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, - []string{"status"}, - ), - } - - if reg != nil { - reg.MustRegister( - m.dsRequests, - m.expressionsQuerySummary, - ) - } - - return m -} diff --git a/pkg/expr/metrics/metrics.go b/pkg/expr/metrics/metrics.go new file mode 100644 index 00000000000..7f798941878 --- /dev/null +++ b/pkg/expr/metrics/metrics.go @@ -0,0 +1,125 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" +) + +// ExprMetrics is a struct that contains all the metrics for an implementation of the expressions service +// shared between multiple versions of expressions service, which are delineated by the subsystem string +type ExprMetrics struct { + DSRequests *prometheus.CounterVec + ExpressionsQuerySummary *prometheus.SummaryVec + SqlCommandDuration *prometheus.HistogramVec + SqlCommandErrorCount *prometheus.CounterVec + SqlCommandCellCount *prometheus.HistogramVec +} + +func newExprMetrics(subsystem string) *ExprMetrics { + return &ExprMetrics{ + DSRequests: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "grafana", + Subsystem: subsystem, + Name: "ds_queries_total", + Help: "Number of datasource queries made via server side expression requests", + }, []string{"error", "dataplane", "datasource_type"}), + + ExpressionsQuerySummary: prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Namespace: "grafana", + Subsystem: subsystem, + Name: "expressions_queries_duration_milliseconds", + Help: "Expressions query summary", + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"status"}, + ), + + SqlCommandDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "grafana", + Subsystem: subsystem, + Name: "sql_command_duration_seconds", + Help: "Duration of SQL command execution", + Buckets: prometheus.DefBuckets, + }, []string{"status"}), + + SqlCommandErrorCount: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: "grafana", + Subsystem: subsystem, + Name: "sql_command_errors_total", + Help: "Total number of SQL command execution errors", + }, []string{}), + + SqlCommandCellCount: prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "grafana", + Subsystem: subsystem, + Name: "sql_command_cell_count", + Help: "Distribution of the total number of cells in each SQL command execution", + Buckets: prometheus.ExponentialBuckets(100, 2, 10), + }, + []string{"status"}, + ), + } +} + +// NewSSEMetrics creates a new ExprMetrics struct for the ST implementation of the expressions service +func NewSSEMetrics(reg prometheus.Registerer) *ExprMetrics { + metricsSubSystem := "sse" + + m := &ExprMetrics{ + DSRequests: newExprMetrics(metricsSubSystem).DSRequests, + + ExpressionsQuerySummary: newExprMetrics(metricsSubSystem).ExpressionsQuerySummary, + + SqlCommandDuration: newExprMetrics(metricsSubSystem).SqlCommandDuration, + + SqlCommandErrorCount: newExprMetrics(metricsSubSystem).SqlCommandErrorCount, + + SqlCommandCellCount: newExprMetrics(metricsSubSystem).SqlCommandCellCount, + } + + if reg != nil { + reg.MustRegister( + m.DSRequests, + m.ExpressionsQuerySummary, + m.SqlCommandDuration, + m.SqlCommandErrorCount, + m.SqlCommandCellCount, + ) + } + + return m +} + +// NewQueryServiceExpressionsMetrics creates a new ExprMetrics struct for the query service implementation of the expressions service +func NewQueryServiceExpressionsMetrics(reg prometheus.Registerer) *ExprMetrics { + metricsSubSystem := "queryservice" + + m := &ExprMetrics{ + DSRequests: newExprMetrics(metricsSubSystem).DSRequests, + + ExpressionsQuerySummary: newExprMetrics(metricsSubSystem).ExpressionsQuerySummary, + + SqlCommandDuration: newExprMetrics(metricsSubSystem).SqlCommandDuration, + + SqlCommandErrorCount: newExprMetrics(metricsSubSystem).SqlCommandErrorCount, + + SqlCommandCellCount: newExprMetrics(metricsSubSystem).SqlCommandCellCount, + } + + if reg != nil { + reg.MustRegister( + m.DSRequests, + m.ExpressionsQuerySummary, + m.SqlCommandDuration, + m.SqlCommandErrorCount, + m.SqlCommandCellCount, + ) + } + + return m +} + +func NewTestMetrics() *ExprMetrics { + return newExprMetrics("test") +} diff --git a/pkg/expr/ml.go b/pkg/expr/ml.go index 9efa143e805..5903857ae2b 100644 --- a/pkg/expr/ml.go +++ b/pkg/expr/ml.go @@ -86,7 +86,7 @@ func (m *MLNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s * } logger.Debug("Data source queried", "responseType", responseType) useDataplane := strings.HasPrefix("dataplane-", responseType) - s.metrics.dsRequests.WithLabelValues(respStatus, fmt.Sprintf("%t", useDataplane), mlPluginID).Inc() + s.metrics.DSRequests.WithLabelValues(respStatus, fmt.Sprintf("%t", useDataplane), mlPluginID).Inc() }() // Execute the command and provide callback function for sending a request via plugin API. diff --git a/pkg/expr/ml_test.go b/pkg/expr/ml_test.go index e7d6768c466..045b7f1a34b 100644 --- a/pkg/expr/ml_test.go +++ b/pkg/expr/ml_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/expr/metrics" "github.com/grafana/grafana/pkg/expr/ml" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/user" @@ -61,7 +62,7 @@ func TestMLNodeExecute(t *testing.T) { features: nil, pluginsClient: pluginsClient, tracer: nil, - metrics: newMetrics(nil), + metrics: metrics.NewSSEMetrics(nil), } cmdResponse := data.NewFrame("test", @@ -198,7 +199,7 @@ func TestMLNodeExecute(t *testing.T) { features: nil, pluginsClient: pluginsClient, tracer: nil, - metrics: newMetrics(nil), + metrics: metrics.NewSSEMetrics(nil), } cmd := &ml.FakeCommand{ diff --git a/pkg/expr/nodes.go b/pkg/expr/nodes.go index e18bad3a7cb..b39e17a1384 100644 --- a/pkg/expr/nodes.go +++ b/pkg/expr/nodes.go @@ -103,7 +103,7 @@ func (gn *CMDNode) NeedsVars() []string { // other nodes they must have already been executed and their results must // already by in vars. func (gn *CMDNode) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, s *Service) (mathexp.Results, error) { - return gn.Command.Execute(ctx, now, vars, s.tracer) + return gn.Command.Execute(ctx, now, vars, s.tracer, s.metrics) } func buildCMDNode(rn *rawNode, toggles featuremgmt.FeatureToggles, sqlExpressionCellLimit int64) (*CMDNode, error) { @@ -320,7 +320,7 @@ func executeDSNodesGrouped(ctx context.Context, now time.Time, vars mathexp.Vars } logger.Debug("Data source queried", "responseType", responseType) useDataplane := strings.HasPrefix(responseType, "dataplane-") - s.metrics.dsRequests.WithLabelValues(respStatus, fmt.Sprintf("%t", useDataplane), firstNode.datasource.Type).Inc() + s.metrics.DSRequests.WithLabelValues(respStatus, fmt.Sprintf("%t", useDataplane), firstNode.datasource.Type).Inc() } resp, err := s.dataService.QueryData(ctx, req) @@ -395,7 +395,7 @@ func (dn *DSNode) Execute(ctx context.Context, now time.Time, _ mathexp.Vars, s } logger.Debug("Data source queried", "responseType", responseType) useDataplane := strings.HasPrefix(responseType, "dataplane-") - s.metrics.dsRequests.WithLabelValues(respStatus, fmt.Sprintf("%t", useDataplane), dn.datasource.Type).Inc() + s.metrics.DSRequests.WithLabelValues(respStatus, fmt.Sprintf("%t", useDataplane), dn.datasource.Type).Inc() }() resp, err := s.dataService.QueryData(ctx, req) diff --git a/pkg/expr/service.go b/pkg/expr/service.go index 1b04ce41c3f..e3ea54ea056 100644 --- a/pkg/expr/service.go +++ b/pkg/expr/service.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/expr/metrics" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/datasources" @@ -65,7 +66,7 @@ type Service struct { pluginsClient backend.CallResourceHandler tracer tracing.Tracer - metrics *metrics + metrics *metrics.ExprMetrics } type pluginContextProvider interface { @@ -81,7 +82,7 @@ func ProvideService(cfg *setting.Cfg, pluginClient plugins.Client, pCtxProvider pCtxProvider: pCtxProvider, features: features, tracer: tracer, - metrics: newMetrics(registerer), + metrics: metrics.NewSSEMetrics(registerer), pluginsClient: pluginClient, converter: &ResultConverter{ Features: features, diff --git a/pkg/expr/service_test.go b/pkg/expr/service_test.go index a343cdaf7b4..306113079db 100644 --- a/pkg/expr/service_test.go +++ b/pkg/expr/service_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/errutil" + "github.com/grafana/grafana/pkg/expr/metrics" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/datasources" @@ -249,7 +250,7 @@ func newMockQueryService(responses map[string]backend.DataResponse, queries []Qu pCtxProvider: pCtxProvider, features: featuremgmt.WithFeatures(), tracer: tracing.InitializeTracerForTest(), - metrics: newMetrics(nil), + metrics: metrics.NewSSEMetrics(nil), converter: &ResultConverter{ Features: features, Tracer: tracing.InitializeTracerForTest(), diff --git a/pkg/expr/sql_command.go b/pkg/expr/sql_command.go index 270ccc3530c..25207accf4a 100644 --- a/pkg/expr/sql_command.go +++ b/pkg/expr/sql_command.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/expr/metrics" "github.com/grafana/grafana/pkg/expr/sql" "github.com/grafana/grafana/pkg/infra/tracing" ) @@ -99,9 +100,22 @@ func (gr *SQLCommand) NeedsVars() []string { // Execute runs the command and returns the results or an error if the command // failed to execute. -func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) { +func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer, metrics *metrics.ExprMetrics) (mathExprResult mathexp.Results, resultError error) { _, span := tracer.Start(ctx, "SSE.ExecuteSQL") - defer span.End() + start := time.Now() + tc := int64(0) + + defer func() { + span.End() + statusLabel := "ok" + duration := float64(time.Since(start).Nanoseconds()) / float64(time.Millisecond) + if resultError != nil { + statusLabel = "error" + metrics.SqlCommandErrorCount.WithLabelValues().Inc() + } + metrics.SqlCommandDuration.WithLabelValues(statusLabel).Observe(duration) + metrics.SqlCommandCellCount.WithLabelValues(statusLabel).Observe(float64(tc)) + }() allFrames := []*data.Frame{} for _, ref := range gr.varsToQuery { @@ -114,14 +128,15 @@ func (gr *SQLCommand) Execute(ctx context.Context, now time.Time, vars mathexp.V allFrames = append(allFrames, frames...) } - totalCells := totalCells(allFrames) + tc = totalCells(allFrames) + // limit of 0 or less means no limit (following convention) - if gr.limit > 0 && totalCells > gr.limit { + if gr.limit > 0 && tc > gr.limit { return mathexp.Results{}, fmt.Errorf( "SQL expression: total cell count across all input tables exceeds limit of %d. Total cells: %d", gr.limit, - totalCells, + tc, ) } diff --git a/pkg/expr/sql_command_test.go b/pkg/expr/sql_command_test.go index 64654b34900..32ed9ee9934 100644 --- a/pkg/expr/sql_command_test.go +++ b/pkg/expr/sql_command_test.go @@ -10,6 +10,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/expr/metrics" + "github.com/prometheus/client_golang/prometheus/testutil" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" ) @@ -134,7 +136,7 @@ func TestSQLCommandCellLimits(t *testing.T) { } } - _, err = cmd.Execute(context.Background(), time.Now(), vars, &testTracer{}) + _, err = cmd.Execute(context.Background(), time.Now(), vars, &testTracer{}, metrics.NewTestMetrics()) if tt.expectError { require.Error(t, err) @@ -146,6 +148,28 @@ func TestSQLCommandCellLimits(t *testing.T) { } } +func TestSQLCommandMetrics(t *testing.T) { + // Create test metrics + m := metrics.NewTestMetrics() + + // Create a command + cmd, err := NewSQLCommand("A", "someformat", "select * from foo", 0) + require.NoError(t, err) + + // Execute successful command + _, err = cmd.Execute(context.Background(), time.Now(), mathexp.Vars{}, &testTracer{}, m) + require.NoError(t, err) + + // Verify error count was not incremented + require.Equal(t, 0, testutil.CollectAndCount(m.SqlCommandErrorCount), "Expected error metric not to be recorded") + + // Verify duration was recorded + require.Equal(t, 1, testutil.CollectAndCount(m.SqlCommandDuration), "Expected duration metric to be recorded") + + // Verify cell count was recorded + require.Equal(t, 1, testutil.CollectAndCount(m.SqlCommandCellCount), "Expected cell count metric to be recorded") +} + type testTracer struct { trace.Tracer } diff --git a/pkg/expr/threshold.go b/pkg/expr/threshold.go index d07a5d7676e..f9e9656e03e 100644 --- a/pkg/expr/threshold.go +++ b/pkg/expr/threshold.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/expr/metrics" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/util" @@ -174,7 +175,7 @@ func (tc *ThresholdCommand) NeedsVars() []string { return []string{tc.ReferenceVar} } -func (tc *ThresholdCommand) Execute(_ context.Context, _ time.Time, vars mathexp.Vars, _ tracing.Tracer) (mathexp.Results, error) { +func (tc *ThresholdCommand) Execute(_ context.Context, _ time.Time, vars mathexp.Vars, _ tracing.Tracer, _ *metrics.ExprMetrics) (mathexp.Results, error) { eval := func(maybeValue *float64) *float64 { if maybeValue == nil { return nil diff --git a/pkg/expr/threshold_bench_test.go b/pkg/expr/threshold_bench_test.go index ddda45eba97..13b42eb44b2 100644 --- a/pkg/expr/threshold_bench_test.go +++ b/pkg/expr/threshold_bench_test.go @@ -33,7 +33,7 @@ func BenchmarkThreshold(b *testing.B) { } b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = greater.Execute(ctx, timeNow, vars, trace) + _, _ = greater.Execute(ctx, timeNow, vars, trace, nil) } }) b.Run("less than", func(b *testing.B) { @@ -43,7 +43,7 @@ func BenchmarkThreshold(b *testing.B) { } b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = greater.Execute(ctx, timeNow, vars, trace) + _, _ = greater.Execute(ctx, timeNow, vars, trace, nil) } }) b.Run("within range", func(b *testing.B) { @@ -53,7 +53,7 @@ func BenchmarkThreshold(b *testing.B) { } b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = greater.Execute(ctx, timeNow, vars, trace) + _, _ = greater.Execute(ctx, timeNow, vars, trace, nil) } }) b.Run("within range, no labels", func(b *testing.B) { @@ -72,7 +72,7 @@ func BenchmarkThreshold(b *testing.B) { } b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = greater.Execute(ctx, timeNow, vars, trace) + _, _ = greater.Execute(ctx, timeNow, vars, trace, nil) } }) b.Run("outside range", func(b *testing.B) { @@ -82,7 +82,7 @@ func BenchmarkThreshold(b *testing.B) { } b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = greater.Execute(ctx, timeNow, vars, trace) + _, _ = greater.Execute(ctx, timeNow, vars, trace, nil) } }) } diff --git a/pkg/expr/threshold_test.go b/pkg/expr/threshold_test.go index 2cfcadab18f..0bdc695e7ed 100644 --- a/pkg/expr/threshold_test.go +++ b/pkg/expr/threshold_test.go @@ -639,7 +639,7 @@ func TestThresholdExecute(t *testing.T) { t.Run(name, func(t *testing.T) { result, err := cmd.Execute(context.Background(), time.Now(), mathexp.Vars{ "A": newResults(input[name]), - }, tracing.InitializeTracerForTest()) + }, tracing.InitializeTracerForTest(), nil) require.NoError(t, err) require.Equal(t, newResults(tc.expected[name]), result) }) diff --git a/pkg/expr/transform.go b/pkg/expr/transform.go index 57925e1da91..db49522eff1 100644 --- a/pkg/expr/transform.go +++ b/pkg/expr/transform.go @@ -81,7 +81,7 @@ func (s *Service) TransformData(ctx context.Context, now time.Time, req *Request respStatus = "failure" } duration := float64(time.Since(start).Nanoseconds()) / float64(time.Millisecond) - s.metrics.expressionsQuerySummary.WithLabelValues(respStatus).Observe(duration) + s.metrics.ExpressionsQuerySummary.WithLabelValues(respStatus).Observe(duration) span.End() }() diff --git a/pkg/registry/apis/query/metrics.go b/pkg/registry/apis/query/metrics.go deleted file mode 100644 index f3b8212f9db..00000000000 --- a/pkg/registry/apis/query/metrics.go +++ /dev/null @@ -1,48 +0,0 @@ -package query - -import ( - "github.com/prometheus/client_golang/prometheus" -) - -const ( - metricsSubSystem = "queryservice" - metricsNamespace = "grafana" -) - -type queryMetrics struct { - dsRequests *prometheus.CounterVec - - // older metric - expressionsQuerySummary *prometheus.SummaryVec -} - -func newQueryMetrics(reg prometheus.Registerer) *queryMetrics { - m := &queryMetrics{ - dsRequests: prometheus.NewCounterVec(prometheus.CounterOpts{ - Namespace: metricsNamespace, - Subsystem: metricsSubSystem, - Name: "ds_queries_total", - Help: "Number of datasource queries made from the query service", - }, []string{"error", "dataplane", "datasource_type"}), - - expressionsQuerySummary: prometheus.NewSummaryVec( - prometheus.SummaryOpts{ - Namespace: metricsNamespace, - Subsystem: metricsSubSystem, - Name: "expressions_queries_duration_milliseconds", - Help: "Expressions query summary", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, - []string{"status"}, - ), - } - - if reg != nil { - reg.MustRegister( - m.dsRequests, - m.expressionsQuerySummary, - ) - } - - return m -} diff --git a/pkg/registry/apis/query/query.go b/pkg/registry/apis/query/query.go index f019f058044..da73b222fab 100644 --- a/pkg/registry/apis/query/query.go +++ b/pkg/registry/apis/query/query.go @@ -360,7 +360,7 @@ func (b *QueryAPIBuilder) handleExpressions(ctx context.Context, req parsedReque respStatus = "failure" } duration := float64(time.Since(start).Nanoseconds()) / float64(time.Millisecond) - b.metrics.expressionsQuerySummary.WithLabelValues(respStatus).Observe(duration) + b.metrics.ExpressionsQuerySummary.WithLabelValues(respStatus).Observe(duration) span.End() }() @@ -403,7 +403,7 @@ func (b *QueryAPIBuilder) handleExpressions(ctx context.Context, req parsedReque } refId := expression.RefID - results, err := expression.Command.Execute(ctx, now, vars, b.tracer) + results, err := expression.Command.Execute(ctx, now, vars, b.tracer, b.metrics) if err != nil { expressionsLogger.Error("error executing expression", "error", err) results.Error = err diff --git a/pkg/registry/apis/query/register.go b/pkg/registry/apis/query/register.go index 83ca935a662..9692c21d2c7 100644 --- a/pkg/registry/apis/query/register.go +++ b/pkg/registry/apis/query/register.go @@ -17,6 +17,7 @@ import ( claims "github.com/grafana/authlib/types" query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" "github.com/grafana/grafana/pkg/expr" + "github.com/grafana/grafana/pkg/expr/metrics" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" @@ -43,7 +44,7 @@ type QueryAPIBuilder struct { authorizer authorizer.Authorizer tracer tracing.Tracer - metrics *queryMetrics + metrics *metrics.ExprMetrics parser *queryParser client clientapi.DataSourceClientSupplier registry query.DataSourceApiServerRegistry @@ -83,7 +84,7 @@ func NewQueryAPIBuilder(features featuremgmt.FeatureToggles, authorizer: ar, registry: registry, parser: newQueryParser(reader, legacy, tracer, log.New("query_parser")), - metrics: newQueryMetrics(registerer), + metrics: metrics.NewQueryServiceExpressionsMetrics(registerer), tracer: tracer, features: features, queryTypes: queryTypes, From 3551e6074cfd113a7fb04dd989d949e64f36e4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Thu, 10 Apr 2025 21:07:49 +0200 Subject: [PATCH 05/37] TraceView: Handle External correlation links correctly (#103594) * TraceView: Handle External correlation links correctly * Improve handling URLs --- .../SpanDetail/SpanDetailLinkButtons.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx index 73f650ee3c0..d3e272e9cc7 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanDetailLinkButtons.tsx @@ -49,6 +49,8 @@ const LINKS_ORDER = [ */ const MAX_LINKS = 3; +const ABSOLUTE_LINK_PATTERN = /^https?:\/\//i; + export const getSpanDetailLinkButtons = (props: Props) => { const { span, createSpanLink, traceToProfilesOptions, timeRange, datasourceType, app } = props; @@ -212,7 +214,22 @@ const createLinkModel = ( if (link.onClick) { link.onClick?.(event); } else { - locationService.push(link.href); + // TODO: Replace with https://github.com/grafana/grafana/issues/103593 + // We need to handle absolute and relative URLs correctly because when + // there are multiple links we group them into a dropdown and not use + // the grafana/ui DataLinkButton component which handles relative and + // absolute URLs nicely. A nice solution would be to have a separate + // component that handles this for us and not pass the onClick in the + // SpanLinkModel when link.href is defined (removing the need of having + // if (link.onClick) in here. + + // if it's an absolute URL - open it in a new window + if (!ABSOLUTE_LINK_PATTERN.test(link.href)) { + // handle relative URLs by changing current URL: + locationService.push(link.href); + } else { + window.open(link.href, '_blank', 'noopener,noreferrer'); + } } }, }, From 190f5a234fbce2f4930731f84ad9c77f76d3ca1d Mon Sep 17 00:00:00 2001 From: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Date: Thu, 10 Apr 2025 20:08:00 +0100 Subject: [PATCH 06/37] Cloud migrations: Update links to docs in cloud stack page (#103789) * modify links * new tab links * remove line --- .../migrate-to-cloud/cloud/EmptyState/MigrationStepsPane.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/migrate-to-cloud/cloud/EmptyState/MigrationStepsPane.tsx b/public/app/features/migrate-to-cloud/cloud/EmptyState/MigrationStepsPane.tsx index c04a597be2c..45be44b9555 100644 --- a/public/app/features/migrate-to-cloud/cloud/EmptyState/MigrationStepsPane.tsx +++ b/public/app/features/migrate-to-cloud/cloud/EmptyState/MigrationStepsPane.tsx @@ -59,10 +59,10 @@ export const MigrationStepsPane = () => { - + {t('migrate-to-cloud.get-started.configure-pdc-link', 'Configure PDC for this stack')} - + {t('migrate-to-cloud.migrate-to-this-stack.link-title', 'View the full migration guide')} From 02b090574dc024382c85a7c40173e3e353a93569 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 10 Apr 2025 22:29:59 +0300 Subject: [PATCH 07/37] Unified: use server access client for bulk operations (#103828) --- pkg/storage/unified/resource/bulk.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/storage/unified/resource/bulk.go b/pkg/storage/unified/resource/bulk.go index 3c580559a55..aff0d472a38 100644 --- a/pkg/storage/unified/resource/bulk.go +++ b/pkg/storage/unified/resource/bulk.go @@ -141,13 +141,10 @@ func (s *server) BulkProcess(stream BulkStore_BulkProcessServer) error { }) } - // HACK!!! always allow everything!!!!!! - access := authlib.FixedAccessClient(true) - if settings.RebuildCollection { for _, k := range settings.Collection { // Can we delete the whole collection - rsp, err := access.Check(ctx, user, authlib.CheckRequest{ + rsp, err := s.access.Check(ctx, user, authlib.CheckRequest{ Namespace: k.Namespace, Group: k.Group, Resource: k.Resource, @@ -163,7 +160,7 @@ func (s *server) BulkProcess(stream BulkStore_BulkProcessServer) error { } // This will be called for each request -- with the folder ID - runner.checker[k.NSGR()], err = access.Compile(ctx, user, authlib.ListRequest{ + runner.checker[k.NSGR()], err = s.access.Compile(ctx, user, authlib.ListRequest{ Namespace: k.Namespace, Group: k.Group, Resource: k.Resource, From 22a9e1b5ff916a812043e3f3f028263c0066e80f Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Thu, 10 Apr 2025 21:37:59 +0200 Subject: [PATCH 08/37] Scopes: Fix translation key of `scopes.dashboards.toggle.expand` (#102515) --- public/app/features/scopes/selector/ScopesSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/scopes/selector/ScopesSelector.tsx b/public/app/features/scopes/selector/ScopesSelector.tsx index 3196b611501..f0bface4c80 100644 --- a/public/app/features/scopes/selector/ScopesSelector.tsx +++ b/public/app/features/scopes/selector/ScopesSelector.tsx @@ -40,7 +40,7 @@ export const ScopesSelector = () => { ? t('scopes.dashboards.toggle.disabled', 'Suggested dashboards list is disabled due to read only mode') : drawerOpened ? t('scopes.dashboards.toggle.collapse', 'Collapse suggested dashboards list') - : t('scopes.dashboards.toggle..expand', 'Expand suggested dashboards list'); + : t('scopes.dashboards.toggle.expand', 'Expand suggested dashboards list'); return (
From fd6fd91115fa747654070a17fcfb44e4bd957cae Mon Sep 17 00:00:00 2001 From: Kevin Yu Date: Thu, 10 Apr 2025 12:49:11 -0700 Subject: [PATCH 09/37] Prometheus: Add support for cloud partners Prometheus data sources (#103482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Add prom flavor support for data source variables and export/import dashboards (#103321) * add dashboard and data source var selection * use match plugin id instead * use updated matchpluginid * formatting * cleanup * regex anchor * update error msg * Alerting: Clean up prometheus-flavored types and functions (#103703) * clean up types and utility functions for dealing with prometheus-flavored data sources * Refactor alerting datasource types to use constants as source of truth * Alerting: Clean up prometheus-flavored types and functions on the bac… (#103716) Alerting: Clean up prometheus-flavored types and functions on the backend * add matchPluginId tests * Update matchPluginId func to bidirectional (#103746) * update matchpluginid func to bidirectional * lint * formatting * use actual isSupportedExternalRulesSourceType in test * add tests in datasource_srv * betterer * remove type assertion * remove unnecessary case * use satisifies to not have to convert tuple to an array of string * add prometheus_flavor test --------- Co-authored-by: Andrew Hackmann <5140848+bossinc@users.noreply.github.com> Co-authored-by: Gilles De Mey Co-authored-by: Alexander Akhmetov --- .../src/utils/matchPluginId.test.ts | 64 ++++++++++++ .../grafana-data/src/utils/matchPluginId.ts | 12 +++ pkg/api/datasource/validation.go | 30 +++--- pkg/api/frontendsettings.go | 2 +- pkg/api/pluginproxy/ds_proxy.go | 2 +- pkg/expr/converter.go | 2 +- .../statscollector/prometheus_flavor.go | 16 ++- .../statscollector/prometheus_flavor_test.go | 34 ++++++- pkg/services/datasources/models.go | 44 +++++---- .../datasources/service/datasource.go | 2 +- .../datasources/service/datasource_test.go | 11 +++ pkg/services/ngalert/api/lotex_prom.go | 25 +++-- pkg/services/ngalert/api/lotex_prom_test.go | 98 +++++++++++++++++++ pkg/services/ngalert/api/lotex_ruler.go | 22 ++--- pkg/services/ngalert/api/lotex_ruler_test.go | 58 ++++++++--- pkg/services/ngalert/api/util.go | 27 ++++- pkg/services/ngalert/api/util_test.go | 83 ++++++++++++++++ .../alerting/unified/api/buildInfo.ts | 17 +++- .../rule-editor/AlertRuleNameInput.tsx | 5 +- .../rule-editor/ExpressionEditor.tsx | 23 +++-- .../tabs/Query/DataSourceModelPreview.tsx | 4 +- .../unified/rule-editor/formDefaults.test.ts | 5 +- .../unified/rule-editor/formProcessing.ts | 8 +- .../alerting/unified/utils/datasource.ts | 60 +++++++++--- .../features/alerting/unified/utils/query.ts | 34 ++----- .../alerting/unified/utils/rule-form.ts | 15 ++- .../dashboard/state/DashboardModel.ts | 9 +- .../features/explore/DrilldownAlertBox.tsx | 11 ++- .../features/plugins/datasource_srv.test.ts | 69 ++++++++++++- .../Integrations/dashboardIntegration.ts | 6 +- public/app/features/trails/utils.ts | 4 +- public/app/plugins/panel/alertlist/module.tsx | 7 +- 32 files changed, 654 insertions(+), 155 deletions(-) create mode 100644 packages/grafana-data/src/utils/matchPluginId.test.ts create mode 100644 pkg/services/ngalert/api/lotex_prom_test.go diff --git a/packages/grafana-data/src/utils/matchPluginId.test.ts b/packages/grafana-data/src/utils/matchPluginId.test.ts new file mode 100644 index 00000000000..124f0129b14 --- /dev/null +++ b/packages/grafana-data/src/utils/matchPluginId.test.ts @@ -0,0 +1,64 @@ +import { PluginMeta, PluginType } from '../types/plugin'; + +import { matchPluginId } from './matchPluginId'; + +const createPluginMeta = (id: string, aliasIDs?: string[]): PluginMeta => ({ + id, + name: 'Test Plugin', + type: PluginType.datasource, + module: 'test', + baseUrl: 'test', + info: { + author: { name: 'Test' }, + description: 'Test', + links: [], + logos: { small: '', large: '' }, + screenshots: [], + updated: '', + version: '', + }, + aliasIDs, +}); + +describe('matchPluginId', () => { + it('should match exact plugin ID', () => { + const pluginMeta = createPluginMeta('test-plugin'); + expect(matchPluginId('test-plugin', pluginMeta)).toBe(true); + }); + + it('should not match different plugin ID', () => { + const pluginMeta = createPluginMeta('test-plugin'); + expect(matchPluginId('different-plugin', pluginMeta)).toBe(false); + }); + + it('should match Amazon Prometheus flavor when idToMatch is prometheus', () => { + const pluginMeta = createPluginMeta('grafana-amazonprometheus-datasource'); + expect(matchPluginId('prometheus', pluginMeta)).toBe(true); + }); + + it('should match Azure Prometheus flavor when idToMatch is prometheus', () => { + const pluginMeta = createPluginMeta('grafana-azureprometheus-datasource'); + expect(matchPluginId('prometheus', pluginMeta)).toBe(true); + }); + + it('should not match non-prometheus flavor when idToMatch is prometheus', () => { + const pluginMeta = createPluginMeta('test-plugin'); + expect(matchPluginId('prometheus', pluginMeta)).toBe(false); + }); + + it('should match alias IDs', () => { + const pluginMeta = createPluginMeta('test-plugin', ['alias1', 'alias2']); + expect(matchPluginId('alias1', pluginMeta)).toBe(true); + expect(matchPluginId('alias2', pluginMeta)).toBe(true); + }); + + it('should not match non-existent alias ID', () => { + const pluginMeta = createPluginMeta('test-plugin', ['alias1', 'alias2']); + expect(matchPluginId('alias3', pluginMeta)).toBe(false); + }); + + it('should handle undefined aliasIDs', () => { + const pluginMeta = createPluginMeta('test-plugin'); + expect(matchPluginId('alias1', pluginMeta)).toBe(false); + }); +}); diff --git a/packages/grafana-data/src/utils/matchPluginId.ts b/packages/grafana-data/src/utils/matchPluginId.ts index 6804597de8f..a80189abd6c 100644 --- a/packages/grafana-data/src/utils/matchPluginId.ts +++ b/packages/grafana-data/src/utils/matchPluginId.ts @@ -5,9 +5,21 @@ export function matchPluginId(idToMatch: string, pluginMeta: PluginMeta) { return true; } + if (isPromFlavor(idToMatch)) { + return isPromFlavor(pluginMeta.id); + } + if (pluginMeta.aliasIDs) { return pluginMeta.aliasIDs.includes(idToMatch); } return false; } + +function isPromFlavor(pluginId: string): boolean { + if (pluginId === 'prometheus') { + return true; + } + const regex = new RegExp('^grafana-[0-9a-z]+prometheus-datasource$'); + return regex.test(pluginId); +} diff --git a/pkg/api/datasource/validation.go b/pkg/api/datasource/validation.go index 55628e4e930..f834cec4e1f 100644 --- a/pkg/api/datasource/validation.go +++ b/pkg/api/datasource/validation.go @@ -16,20 +16,22 @@ var logger = log.New("datasource") // requiredURL contains the set of data sources that require a URL. var requiredURL = map[string]bool{ - datasources.DS_GRAPHITE: true, - datasources.DS_INFLUXDB: true, - datasources.DS_INFLUXDB_08: true, - datasources.DS_ES: true, - datasources.DS_PROMETHEUS: true, - datasources.DS_ALERTMANAGER: true, - datasources.DS_JAEGER: true, - datasources.DS_LOKI: true, - datasources.DS_OPENTSDB: true, - datasources.DS_TEMPO: true, - datasources.DS_ZIPKIN: true, - datasources.DS_MYSQL: true, - datasources.DS_POSTGRES: true, - datasources.DS_MSSQL: true, + datasources.DS_GRAPHITE: true, + datasources.DS_INFLUXDB: true, + datasources.DS_INFLUXDB_08: true, + datasources.DS_ES: true, + datasources.DS_PROMETHEUS: true, + datasources.DS_AMAZON_PROMETHEUS: true, + datasources.DS_AZURE_PROMETHEUS: true, + datasources.DS_ALERTMANAGER: true, + datasources.DS_JAEGER: true, + datasources.DS_LOKI: true, + datasources.DS_OPENTSDB: true, + datasources.DS_TEMPO: true, + datasources.DS_ZIPKIN: true, + datasources.DS_MYSQL: true, + datasources.DS_POSTGRES: true, + datasources.DS_MSSQL: true, } // URLValidationError represents an error from validating a data source URL. diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index ab28562571f..c069cf8cf9a 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -551,7 +551,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug dsDTO.Database = ds.Database } - if ds.Type == datasources.DS_PROMETHEUS { + if ds.Type == datasources.DS_PROMETHEUS || ds.Type == datasources.DS_AMAZON_PROMETHEUS || ds.Type == datasources.DS_AZURE_PROMETHEUS { // add unproxied server URL for link to Prometheus web UI ds.JsonData.Set("directUrl", ds.URL) } diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 82b8cb4fca3..a6f75cd9ec0 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -313,7 +313,7 @@ func (proxy *DataSourceProxy) validateRequest() error { } // Trailing validation below this point for routes that were not matched - if proxy.ds.Type == datasources.DS_PROMETHEUS { + if proxy.ds.Type == datasources.DS_PROMETHEUS || proxy.ds.Type == datasources.DS_AMAZON_PROMETHEUS || proxy.ds.Type == datasources.DS_AZURE_PROMETHEUS { if proxy.ctx.Req.Method == "DELETE" { return errors.New("non allow-listed DELETEs not allowed on proxied Prometheus datasource") } diff --git a/pkg/expr/converter.go b/pkg/expr/converter.go index c804662304d..b328c5eca42 100644 --- a/pkg/expr/converter.go +++ b/pkg/expr/converter.go @@ -182,7 +182,7 @@ func getResponseFrame(logger *log.ConcreteLogger, resp *backend.QueryDataRespons } func isAllFrameVectors(datasourceType string, frames data.Frames) bool { - if datasourceType != datasources.DS_PROMETHEUS { + if datasourceType != datasources.DS_PROMETHEUS && datasourceType != datasources.DS_AMAZON_PROMETHEUS && datasourceType != datasources.DS_AZURE_PROMETHEUS { return false } allVector := false diff --git a/pkg/infra/usagestats/statscollector/prometheus_flavor.go b/pkg/infra/usagestats/statscollector/prometheus_flavor.go index 8cd2ed31318..c6a6f8d152a 100644 --- a/pkg/infra/usagestats/statscollector/prometheus_flavor.go +++ b/pkg/infra/usagestats/statscollector/prometheus_flavor.go @@ -46,12 +46,26 @@ func (s *Service) detectPrometheusVariants(ctx context.Context) (map[string]int6 s.log.Error("Failed to read all Prometheus data sources", "error", err) return nil, err } + dsAmazonProm := &datasources.GetDataSourcesByTypeQuery{Type: "grafana-amazonprometheus-datasource"} + dataSourcesAmazonProm, err := s.datasources.GetDataSourcesByType(ctx, dsAmazonProm) + if err != nil { + s.log.Error("Failed to read all Amazon Prometheus data sources", "error", err) + return nil, err + } + dsAzureProm := &datasources.GetDataSourcesByTypeQuery{Type: "grafana-azureprometheus-datasource"} + dataSourcesAzureProm, err := s.datasources.GetDataSourcesByType(ctx, dsAzureProm) + if err != nil { + s.log.Error("Failed to read all Azure Prometheus data sources", "error", err) + return nil, err + } + + allPromDataSources := append(append(dataSources, dataSourcesAmazonProm...), dataSourcesAzureProm...) g, ctx := errgroup.WithContext(ctx) g.SetLimit(10) flavors := sync.Map{} - for _, ds := range dataSources { + for _, ds := range allPromDataSources { ds := ds g.Go(func() error { variant, err := s.detectPrometheusVariant(ctx, ds) diff --git a/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go b/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go index ed64ecb7319..56dbace0553 100644 --- a/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go +++ b/pkg/infra/usagestats/statscollector/prometheus_flavor_test.go @@ -32,6 +32,18 @@ func TestDetectPrometheusVariant(t *testing.T) { })) t.Cleanup(cortex.Close) + // Amazon Prometheus is Cortex-like + amazonPrometheus := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + t.Cleanup(amazonPrometheus.Close) + + // Azure Prometheus is Cortex-like + azurePrometheus := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + t.Cleanup(azurePrometheus.Close) + sqlStore := dbtest.NewFakeDB() statsService := statstest.NewFakeService() s := createService( @@ -80,6 +92,26 @@ func TestDetectPrometheusVariant(t *testing.T) { Access: "proxy", URL: cortex.URL, }, + { + ID: 5, + UID: "amazon-prometheus", + OrgID: 1, + Version: 1, + Name: "Amazon Prometheus", + Type: "prometheus", + Access: "proxy", + URL: amazonPrometheus.URL, + }, + { + ID: 6, + UID: "azure-prometheus", + OrgID: 1, + Version: 1, + Name: "Azure Prometheus", + Type: "prometheus", + Access: "proxy", + URL: azurePrometheus.URL, + }, }}), ) @@ -88,5 +120,5 @@ func TestDetectPrometheusVariant(t *testing.T) { assert.Equal(t, int64(2), flavors["mimir"]) assert.Equal(t, int64(1), flavors["vanilla"]) - assert.Equal(t, int64(1), flavors["cortex-like"]) + assert.Equal(t, int64(3), flavors["cortex-like"]) } diff --git a/pkg/services/datasources/models.go b/pkg/services/datasources/models.go index eb4f51f389c..d81e99bdf11 100644 --- a/pkg/services/datasources/models.go +++ b/pkg/services/datasources/models.go @@ -11,27 +11,29 @@ import ( ) const ( - DS_ACCESS_DIRECT = "direct" - DS_ACCESS_PROXY = "proxy" - DS_ALERTMANAGER = "alertmanager" - DS_AZURE_MONITOR = "grafana-azure-monitor-datasource" - DS_DYNATRACE = "grafana-dynatrace-datasource" - DS_ES = "elasticsearch" - DS_ES_OPEN_DISTRO = "grafana-es-open-distro-datasource" - DS_ES_OPENSEARCH = "grafana-opensearch-datasource" - DS_GRAPHITE = "graphite" - DS_INFLUXDB = "influxdb" - DS_INFLUXDB_08 = "influxdb_08" - DS_JAEGER = "jaeger" - DS_LOKI = "loki" - DS_MSSQL = "mssql" - DS_MYSQL = "mysql" - DS_OPENTSDB = "opentsdb" - DS_POSTGRES = "grafana-postgresql-datasource" - DS_PROMETHEUS = "prometheus" - DS_TEMPO = "tempo" - DS_TESTDATA = "grafana-testdata-datasource" - DS_ZIPKIN = "zipkin" + DS_ACCESS_DIRECT = "direct" + DS_ACCESS_PROXY = "proxy" + DS_ALERTMANAGER = "alertmanager" + DS_AZURE_MONITOR = "grafana-azure-monitor-datasource" + DS_DYNATRACE = "grafana-dynatrace-datasource" + DS_ES = "elasticsearch" + DS_ES_OPEN_DISTRO = "grafana-es-open-distro-datasource" + DS_ES_OPENSEARCH = "grafana-opensearch-datasource" + DS_GRAPHITE = "graphite" + DS_INFLUXDB = "influxdb" + DS_INFLUXDB_08 = "influxdb_08" + DS_JAEGER = "jaeger" + DS_LOKI = "loki" + DS_MSSQL = "mssql" + DS_MYSQL = "mysql" + DS_OPENTSDB = "opentsdb" + DS_POSTGRES = "grafana-postgresql-datasource" + DS_PROMETHEUS = "prometheus" + DS_AMAZON_PROMETHEUS = "grafana-amazonprometheus-datasource" + DS_AZURE_PROMETHEUS = "grafana-azureprometheus-datasource" + DS_TEMPO = "tempo" + DS_TESTDATA = "grafana-testdata-datasource" + DS_ZIPKIN = "zipkin" // CustomHeaderName is the prefix that is used to store the name of a custom header. CustomHeaderName = "httpHeaderName" // CustomHeaderValue is the prefix that is used to store the value of a custom header. diff --git a/pkg/services/datasources/service/datasource.go b/pkg/services/datasources/service/datasource.go index 528651939b3..2f00a788c44 100644 --- a/pkg/services/datasources/service/datasource.go +++ b/pkg/services/datasources/service/datasource.go @@ -922,7 +922,7 @@ func awsServiceNamespace(dsType string, jsonData *simplejson.Json) string { } else { return "es" } - case datasources.DS_PROMETHEUS, datasources.DS_ALERTMANAGER: + case datasources.DS_PROMETHEUS, datasources.DS_AMAZON_PROMETHEUS, datasources.DS_ALERTMANAGER: return "aps" default: panic(fmt.Sprintf("Unsupported datasource %q", dsType)) diff --git a/pkg/services/datasources/service/datasource_test.go b/pkg/services/datasources/service/datasource_test.go index a94c54a08bd..319f039814e 100644 --- a/pkg/services/datasources/service/datasource_test.go +++ b/pkg/services/datasources/service/datasource_test.go @@ -1000,6 +1000,11 @@ func TestService_awsServiceNamespace(t *testing.T) { givenDs: datasources.DS_PROMETHEUS, givenJson: `{ "sigV4Auth": true, "serverless": true }`, want: "aps", + }, { + desc: "amazon prometheus", + givenDs: datasources.DS_AMAZON_PROMETHEUS, + givenJson: `{ "sigV4Auth": true }`, + want: "aps", }, { desc: "alertmanager", givenDs: datasources.DS_ALERTMANAGER, @@ -1011,6 +1016,12 @@ func TestService_awsServiceNamespace(t *testing.T) { givenJson: `{ "sigV4Auth": true, "serverless": true }`, want: "aps", panic: true, + }, { + desc: "azure prometheus", + givenDs: datasources.DS_AZURE_PROMETHEUS, + givenJson: `{ "sigV4Auth": true }`, + want: "aps", + panic: true, }, } for _, tc := range testCases { diff --git a/pkg/services/ngalert/api/lotex_prom.go b/pkg/services/ngalert/api/lotex_prom.go index 761fe90a4af..b50bea6e6b1 100644 --- a/pkg/services/ngalert/api/lotex_prom.go +++ b/pkg/services/ngalert/api/lotex_prom.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/web" ) @@ -15,16 +16,16 @@ type promEndpoints struct { rules, alerts string } -var dsTypeToLotexRoutes = map[string]promEndpoints{ - "prometheus": { +var ( + prometheusEndpoints = promEndpoints{ rules: "/api/v1/rules", alerts: "/api/v1/alerts", - }, - "loki": { + } + lokiEndpoints = promEndpoints{ rules: "/prometheus/api/v1/rules", alerts: "/prometheus/api/v1/alerts", - }, -} + } +) type LotexProm struct { log log.Logger @@ -91,9 +92,15 @@ func (p *LotexProm) getEndpoints(ctx *contextmodel.ReqContext) (*promEndpoints, return nil, fmt.Errorf("URL for this data source is empty") } - routes, ok := dsTypeToLotexRoutes[ds.Type] - if !ok { - return nil, fmt.Errorf("unexpected datasource type. expecting loki or prometheus") + var routes promEndpoints + switch { + case isPrometheusCompatible(ds.Type): + routes = prometheusEndpoints + case ds.Type == datasources.DS_LOKI: + routes = lokiEndpoints + default: + return nil, unexpectedDatasourceTypeError(ds.Type, "loki, prometheus, amazon prometheus, azure prometheus") } + return &routes, nil } diff --git a/pkg/services/ngalert/api/lotex_prom_test.go b/pkg/services/ngalert/api/lotex_prom_test.go new file mode 100644 index 00000000000..7fd704ff422 --- /dev/null +++ b/pkg/services/ngalert/api/lotex_prom_test.go @@ -0,0 +1,98 @@ +package api + +import ( + "errors" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/log" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/datasourceproxy" + "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/web" +) + +func TestLotexProm_GetEndpoints(t *testing.T) { + tc := []struct { + name string + namedParams map[string]string + datasourceCache datasources.CacheService + expectedRoutes *promEndpoints + err error + }{ + { + name: "with an empty datasource UID", + namedParams: map[string]string{":DatasourceUID": ""}, + err: errors.New("datasource UID is invalid"), + }, + { + name: "with an error while trying to fetch the datasource", + namedParams: map[string]string{":DatasourceUID": "d164"}, + datasourceCache: fakeCacheService{err: datasources.ErrDataSourceNotFound}, + err: errors.New("data source not found"), + }, + { + name: "with an empty datasource URL", + namedParams: map[string]string{":DatasourceUID": "d164"}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{}}, + err: errors.New("URL for this data source is empty"), + }, + { + name: "with an unsupported datasource type", + namedParams: map[string]string{":DatasourceUID": "d164"}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: "unsupported-type"}}, + err: errors.New("unexpected datasource type 'unsupported-type', expected loki, prometheus, amazon prometheus, azure prometheus"), + }, + { + name: "with a Loki datasource", + namedParams: map[string]string{":DatasourceUID": "d164"}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: datasources.DS_LOKI}}, + expectedRoutes: &lokiEndpoints, + err: nil, + }, + { + name: "with a Prometheus datasource", + namedParams: map[string]string{":DatasourceUID": "d164"}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://prom.com", Type: datasources.DS_PROMETHEUS}}, + expectedRoutes: &prometheusEndpoints, + err: nil, + }, + { + name: "with an Amazon Prometheus datasource", + namedParams: map[string]string{":DatasourceUID": "d164"}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://amp.com", Type: datasources.DS_AMAZON_PROMETHEUS}}, + expectedRoutes: &prometheusEndpoints, + err: nil, + }, + { + name: "with an Azure Prometheus datasource", + namedParams: map[string]string{":DatasourceUID": "d164"}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://azp.com", Type: datasources.DS_AZURE_PROMETHEUS}}, + expectedRoutes: &prometheusEndpoints, + err: nil, + }, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + proxy := &AlertingProxy{DataProxy: &datasourceproxy.DataSourceProxyService{DataSourceCache: tt.datasourceCache}} + prom := &LotexProm{AlertingProxy: proxy, log: log.NewNopLogger()} + + // Setup request context. + httpReq, err := http.NewRequest(http.MethodGet, "http://grafanacloud.com", nil) + require.NoError(t, err) + ctx := &contextmodel.ReqContext{Context: &web.Context{Req: web.SetURLParams(httpReq, tt.namedParams)}} + + endpoints, err := prom.getEndpoints(ctx) + + if tt.err != nil { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.expectedRoutes, endpoints) + } + }) + } +} diff --git a/pkg/services/ngalert/api/lotex_ruler.go b/pkg/services/ngalert/api/lotex_ruler.go index 7e920dbc136..d3dedfe2866 100644 --- a/pkg/services/ngalert/api/lotex_ruler.go +++ b/pkg/services/ngalert/api/lotex_ruler.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/web" ) @@ -23,9 +24,6 @@ const ( ) const ( - PrometheusDatasourceType = "prometheus" - LokiDatasourceType = "loki" - mimirPrefix = "/config/v1/rules" prometheusPrefix = "/rules" lokiPrefix = "/api/prom/rules" @@ -33,11 +31,6 @@ const ( subtypeQuery = "subtype" ) -var dsTypeToRulerPrefix = map[string]string{ - PrometheusDatasourceType: prometheusPrefix, - LokiDatasourceType: lokiPrefix, -} - var subtypeToPrefix = map[string]string{ Prometheus: prometheusPrefix, Cortex: prometheusPrefix, @@ -237,13 +230,18 @@ func (r *LotexRuler) validateAndGetPrefix(ctx *contextmodel.ReqContext) (string, return "", fmt.Errorf("URL for this data source is empty") } - prefix, ok := dsTypeToRulerPrefix[ds.Type] - if !ok { - return "", fmt.Errorf("unexpected datasource type. expecting loki or prometheus") + var prefix string + switch { + case isPrometheusCompatible(ds.Type): + prefix = prometheusPrefix + case ds.Type == datasources.DS_LOKI: + prefix = lokiPrefix + default: + return "", unexpectedDatasourceTypeError(ds.Type, "loki, prometheus, amazon prometheus, azure prometheus") } // If the datasource is Loki, there's nothing else for us to do - it doesn't have subtypes. - if ds.Type == LokiDatasourceType { + if ds.Type == datasources.DS_LOKI { return prefix, nil } diff --git a/pkg/services/ngalert/api/lotex_ruler_test.go b/pkg/services/ngalert/api/lotex_ruler_test.go index bfbc8dd7fe4..484a92541ed 100644 --- a/pkg/services/ngalert/api/lotex_ruler_test.go +++ b/pkg/services/ngalert/api/lotex_ruler_test.go @@ -49,48 +49,74 @@ func TestLotexRuler_ValidateAndGetPrefix(t *testing.T) { { name: "with an unsupported datasource type", namedParams: map[string]string{":DatasourceUID": "d164"}, - datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com"}}, - err: errors.New("unexpected datasource type. expecting loki or prometheus"), + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: "unsupported-type"}}, + err: errors.New("unexpected datasource type 'unsupported-type', expected loki, prometheus, amazon prometheus, azure prometheus"), }, { name: "with a Loki datasource", namedParams: map[string]string{":DatasourceUID": "d164"}, - datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: LokiDatasourceType}}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: datasources.DS_LOKI}}, expected: "/api/prom/rules", }, { name: "with a Prometheus datasource", namedParams: map[string]string{":DatasourceUID": "d164"}, - datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: PrometheusDatasourceType}}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: datasources.DS_PROMETHEUS}}, + expected: "/rules", + }, + { + name: "with an Amazon Prometheus datasource", + namedParams: map[string]string{":DatasourceUID": "d164"}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://amp.com", Type: datasources.DS_AMAZON_PROMETHEUS}}, + expected: "/rules", + }, + { + name: "with an Azure Prometheus datasource", + namedParams: map[string]string{":DatasourceUID": "d164"}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://azp.com", Type: datasources.DS_AZURE_PROMETHEUS}}, expected: "/rules", }, { name: "with a Prometheus datasource and subtype of Cortex", namedParams: map[string]string{":DatasourceUID": "d164"}, urlParams: "?subtype=cortex", - datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: PrometheusDatasourceType}}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: datasources.DS_PROMETHEUS}}, expected: "/rules", }, { name: "with a Prometheus datasource and subtype of Mimir", namedParams: map[string]string{":DatasourceUID": "d164"}, urlParams: "?subtype=mimir", - datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: PrometheusDatasourceType}}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: datasources.DS_PROMETHEUS}}, expected: "/config/v1/rules", }, { name: "with a Prometheus datasource and subtype of Prometheus", namedParams: map[string]string{":DatasourceUID": "d164"}, urlParams: "?subtype=prometheus", - datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: PrometheusDatasourceType}}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: datasources.DS_PROMETHEUS}}, expected: "/rules", }, { name: "with a Prometheus datasource and no subtype", namedParams: map[string]string{":DatasourceUID": "d164"}, - datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: PrometheusDatasourceType}}, + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://loki.com", Type: datasources.DS_PROMETHEUS}}, expected: "/rules", }, + { + name: "with an Amazon Prometheus datasource and subtype of Mimir", + namedParams: map[string]string{":DatasourceUID": "d164"}, + urlParams: "?subtype=mimir", + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://amp.com", Type: datasources.DS_AMAZON_PROMETHEUS}}, + expected: "/config/v1/rules", + }, + { + name: "with an Azure Prometheus datasource and subtype of Mimir", + namedParams: map[string]string{":DatasourceUID": "d164"}, + urlParams: "?subtype=mimir", + datasourceCache: fakeCacheService{datasource: &datasources.DataSource{URL: "http://azp.com", Type: datasources.DS_AZURE_PROMETHEUS}}, + expected: "/config/v1/rules", + }, } for _, tt := range tc { @@ -149,7 +175,7 @@ func TestLotexRuler_RouteDeleteNamespaceRulesConfig(t *testing.T) { expected: "http://mimir.com/config/v1/rules/namespace%2Fwith%2Fslashes?subtype=mimir", urlParams: "?subtype=mimir", namedParams: map[string]string{":DatasourceUID": "d164"}, - datasource: &datasources.DataSource{URL: "http://mimir.com", Type: PrometheusDatasourceType}, + datasource: &datasources.DataSource{URL: "http://mimir.com", Type: datasources.DS_PROMETHEUS}, }, { name: "with a namespace that does not need to be escaped", @@ -157,7 +183,7 @@ func TestLotexRuler_RouteDeleteNamespaceRulesConfig(t *testing.T) { expected: "http://mimir.com/config/v1/rules/namespace_without_slashes?subtype=mimir", urlParams: "?subtype=mimir", namedParams: map[string]string{":DatasourceUID": "d164"}, - datasource: &datasources.DataSource{URL: "http://mimir.com", Type: PrometheusDatasourceType}, + datasource: &datasources.DataSource{URL: "http://mimir.com", Type: datasources.DS_PROMETHEUS}, }, } @@ -210,7 +236,7 @@ func TestLotexRuler_RouteDeleteRuleGroupConfig(t *testing.T) { expected: "http://mimir.com/config/v1/rules/namespace%2Fwith%2Fslashes/group%2Fwith%2Fslashes?subtype=mimir", urlParams: "?subtype=mimir", namedParams: map[string]string{":DatasourceUID": "d164"}, - datasource: &datasources.DataSource{URL: "http://mimir.com", Type: PrometheusDatasourceType}, + datasource: &datasources.DataSource{URL: "http://mimir.com", Type: datasources.DS_PROMETHEUS}, }, { name: "with a namespace that does not need to be escaped", @@ -219,7 +245,7 @@ func TestLotexRuler_RouteDeleteRuleGroupConfig(t *testing.T) { expected: "http://mimir.com/config/v1/rules/namespace_without_slashes/group_without_slashes?subtype=mimir", urlParams: "?subtype=mimir", namedParams: map[string]string{":DatasourceUID": "d164"}, - datasource: &datasources.DataSource{URL: "http://mimir.com", Type: PrometheusDatasourceType}, + datasource: &datasources.DataSource{URL: "http://mimir.com", Type: datasources.DS_PROMETHEUS}, }, } @@ -271,7 +297,7 @@ func TestLotexRuler_RouteGetNamespaceRulesConfig(t *testing.T) { expected: "http://mimir.com/config/v1/rules/namespace%2Fwith%2Fslashes?subtype=mimir", urlParams: "?subtype=mimir", namedParams: map[string]string{":DatasourceUID": "d164"}, - datasource: &datasources.DataSource{URL: "http://mimir.com", Type: PrometheusDatasourceType}, + datasource: &datasources.DataSource{URL: "http://mimir.com", Type: datasources.DS_PROMETHEUS}, }, { name: "with a namespace that does not need to be escaped", @@ -279,7 +305,7 @@ func TestLotexRuler_RouteGetNamespaceRulesConfig(t *testing.T) { expected: "http://mimir.com/config/v1/rules/namespace_without_slashes?subtype=mimir", urlParams: "?subtype=mimir", namedParams: map[string]string{":DatasourceUID": "d164"}, - datasource: &datasources.DataSource{URL: "http://mimir.com", Type: PrometheusDatasourceType}, + datasource: &datasources.DataSource{URL: "http://mimir.com", Type: datasources.DS_PROMETHEUS}, }, } @@ -332,7 +358,7 @@ func TestLotexRuler_RouteGetRulegGroupConfig(t *testing.T) { expected: "http://mimir.com/config/v1/rules/namespace%2Fwith%2Fslashes/group%2Fwith%2Fslashes?subtype=mimir", urlParams: "?subtype=mimir", namedParams: map[string]string{":DatasourceUID": "d164"}, - datasource: &datasources.DataSource{URL: "http://mimir.com", Type: PrometheusDatasourceType}, + datasource: &datasources.DataSource{URL: "http://mimir.com", Type: datasources.DS_PROMETHEUS}, }, { name: "with a namespace that does not need to be escaped", @@ -341,7 +367,7 @@ func TestLotexRuler_RouteGetRulegGroupConfig(t *testing.T) { expected: "http://mimir.com/config/v1/rules/namespace_without_slashes/group_without_slashes?subtype=mimir", urlParams: "?subtype=mimir", namedParams: map[string]string{":DatasourceUID": "d164"}, - datasource: &datasources.DataSource{URL: "http://mimir.com", Type: PrometheusDatasourceType}, + datasource: &datasources.DataSource{URL: "http://mimir.com", Type: datasources.DS_PROMETHEUS}, }, } diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index 67c2e5a1363..e5840c992a7 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -31,7 +31,28 @@ const ( groupQueryTag = "QUERY_GROUP" ) -var searchRegex = regexp.MustCompile(`\{(\w+)\}`) +var ( + searchRegex = regexp.MustCompile(`\{(\w+)\}`) + + prometheusCompatibleDsTypes = []string{ + datasources.DS_PROMETHEUS, + datasources.DS_AMAZON_PROMETHEUS, + datasources.DS_AZURE_PROMETHEUS, + } +) + +func isPrometheusCompatible(dsType string) bool { + for _, t := range prometheusCompatibleDsTypes { + if dsType == t { + return true + } + } + return false +} + +func isLotexRulerCompatible(dsType string) bool { + return dsType == datasources.DS_LOKI || isPrometheusCompatible(dsType) +} func toMacaronPath(path string) string { return string(searchRegex.ReplaceAllFunc([]byte(path), func(s []byte) []byte { @@ -52,8 +73,8 @@ func getDatasourceByUID(ctx *contextmodel.ReqContext, cache datasources.CacheSer return nil, unexpectedDatasourceTypeError(ds.Type, "alertmanager") } case apimodels.LoTexRulerBackend: - if ds.Type != "loki" && ds.Type != "prometheus" { - return nil, unexpectedDatasourceTypeError(ds.Type, "loki, prometheus") + if !isLotexRulerCompatible(ds.Type) { + return nil, unexpectedDatasourceTypeError(ds.Type, "loki, prometheus, amazon prometheus, azure prometheus") } default: return nil, unexpectedDatasourceTypeError(ds.Type, expectedType.String()) diff --git a/pkg/services/ngalert/api/util_test.go b/pkg/services/ngalert/api/util_test.go index 4e657cfc250..ba3a1dfab47 100644 --- a/pkg/services/ngalert/api/util_test.go +++ b/pkg/services/ngalert/api/util_test.go @@ -13,6 +13,7 @@ import ( accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/auth" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/ngalert/eval" models2 "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/org" @@ -176,3 +177,85 @@ func (r *recordingConditionValidator) Validate(_ eval.EvaluationContext, conditi } var _ ConditionValidator = &recordingConditionValidator{} + +func TestIsPrometheusCompatible(t *testing.T) { + testCases := []struct { + name string + dsType string + expected bool + }{ + { + name: "prometheus datasource should be compatible", + dsType: datasources.DS_PROMETHEUS, + expected: true, + }, + { + name: "amazon prometheus datasource should be compatible", + dsType: datasources.DS_AMAZON_PROMETHEUS, + expected: true, + }, + { + name: "azure prometheus datasource should be compatible", + dsType: datasources.DS_AZURE_PROMETHEUS, + expected: true, + }, + { + name: "loki datasource should not be prometheus compatible", + dsType: datasources.DS_LOKI, + expected: false, + }, + { + name: "other datasource types should not be compatible", + dsType: "some-other-datasource", + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := isPrometheusCompatible(tc.dsType) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestIsLotexRulerCompatible(t *testing.T) { + testCases := []struct { + name string + dsType string + expected bool + }{ + { + name: "prometheus datasource should be compatible", + dsType: datasources.DS_PROMETHEUS, + expected: true, + }, + { + name: "amazon prometheus datasource should be compatible", + dsType: datasources.DS_AMAZON_PROMETHEUS, + expected: true, + }, + { + name: "azure prometheus datasource should be compatible", + dsType: datasources.DS_AZURE_PROMETHEUS, + expected: true, + }, + { + name: "loki datasource should be compatible", + dsType: datasources.DS_LOKI, + expected: true, + }, + { + name: "other datasource types should not be compatible", + dsType: "some-other-datasource", + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := isLotexRulerCompatible(tc.dsType) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/public/app/features/alerting/unified/api/buildInfo.ts b/public/app/features/alerting/unified/api/buildInfo.ts index 2aa4a1e363b..371dfa45eee 100644 --- a/public/app/features/alerting/unified/api/buildInfo.ts +++ b/public/app/features/alerting/unified/api/buildInfo.ts @@ -9,7 +9,14 @@ import { } from 'app/types/unified-alerting-dto'; import { RULER_NOT_SUPPORTED_MSG } from '../utils/constants'; -import { GRAFANA_RULES_SOURCE_NAME, getDataSourceByName, getRulesDataSourceByUID } from '../utils/datasource'; +import { + GRAFANA_RULES_SOURCE_NAME, + SUPPORTED_EXTERNAL_RULE_SOURCE_TYPES, + SupportedExternalRulesSourceType, + getDataSourceByName, + getRulesDataSourceByUID, + isSupportedExternalRulesSourceType, +} from '../utils/datasource'; import { fetchRules } from './prometheus'; import { fetchTestRulerRulesGroup } from './ruler'; @@ -34,8 +41,10 @@ export async function discoverFeaturesByUid(dataSourceUid: string): Promise { const { url, name, type } = dsSettings; diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx index 8639aa2a3c5..77337cde435 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx @@ -8,6 +8,7 @@ import { t } from 'app/core/internationalization'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { RuleFormType, RuleFormValues } from '../../types/rule-form'; +import { isSupportedExternalPrometheusFlavoredRulesSourceType } from '../../utils/datasource'; import { isCloudRecordingRuleByType, isGrafanaRecordingRuleByType, isRecordingRuleByType } from '../../utils/rules'; import { RuleEditorSection } from './RuleEditorSection'; @@ -109,7 +110,9 @@ export const AlertRuleNameAndMetric = () => { current={field.value} noDefault // Filter with `filter` prop instead of `type` prop to avoid showing the `-- Grafana --` data source - filter={(ds: DataSourceInstanceSettings) => ds.type === 'prometheus'} + filter={(ds: DataSourceInstanceSettings) => + isSupportedExternalPrometheusFlavoredRulesSourceType(ds.type) + } onChange={(ds: DataSourceInstanceSettings) => { setValue('targetDatasourceUid', ds.uid); }} diff --git a/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx b/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx index fc7b4e557d4..6ef2d04ffba 100644 --- a/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/ExpressionEditor.tsx @@ -11,6 +11,8 @@ import { Alert, Button, useStyles2 } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; import { LokiQuery } from 'app/plugins/datasource/loki/types'; +import { isSupportedExternalRulesSourceType } from '../../utils/datasource'; + import { CloudAlertPreview } from './CloudAlertPreview'; import { usePreview } from './PreviewRule'; @@ -130,16 +132,17 @@ type QueryMappers = { export function useQueryMappers(dataSourceName: string): QueryMappers { return useMemo(() => { const settings = getDataSourceSrv().getInstanceSettings(dataSourceName); - - switch (settings?.type) { - case 'loki': - case 'prometheus': - return { - mapToValue: (query: DataQuery) => (query as PromQuery | LokiQuery).expr, - mapToQuery: (existing: DataQuery, value: string | undefined) => ({ ...existing, expr: value }), - }; - default: - throw new Error(`${dataSourceName} is not supported as an expression editor`); + if (!settings) { + throw new Error(`Datasource ${dataSourceName} not found`); } + + if (!isSupportedExternalRulesSourceType(settings.type)) { + throw new Error(`${settings.type} is not supported as an expression editor`); + } + + return { + mapToValue: (query: DataQuery) => (query as PromQuery | LokiQuery).expr, + mapToQuery: (existing: DataQuery, value: string | undefined) => ({ ...existing, expr: value }), + }; }, [dataSourceName]); } diff --git a/public/app/features/alerting/unified/components/rule-viewer/tabs/Query/DataSourceModelPreview.tsx b/public/app/features/alerting/unified/components/rule-viewer/tabs/Query/DataSourceModelPreview.tsx index 6798611f9f8..aff14fa50e2 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/tabs/Query/DataSourceModelPreview.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/tabs/Query/DataSourceModelPreview.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { DataSourceInstanceSettings } from '@grafana/data'; import { AlertDataQuery } from 'app/types/unified-alerting-dto'; -import { DataSourceType } from '../../../../utils/datasource'; +import { DataSourceType, isSupportedExternalPrometheusFlavoredRulesSourceType } from '../../../../utils/datasource'; import { isPromOrLokiQuery } from '../../../../utils/rule-form'; import { SQLQueryPreview, isSQLLikeQuery } from './SQLQueryPreview'; @@ -18,7 +18,7 @@ interface DatasourceModelPreviewProps { } function DatasourceModelPreview({ model, dataSource: datasource }: DatasourceModelPreviewProps): React.ReactNode { - if (datasource.type === DataSourceType.Prometheus && isPromOrLokiQuery(model)) { + if (isSupportedExternalPrometheusFlavoredRulesSourceType(datasource.type) && isPromOrLokiQuery(model)) { return ; } diff --git a/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts b/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts index 9dd60a76b8d..4cb203e15a2 100644 --- a/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts +++ b/public/app/features/alerting/unified/rule-editor/formDefaults.test.ts @@ -10,7 +10,10 @@ import { MANUAL_ROUTING_KEY, getDefaultQueries } from '../utils/rule-form'; import { formValuesFromQueryParams, getDefaultFormValues, getDefautManualRouting } from './formDefaults'; import { isAlertQueryOfAlertData } from './formProcessing'; -jest.mock('../utils/datasource'); +jest.mock('../utils/datasource', () => ({ + ...jest.requireActual('../utils/datasource'), + getDefaultOrFirstCompatibleDataSource: jest.fn(), +})); const mocks = { getDefaultOrFirstCompatibleDataSource: jest.mocked(getDefaultOrFirstCompatibleDataSource), diff --git a/public/app/features/alerting/unified/rule-editor/formProcessing.ts b/public/app/features/alerting/unified/rule-editor/formProcessing.ts index d9f79892ce5..e986604422d 100644 --- a/public/app/features/alerting/unified/rule-editor/formProcessing.ts +++ b/public/app/features/alerting/unified/rule-editor/formProcessing.ts @@ -8,7 +8,7 @@ import { AlertDataQuery, AlertQuery } from 'app/types/unified-alerting-dto'; import { KVObject, RuleFormValues } from '../types/rule-form'; import { defaultAnnotations } from '../utils/constants'; -import { DataSourceType } from '../utils/datasource'; +import { isSupportedExternalRulesSourceType } from '../utils/datasource'; import { getInstantFromDataQuery } from '../utils/rule-form'; export function setQueryEditorSettings(values: RuleFormValues): RuleFormValues { @@ -48,9 +48,9 @@ export function setInstantOrRange(values: RuleFormValues): RuleFormValues { return query; } // data query - const defaultToInstant = - query.model.datasource?.type === DataSourceType.Loki || - query.model.datasource?.type === DataSourceType.Prometheus; + const defaultToInstant = query.model.datasource?.type + ? isSupportedExternalRulesSourceType(query.model.datasource.type) + : false; const isInstant = 'instant' in query.model && query.model.instant !== undefined ? query.model.instant : defaultToInstant; return { diff --git a/public/app/features/alerting/unified/utils/datasource.ts b/public/app/features/alerting/unified/utils/datasource.ts index c81dfd4c2a8..b39a2d66252 100644 --- a/public/app/features/alerting/unified/utils/datasource.ts +++ b/public/app/features/alerting/unified/utils/datasource.ts @@ -38,10 +38,15 @@ export const GrafanaRulesSource: GrafanaRulesSourceIdentifier = { ruleSourceType: 'grafana', }; +/** + * @deprecated use "SupportedRulesSourceType" and related types instead + */ export enum DataSourceType { Alertmanager = 'alertmanager', Loki = 'loki', Prometheus = 'prometheus', + AmazonPrometheus = 'grafana-amazonprometheus-datasource', + AzurePrometheus = 'grafana-azureprometheus-datasource', } export interface AlertManagerDataSource { @@ -52,8 +57,6 @@ export interface AlertManagerDataSource { handleGrafanaManagedAlerts?: boolean; } -export const RulesDataSourceTypes: string[] = [DataSourceType.Loki, DataSourceType.Prometheus]; - export function getRulesDataSources() { const hasReadPermission = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead); const hasWritePermission = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalWrite); @@ -62,7 +65,7 @@ export function getRulesDataSources() { } return getAllDataSources() - .filter((ds) => RulesDataSourceTypes.includes(ds.type)) + .filter((ds) => isSupportedExternalRulesSourceType(ds.type)) .filter((ds) => isDataSourceManagingAlerts(ds)) .sort((a, b) => a.name.localeCompare(b.name)); } @@ -203,17 +206,6 @@ export function getAlertManagerDataSourcesByPermission(permission: 'instance' | return { availableInternalDataSources, availableExternalDataSources }; } -export function getLotexDataSourceByName(dataSourceName: string): DataSourceInstanceSettings { - const dataSource = getDataSourceByName(dataSourceName); - if (!dataSource) { - throw new Error(`Data source ${dataSourceName} not found`); - } - if (dataSource.type !== DataSourceType.Loki && dataSource.type !== DataSourceType.Prometheus) { - throw new Error(`Unexpected data source type ${dataSource.type}`); - } - return dataSource; -} - export function getAllRulesSourceNames(): string[] { const availableRulesSources: string[] = getRulesDataSources().map((r) => r.name); @@ -356,3 +348,43 @@ export function ruleIdentifierToRuleSourceIdentifier(ruleIdentifier: RuleIdentif ruleSourceType: 'datasource', }; } + +/** + * Check if the given type is a supported external Prometheus flavored rules source type. + */ +export function isSupportedExternalPrometheusFlavoredRulesSourceType( + type: string +): type is SupportedExternalPrometheusFlavoredRulesSourceType { + return SUPPORTED_EXTERNAL_PROMETHEUS_FLAVORED_RULE_SOURCE_TYPES.find((t) => t === type) !== undefined; +} +export const SUPPORTED_EXTERNAL_PROMETHEUS_FLAVORED_RULE_SOURCE_TYPES = [ + 'prometheus', + 'grafana-amazonprometheus-datasource', + 'grafana-azureprometheus-datasource', +] as const; +export type SupportedExternalPrometheusFlavoredRulesSourceType = + (typeof SUPPORTED_EXTERNAL_PROMETHEUS_FLAVORED_RULE_SOURCE_TYPES)[number]; // infer the type from the tuple above so we can maintain a single source of truth + +/** + * Check if the given type is a supported external rules source type. Includes Loki and Prometheus flavored types. + */ +export function isSupportedExternalRulesSourceType(type: string): type is SupportedExternalRulesSourceType { + return SUPPORTED_EXTERNAL_RULE_SOURCE_TYPES.find((t) => t === type) !== undefined; +} +export type SupportedExternalRulesSourceType = 'loki' | SupportedExternalPrometheusFlavoredRulesSourceType; +export const SUPPORTED_EXTERNAL_RULE_SOURCE_TYPES = [ + 'loki', + ...SUPPORTED_EXTERNAL_PROMETHEUS_FLAVORED_RULE_SOURCE_TYPES, +] as const; + +/** + * Check if the given type is a supported rules source type. Includes "grafana" for Grafana Managed Rules. + */ +export function isSupportedRulesSourceType(type: string): type is SupportedRulesSourceType { + return type === GRAFANA_RULES_SOURCE_NAME || isSupportedExternalRulesSourceType(type); +} +export type SupportedRulesSourceType = 'grafana' | SupportedExternalRulesSourceType; +export const SUPPORTED_RULE_SOURCE_TYPES = [ + GRAFANA_RULES_SOURCE_NAME, + ...SUPPORTED_EXTERNAL_RULE_SOURCE_TYPES, +] as const satisfies string[]; diff --git a/public/app/features/alerting/unified/utils/query.ts b/public/app/features/alerting/unified/utils/query.ts index dc40ca3534e..daa30207e94 100644 --- a/public/app/features/alerting/unified/utils/query.ts +++ b/public/app/features/alerting/unified/utils/query.ts @@ -7,7 +7,7 @@ import { LokiQuery } from 'app/plugins/datasource/loki/types'; import { CombinedRule } from 'app/types/unified-alerting'; import { AlertQuery } from 'app/types/unified-alerting-dto'; -import { isCloudRulesSource } from './datasource'; +import { isCloudRulesSource, isSupportedExternalRulesSourceType } from './datasource'; import { rulerRuleType } from './rules'; import { safeParsePrometheusDuration } from './time'; @@ -77,28 +77,14 @@ export function dataQueryToAlertQuery(dataQuery: DataQuery, dataSourceUid: strin } function cloudAlertRuleToModel(dsSettings: DataSourceInstanceSettings, rule: CombinedRule): DataQuery { - const refId = 'A'; - - switch (dsSettings.type) { - case 'prometheus': { - const query: PromQuery = { - refId, - expr: rule.query, - }; - - return query; - } - - case 'loki': { - const query: LokiQuery = { - refId, - expr: rule.query, - }; - - return query; - } - - default: - throw new Error(`Query for datasource type ${dsSettings.type} is currently not supported by cloud alert rules.`); + if (!isSupportedExternalRulesSourceType(dsSettings.type)) { + throw new Error(`Query for datasource type ${dsSettings.type} is currently not supported by cloud alert rules.`); } + + const query: LokiQuery | PromQuery = { + refId: 'A', + expr: rule.query, + }; + + return query; } diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index ad0536595af..2be48613f0a 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -51,10 +51,11 @@ import { import { Annotation } from './constants'; import { - DataSourceType, GRAFANA_RULES_SOURCE_NAME, getDefaultOrFirstCompatibleDataSource, isGrafanaRulesSource, + isSupportedExternalPrometheusFlavoredRulesSourceType, + isSupportedExternalRulesSourceType, } from './datasource'; import { arrayToRecord, recordToArray } from './misc'; import { isGrafanaAlertingRuleByType, isGrafanaRecordingRuleByType, rulerRuleType } from './rules'; @@ -482,7 +483,7 @@ export const getDefaultQueries = (isRecordingRule = false): AlertQuery[] => { const relativeTimeRange = getDefaultRelativeTimeRange(); const expressions = isRecordingRule ? getDefaultExpressionsForRecording('B') : getDefaultExpressions('B', 'C'); - const isLokiOrPrometheus = dataSource?.type === DataSourceType.Prometheus || dataSource?.type === DataSourceType.Loki; + const isLokiOrPrometheus = dataSource ? isSupportedExternalRulesSourceType(dataSource.type) : false; return [ { refId: 'A', @@ -876,9 +877,12 @@ export function getInstantFromDataQuery(query: AlertQuery): bool // find the datasource type from the UID const type = getDataSourceSrv().getInstanceSettings(dataSourceUID)?.type; + if (!type) { + return undefined; + } - // if the datasource is not prometheus or loki, return "undefined" - if (type !== DataSourceType.Prometheus && type !== DataSourceType.Loki) { + // if the datasource is not a supported prometheus flavor or loki, return "undefined" + if (!isSupportedExternalRulesSourceType(type)) { return undefined; } @@ -888,6 +892,7 @@ export function getInstantFromDataQuery(query: AlertQuery): bool const isInstantForPrometheus = 'instant' in model && model.instant !== undefined ? model.instant : true; const isInstantForLoki = 'queryType' in model && model.queryType !== undefined ? model.queryType === 'instant' : true; - const isInstant = type === DataSourceType.Prometheus ? isInstantForPrometheus : isInstantForLoki; + const isPrometheusFlavoredDataSourceType = isSupportedExternalPrometheusFlavoredRulesSourceType(type); + const isInstant = isPrometheusFlavoredDataSourceType ? isInstantForPrometheus : isInstantForLoki; return isInstant; } diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index f57845ceee6..eacc6be7378 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -1188,7 +1188,14 @@ export class DashboardModel implements TimeModel { toggleExemplarsForAll() { for (const panel of this.panels) { for (const target of panel.targets) { - if (!(target.datasource && target.datasource.type === 'prometheus')) { + if ( + !( + target.datasource && + (target.datasource.type === 'prometheus' || + target.datasource.type === 'grafana-amazonprometheus-datasource' || + target.datasource.type === 'grafana-azureprometheus-datasource') + ) + ) { continue; } diff --git a/public/app/features/explore/DrilldownAlertBox.tsx b/public/app/features/explore/DrilldownAlertBox.tsx index b6618f3f479..8896f1523ad 100644 --- a/public/app/features/explore/DrilldownAlertBox.tsx +++ b/public/app/features/explore/DrilldownAlertBox.tsx @@ -9,9 +9,14 @@ type Props = { }; export function DrilldownAlertBox(props: Props) { - const isDsCompatibleWithDrilldown = ['prometheus', 'loki', 'tempo', 'grafana-pyroscope-datasource'].includes( - props.datasourceType - ); + const isDsCompatibleWithDrilldown = [ + 'prometheus', + 'grafana-amazonprometheus-datasource', + 'grafana-azureprometheus-datasource', + 'loki', + 'tempo', + 'grafana-pyroscope-datasource', + ].includes(props.datasourceType); const [dismissed, setDismissed] = useLocalStorage('grafana.explore.drilldownsBoxDismissed', false); diff --git a/public/app/features/plugins/datasource_srv.test.ts b/public/app/features/plugins/datasource_srv.test.ts index 4d2592ea4b6..94e714ce35c 100644 --- a/public/app/features/plugins/datasource_srv.test.ts +++ b/public/app/features/plugins/datasource_srv.test.ts @@ -147,6 +147,24 @@ describe('datasource_srv', () => { uid: 'testdata', meta: { metrics: true, id: 'grafana-testdata-datasource', aliasIDs: ['testdata'] }, }, + Prometheus: { + type: 'prometheus', + name: 'Prometheus', + uid: 'uid-code-prometheus', + meta: { metrics: true, id: 'prometheus' }, + }, + AmazonPrometheus: { + type: 'grafana-amazonprometheus-datasource', + name: 'Amazon Prometheus', + uid: 'uid-code-amp', + meta: { metrics: true, id: 'grafana-amazonprometheus-datasource' }, + }, + AzurePrometheus: { + type: 'grafana-azureprometheus-datasource', + name: 'Azure Prometheus', + uid: 'uid-code-azp', + meta: { metrics: true, id: 'grafana-azureprometheus-datasource' }, + }, }; describe('Given a list of data sources', () => { @@ -309,7 +327,7 @@ describe('datasource_srv', () => { describe('when getting external metric sources', () => { it('should return list of explore sources', () => { const externalSources = dataSourceSrv.getExternal(); - expect(externalSources.length).toBe(8); + expect(externalSources.length).toBe(11); }); }); @@ -363,6 +381,28 @@ describe('datasource_srv', () => { expect(list[0].name).toBe('Jaeger'); }); + it('should include Prometheus flavor data sources when pluginId is prometheus', () => { + const list = dataSourceSrv.getList({ pluginId: 'prometheus' }); + expect(list.length).toBe(3); + expect(list[0].name).toBe('Amazon Prometheus'); + expect(list[0].type).toBe('grafana-amazonprometheus-datasource'); + expect(list[1].name).toBe('Azure Prometheus'); + expect(list[1].type).toBe('grafana-azureprometheus-datasource'); + expect(list[2].name).toBe('Prometheus'); + expect(list[2].type).toBe('prometheus'); + }); + + it('should include compatible Prometheus data sources when pluginId is a flavor of prometheus', () => { + const list = dataSourceSrv.getList({ pluginId: 'grafana-amazonprometheus-datasource' }); + expect(list.length).toBe(3); + expect(list[0].name).toBe('Amazon Prometheus'); + expect(list[0].type).toBe('grafana-amazonprometheus-datasource'); + expect(list[1].name).toBe('Azure Prometheus'); + expect(list[1].type).toBe('grafana-azureprometheus-datasource'); + expect(list[2].name).toBe('Prometheus'); + expect(list[2].type).toBe('prometheus'); + }); + it('should not include runtime datasources in list', () => { const list = dataSourceSrv.getList({ pluginId: 'grafana-runtime-datasource' }); expect(list.length).toBe(0); @@ -386,6 +426,24 @@ describe('datasource_srv', () => { "type": "test-db", "uid": "uid-code-aaa", }, + { + "meta": { + "id": "grafana-amazonprometheus-datasource", + "metrics": true, + }, + "name": "Amazon Prometheus", + "type": "grafana-amazonprometheus-datasource", + "uid": "uid-code-amp", + }, + { + "meta": { + "id": "grafana-azureprometheus-datasource", + "metrics": true, + }, + "name": "Azure Prometheus", + "type": "grafana-azureprometheus-datasource", + "uid": "uid-code-azp", + }, { "isDefault": true, "meta": { @@ -412,6 +470,15 @@ describe('datasource_srv', () => { "type": "test-db", "uid": "uid-code-mmm", }, + { + "meta": { + "id": "prometheus", + "metrics": true, + }, + "name": "Prometheus", + "type": "prometheus", + "uid": "uid-code-prometheus", + }, { "meta": { "aliasIDs": [ diff --git a/public/app/features/trails/Integrations/dashboardIntegration.ts b/public/app/features/trails/Integrations/dashboardIntegration.ts index dd52e73fe44..1eaa0e3dff3 100644 --- a/public/app/features/trails/Integrations/dashboardIntegration.ts +++ b/public/app/features/trails/Integrations/dashboardIntegration.ts @@ -30,7 +30,11 @@ export async function addDataTrailPanelAction(dashboard: DashboardScene, panel: return; } - if (datasource.type !== 'prometheus') { + if ( + datasource.type !== 'prometheus' && + datasource.type !== 'grafana-amazonprometheus-datasource' && + datasource.type !== 'grafana-azureprometheus-datasource' + ) { return; } diff --git a/public/app/features/trails/utils.ts b/public/app/features/trails/utils.ts index 2b721161203..d3ac6359293 100644 --- a/public/app/features/trails/utils.ts +++ b/public/app/features/trails/utils.ts @@ -121,7 +121,9 @@ export function getDatasourceForNewTrail(): string | undefined { return prevDataSource; } } - const promDatasources = getDatasourceSrv().getList({ type: 'prometheus' }); + const promDatasources = getDatasourceSrv().getList({ + type: ['prometheus', 'grafana-amazonprometheus-datasource', 'grafana-azureprometheus-datasource'], + }); if (promDatasources.length > 0) { const defaultDatasource = promDatasources.find((mds) => mds.isDefault); diff --git a/public/app/plugins/panel/alertlist/module.tsx b/public/app/plugins/panel/alertlist/module.tsx index 2a7d462c2d7..e11dd65ff5d 100644 --- a/public/app/plugins/panel/alertlist/module.tsx +++ b/public/app/plugins/panel/alertlist/module.tsx @@ -4,7 +4,10 @@ import { OldFolderPicker } from 'app/core/components/Select/OldFolderPicker'; import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { PermissionLevelString } from 'app/types'; -import { GRAFANA_DATASOURCE_NAME } from '../../../features/alerting/unified/utils/datasource'; +import { + GRAFANA_DATASOURCE_NAME, + SUPPORTED_RULE_SOURCE_TYPES, +} from '../../../features/alerting/unified/utils/datasource'; import { GroupBy } from './GroupByWithLoading'; import { UnifiedAlertListPanel } from './UnifiedAlertList'; @@ -112,7 +115,7 @@ const unifiedAlertList = new PanelPlugin(UnifiedAlertLi props.onChange(ds.name)} From 73ba19a98e259d73f080d4ce8c7c5b89d1e9e5da Mon Sep 17 00:00:00 2001 From: Nick Richmond <5732000+NWRichmond@users.noreply.github.com> Date: Thu, 10 Apr 2025 15:50:10 -0400 Subject: [PATCH 10/37] Dashboards: Fix panel link to Grafana Metrics Drilldown (#103759) * fix: panel link to Grafana Metrics Drilldown * test: handling of plugin links --- .../scene/PanelMenuBehavior.test.tsx | 149 ++++++++++++++++++ .../scene/PanelMenuBehavior.tsx | 51 ++++-- 2 files changed, 187 insertions(+), 13 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx index 5f0e1ae0e18..998541edf80 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.test.tsx @@ -553,6 +553,155 @@ describe('panelMenuBehavior', () => { expect(menu.state.items?.length).toBe(1); expect(menu.state.items?.[0].text).toBe('Explore'); }); + + describe('plugin links', () => { + it('should not show Metrics Drilldown menu when no Metrics Drilldown links exist', async () => { + getPluginExtensionsMock.mockReturnValue({ + extensions: [ + { + id: '1', + pluginId: '...', + type: PluginExtensionTypes.link, + title: 'Other Extension', + description: 'Some other extension', + path: '/a/other-app/action', + }, + ], + }); + + const { menu, panel } = await buildTestScene({}); + + panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); + + mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); + mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); + + menu.activate(); + + await new Promise((r) => setTimeout(r, 1)); + + const metricsDrilldownMenu = menu.state.items?.find((i) => i.text === 'Metrics drilldown'); + const extensionsMenu = menu.state.items?.find((i) => i.text === 'Extensions'); + + expect(metricsDrilldownMenu).toBeUndefined(); + expect(extensionsMenu).toBeDefined(); + expect(extensionsMenu?.subMenu).toEqual([ + expect.objectContaining({ + text: 'Other Extension', + href: '/a/other-app/action', + }), + ]); + }); + + it('should separate Metrics Drilldown links into their own menu', async () => { + getPluginExtensionsMock.mockReturnValue({ + extensions: [ + { + id: '1', + pluginId: '...', + type: PluginExtensionTypes.link, + title: 'Open in Metrics Drilldown', + description: 'Open current query in Metrics Drilldown', + path: '/a/grafana-metricsdrilldown-app/trail', + category: 'metrics-drilldown', + }, + { + id: '2', + pluginId: '...', + type: PluginExtensionTypes.link, + title: 'Other Extension', + description: 'Some other extension', + path: '/a/other-app/action', + }, + ], + }); + + const { menu, panel } = await buildTestScene({}); + + panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); + + mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); + mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); + + menu.activate(); + + await new Promise((r) => setTimeout(r, 1)); + + expect(menu.state.items?.length).toBe(8); // 6 base items + 2 extension menus + + const metricsDrilldownMenu = menu.state.items?.find((i) => i.text === 'Metrics drilldown'); + const extensionsMenu = menu.state.items?.find((i) => i.text === 'Extensions'); + + expect(metricsDrilldownMenu).toBeDefined(); + expect(metricsDrilldownMenu?.iconClassName).toBe('code-branch'); + expect(metricsDrilldownMenu?.subMenu).toEqual([ + expect.objectContaining({ + text: 'metrics-drilldown', + type: 'group', + subMenu: expect.arrayContaining([ + expect.objectContaining({ + text: 'Open in Metrics Drilld...', + href: '/a/grafana-metricsdrilldown-app/trail', + }), + ]), + }), + ]); + + expect(extensionsMenu).toBeDefined(); + expect(extensionsMenu?.iconClassName).toBe('plug'); + expect(extensionsMenu?.subMenu).toEqual([ + expect.objectContaining({ + text: 'Other Extension', + href: '/a/other-app/action', + }), + ]); + }); + + it('should not show extensions menu when no non-Metrics Drilldown links exist', async () => { + getPluginExtensionsMock.mockReturnValue({ + extensions: [ + { + id: '1', + pluginId: '...', + type: PluginExtensionTypes.link, + title: 'Open in Metrics Drilldown', + description: 'Open current query in Metrics Drilldown', + path: '/a/grafana-metricsdrilldown-app/trail', + category: 'metrics-drilldown', + }, + ], + }); + + const { menu, panel } = await buildTestScene({}); + + panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false }); + + mocks.contextSrv.hasAccessToExplore.mockReturnValue(true); + mocks.getExploreUrl.mockReturnValue(Promise.resolve('/explore')); + + menu.activate(); + + await new Promise((r) => setTimeout(r, 1)); + + const metricsDrilldownMenu = menu.state.items?.find((i) => i.text === 'Metrics drilldown'); + const extensionsMenu = menu.state.items?.find((i) => i.text === 'Extensions'); + + expect(metricsDrilldownMenu).toBeDefined(); + expect(extensionsMenu).toBeUndefined(); + expect(metricsDrilldownMenu?.subMenu).toEqual([ + expect.objectContaining({ + text: 'metrics-drilldown', + type: 'group', + subMenu: expect.arrayContaining([ + expect.objectContaining({ + text: 'Open in Metrics Drilld...', + href: '/a/grafana-metricsdrilldown-app/trail', + }), + ]), + }), + ]); + }); + }); }); describe('onCreateAlert', () => { diff --git a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx index 82909f80268..664526928a9 100644 --- a/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx +++ b/public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx @@ -4,6 +4,7 @@ import { LinkModel, PanelMenuItem, PanelPlugin, + PluginExtensionLink, PluginExtensionPanelContext, PluginExtensionPoints, PluginExtensionTypes, @@ -27,7 +28,6 @@ import { createPluginExtensionsGetter } from 'app/features/plugins/extensions/ge import { pluginExtensionRegistries } from 'app/features/plugins/extensions/registry/setup'; import { GetPluginExtensions } from 'app/features/plugins/extensions/types'; import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils'; -import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration'; import { dispatch } from 'app/store/store'; import { AccessControlAction } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; @@ -55,6 +55,9 @@ function setupGetPluginExtensions() { return getPluginExtensions; } +// Define the category for metrics drilldown links +const METRICS_DRILLDOWN_CATEGORY = 'metrics-drilldown'; + /** * Behavior is called when VizPanelMenu is activated (ie when it's opened). */ @@ -292,10 +295,6 @@ export function panelMenuBehavior(menu: VizPanelMenu) { }); } - if (config.featureToggles.exploreMetrics) { - await addDataTrailPanelAction(dashboard, panel, items); - } - if (exploreMenuItem) { items.push(exploreMenuItem); } @@ -310,15 +309,41 @@ export function panelMenuBehavior(menu: VizPanelMenu) { limitPerPlugin: 3, }); - const linkExtensions = extensions.filter((extension) => extension.type === PluginExtensionTypes.link); - if (extensions.length > 0 && !dashboard.state.isEditing) { - items.push({ - text: 'Extensions', - iconClassName: 'plug', - type: 'submenu', - subMenu: createExtensionSubMenu(linkExtensions), - }); + const linkExtensions = extensions.filter((extension) => extension.type === PluginExtensionTypes.link); + + // Separate metrics drilldown links from other links + const [metricsDrilldownLinks, otherLinks] = linkExtensions.reduce<[PluginExtensionLink[], PluginExtensionLink[]]>( + ([metricsDrilldownLinks, otherLinks], link) => { + if (link.category === METRICS_DRILLDOWN_CATEGORY) { + metricsDrilldownLinks.push(link); + } else { + otherLinks.push(link); + } + return [metricsDrilldownLinks, otherLinks]; + }, + [[], []] + ); + + // Add specific "Metrics drilldown" menu + if (metricsDrilldownLinks.length > 0) { + items.push({ + text: 'Metrics drilldown', + iconClassName: 'code-branch', + type: 'submenu', + subMenu: createExtensionSubMenu(metricsDrilldownLinks), + }); + } + + // Add generic "Extensions" menu for other links + if (otherLinks.length > 0) { + items.push({ + text: 'Extensions', + iconClassName: 'plug', + type: 'submenu', + subMenu: createExtensionSubMenu(otherLinks), + }); + } } if (moreSubMenu.length) { From 0bad0526f529a16ac1cc488e6a960e847d8c32b4 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Thu, 10 Apr 2025 22:54:56 +0300 Subject: [PATCH 11/37] Provisioning: Simplify component logic (#103816) --- .../provisioning/Config/ConfigForm.tsx | 46 +-- .../provisioning/Job/FinishedJobStatus.tsx | 24 +- .../features/provisioning/Job/JobStatus.tsx | 25 +- .../Repository/RepositoryLink.tsx | 18 +- .../provisioning/Wizard/BootstrapStep.tsx | 157 +++----- .../provisioning/Wizard/ConnectStep.tsx | 5 +- .../Wizard/ProvisioningWizard.tsx | 336 +++++++++++++----- .../provisioning/Wizard/SynchronizeStep.tsx | 49 +-- .../provisioning/Wizard/WizardContent.tsx | 262 -------------- .../features/provisioning/Wizard/actions.ts | 140 ++++---- .../app/features/provisioning/Wizard/types.ts | 9 +- .../provisioning/hooks/useStepStatus.ts | 25 -- public/locales/en-US/grafana.json | 24 +- 13 files changed, 440 insertions(+), 680 deletions(-) delete mode 100644 public/app/features/provisioning/Wizard/WizardContent.tsx delete mode 100644 public/app/features/provisioning/hooks/useStepStatus.ts diff --git a/public/app/features/provisioning/Config/ConfigForm.tsx b/public/app/features/provisioning/Config/ConfigForm.tsx index 522a6c7f443..780cdca848e 100644 --- a/public/app/features/provisioning/Config/ConfigForm.tsx +++ b/public/app/features/provisioning/Config/ConfigForm.tsx @@ -39,7 +39,7 @@ export function getDefaultValues(repository?: RepositorySpec): RepositoryFormDat path: 'grafana/', sync: { enabled: false, - target: 'folder', + target: 'instance', intervalSeconds: 60, }, }; @@ -47,6 +47,20 @@ export function getDefaultValues(repository?: RepositorySpec): RepositoryFormDat return specToData(repository); } +const getOptions = () => { + const typeOptions = [ + { value: 'github', label: t('provisioning.config-form.option-github', 'GitHub') }, + { value: 'local', label: t('provisioning.config-form.option-local', 'Local') }, + ]; + + const targetOptions = [ + { value: 'instance', label: t('provisioning.config-form.option-entire-instance', 'Entire instance') }, + { value: 'folder', label: t('provisioning.config-form.option-managed-folder', 'Managed folder') }, + ]; + + return [typeOptions, targetOptions]; +}; + export interface ConfigFormProps { data?: Repository; } @@ -66,22 +80,8 @@ export function ConfigForm({ data }: ConfigFormProps) { const [tokenConfigured, setTokenConfigured] = useState(isEdit); const navigate = useNavigate(); const [type, readOnly] = watch(['type', 'readOnly']); - - const typeOptions = useMemo( - () => [ - { value: 'github', label: t('provisioning.config-form.option-github', 'GitHub') }, - { value: 'local', label: t('provisioning.config-form.option-local', 'Local') }, - ], - [] - ); - - const targetOptions = useMemo( - () => [ - { value: 'instance', label: t('provisioning.config-form.option-entire-instance', 'Entire instance') }, - { value: 'folder', label: t('provisioning.config-form.option-managed-folder', 'Managed folder') }, - ], - [] - ); + const [typeOptions, targetOptions] = useMemo(() => getOptions(), []); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { if (request.isSuccess) { @@ -93,14 +93,16 @@ export function ConfigForm({ data }: ConfigFormProps) { } }, [request.isSuccess, reset, getValues, navigate]); - const onSubmit = (form: RepositoryFormData) => { + const onSubmit = async (form: RepositoryFormData) => { + setIsLoading(true); const spec = dataToSpec(form); if (spec.github) { spec.github.token = form.token || data?.spec?.github?.token; // If we're still keeping this as GitHub, persist the old token. If we set a new one, it'll be re-encrypted into here. spec.github.encryptedToken = data?.spec?.github?.encryptedToken; } - submitData(spec); + await submitData(spec); + setIsLoading(false); }; // NOTE: We do not want the lint option to be listed. @@ -202,7 +204,7 @@ export function ConfigForm({ data }: ConfigFormProps) { label={t('provisioning.config-form.label-path', 'Path')} description={t('provisioning.config-form.description-path', 'Path to a subdirectory in the Git repository')} > - + )} @@ -303,8 +305,8 @@ export function ConfigForm({ data }: ConfigFormProps) { - diff --git a/public/app/features/provisioning/Job/FinishedJobStatus.tsx b/public/app/features/provisioning/Job/FinishedJobStatus.tsx index 0f0277f510b..c7932eaecd1 100644 --- a/public/app/features/provisioning/Job/FinishedJobStatus.tsx +++ b/public/app/features/provisioning/Job/FinishedJobStatus.tsx @@ -4,24 +4,17 @@ import { Alert, Spinner, Stack, Text } from '@grafana/ui'; import { useGetRepositoryJobsWithPathQuery } from 'app/api/clients/provisioning'; import { Trans, t } from 'app/core/internationalization'; +import { StepStatusInfo } from '../Wizard/types'; + import { JobContent } from './JobContent'; -import { useJobStatusEffect } from './hooks'; export interface FinishedJobProps { jobUid: string; repositoryName: string; - onStatusChange?: (success: boolean) => void; - onRunningChange?: (isRunning: boolean) => void; - onErrorChange?: (error: string | null) => void; + onStatusChange: (status: StepStatusInfo, error?: string) => void; } -export function FinishedJobStatus({ - jobUid, - repositoryName, - onStatusChange, - onRunningChange, - onErrorChange, -}: FinishedJobProps) { +export function FinishedJobStatus({ jobUid, repositoryName, onStatusChange }: FinishedJobProps) { const hasRetried = useRef(false); const finishedQuery = useGetRepositoryJobsWithPathQuery({ name: repositoryName, @@ -31,8 +24,6 @@ export function FinishedJobStatus({ const job = finishedQuery.data; - useJobStatusEffect(job, onStatusChange, onRunningChange, onErrorChange); - useEffect(() => { const shouldRetry = !job && !hasRetried.current && !finishedQuery.isFetching; let timeoutId: ReturnType; @@ -44,14 +35,19 @@ export function FinishedJobStatus({ }, 1000); } + if (finishedQuery.isSuccess) { + onStatusChange({ status: 'success' }); + } + return () => { if (timeoutId) { clearTimeout(timeoutId); } }; - }, [finishedQuery, job]); + }, [finishedQuery, job, onStatusChange]); if (retryFailed) { + onStatusChange({ status: 'error' }); return ( diff --git a/public/app/features/provisioning/Job/JobStatus.tsx b/public/app/features/provisioning/Job/JobStatus.tsx index 1a80c742ab0..7ab68023055 100644 --- a/public/app/features/provisioning/Job/JobStatus.tsx +++ b/public/app/features/provisioning/Job/JobStatus.tsx @@ -2,17 +2,17 @@ import { Spinner, Stack, Text } from '@grafana/ui'; import { Job, useListJobQuery } from 'app/api/clients/provisioning'; import { Trans } from 'app/core/internationalization'; +import { StepStatusInfo } from '../Wizard/types'; + import { ActiveJobStatus } from './ActiveJobStatus'; import { FinishedJobStatus } from './FinishedJobStatus'; export interface JobStatusProps { watch: Job; - onStatusChange?: (success: boolean) => void; - onRunningChange?: (isRunning: boolean) => void; - onErrorChange?: (error: string | null) => void; + onStatusChange: (status: StepStatusInfo, error?: string) => void; } -export function JobStatus({ watch, onStatusChange, onRunningChange, onErrorChange }: JobStatusProps) { +export function JobStatus({ watch, onStatusChange }: JobStatusProps) { const activeQuery = useListJobQuery({ fieldSelector: `metadata.name=${watch.metadata?.name}`, watch: true, @@ -36,25 +36,12 @@ export function JobStatus({ watch, onStatusChange, onRunningChange, onErrorChang } if (activeJob) { - return ( - - ); + return ; } if (shouldCheckFinishedJobs) { return ( - + ); } diff --git a/public/app/features/provisioning/Repository/RepositoryLink.tsx b/public/app/features/provisioning/Repository/RepositoryLink.tsx index 3296dde6b7d..898c1eb7dab 100644 --- a/public/app/features/provisioning/Repository/RepositoryLink.tsx +++ b/public/app/features/provisioning/Repository/RepositoryLink.tsx @@ -14,16 +14,12 @@ export function RepositoryLink({ name }: RepositoryLinkProps) { const repoQuery = useGetRepositoryQuery(name ? { name } : skipToken); const repo = repoQuery.data; - if (!repo || repoQuery.isLoading || repo.spec?.type !== 'github' || !repo.spec?.github?.url) { + if (!repo || repoQuery.isLoading) { return null; } const repoHref = getRepoHref(repo.spec?.github); - if (!repoHref) { - return null; - } - return ( @@ -32,11 +28,13 @@ export function RepositoryLink({ name }: RepositoryLinkProps) { and the external storage will be synchronized. - - - View repository - - + {repoHref && ( + + + View repository + + + )} ); } diff --git a/public/app/features/provisioning/Wizard/BootstrapStep.tsx b/public/app/features/provisioning/Wizard/BootstrapStep.tsx index 6fb049a8539..f06e5764450 100644 --- a/public/app/features/provisioning/Wizard/BootstrapStep.tsx +++ b/public/app/features/provisioning/Wizard/BootstrapStep.tsx @@ -1,78 +1,66 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { - Alert, - Box, - Card, - Field, - FieldSet, - Icon, - Input, - LoadingPlaceholder, - Stack, - Switch, - Text, - Tooltip, -} from '@grafana/ui'; +import { Box, Card, Field, Input, LoadingPlaceholder, Stack, Text } from '@grafana/ui'; import { RepositoryViewList, useGetRepositoryFilesQuery, useGetResourceStatsQuery } from 'app/api/clients/provisioning'; import { t, Trans } from 'app/core/internationalization'; -import { StepStatus } from '../hooks/useStepStatus'; - -import { getState } from './actions'; -import { ModeOption, WizardFormData } from './types'; +import { getResourceStats, useModeOptions } from './actions'; +import { StepStatusInfo, WizardFormData } from './types'; interface Props { onOptionSelect: (requiresMigration: boolean) => void; - onStepUpdate: (status: StepStatus, error?: string) => void; + onStepStatusUpdate: (info: StepStatusInfo) => void; settingsData?: RepositoryViewList; repoName: string; } -export function BootstrapStep({ onOptionSelect, settingsData, repoName }: Props) { +export function BootstrapStep({ onOptionSelect, settingsData, repoName, onStepStatusUpdate }: Props) { const { register, control, setValue, watch, + getValues, formState: { errors }, } = useFormContext(); - const selectedTarget = watch('repository.sync.target'); - const repoType = watch('repository.type'); - const resourceStats = useGetResourceStatsQuery(); const filesQuery = useGetRepositoryFilesQuery({ name: repoName }); - const [selectedOption, setSelectedOption] = useState(null); - - const state = useMemo(() => { - return getState(repoName, settingsData, filesQuery.data, resourceStats.data); - }, [repoName, settingsData, resourceStats.data, filesQuery.data]); + const selectedTarget = watch('repository.sync.target'); + const options = useModeOptions(repoName, settingsData); + const { resourceCount, resourceCountString, fileCount } = useMemo( + () => getResourceStats(filesQuery.data, resourceStats.data), + [filesQuery.data, resourceStats.data] + ); useEffect(() => { - if (state.actions.length && !selectedOption) { - const first = state.actions[0]; - setSelectedOption(first); - onOptionSelect(first.operation === 'migrate'); - setValue('repository.sync.target', first.target); + // Pick a name nice name based on type+settings + const repository = getValues('repository'); + switch (repository.type) { + case 'github': + const name = repository.url ?? 'github'; + setValue('repository.title', name.replace('https://github.com/', '')); + break; + case 'local': + setValue('repository.title', repository.path ?? 'local'); + break; } - }, [state, selectedOption, setValue, onOptionSelect]); + }, [getValues, setValue]); - const handleOptionSelect = useCallback( - (option: ModeOption) => { - // Select the new option and update form state - setSelectedOption(option); - setValue('repository.sync.target', option.target); + useEffect(() => { + const isLoading = resourceStats.isLoading || filesQuery.isLoading; + onStepStatusUpdate({ status: isLoading ? 'running' : 'idle' }); + }, [filesQuery.isLoading, onStepStatusUpdate, resourceStats.isLoading]); - if (option.operation === 'migrate') { - setValue('migrate.history', true); - setValue('migrate.identifier', true); - } - onOptionSelect(option.operation === 'migrate'); - }, - [setValue, onOptionSelect] - ); + // Auto select the first option on mount + useEffect(() => { + const { target } = options[0]; + setValue('repository.sync.target', target); + onOptionSelect(target !== 'folder' || resourceCount > 0); + // Only run this effect on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); if (resourceStats.isLoading || filesQuery.isLoading) { return ( @@ -84,9 +72,6 @@ export function BootstrapStep({ onOptionSelect, settingsData, repoName }: Props) ); } - // Show the history selection - const canIncludeHistory = repoType === 'github' && settingsData?.legacyStorage; - return ( @@ -98,9 +83,7 @@ export function BootstrapStep({ onOptionSelect, settingsData, repoName }: Props) - {state.resourceCount > 0 - ? state.resourceCountString - : t('provisioning.bootstrap-step.empty', 'Empty')} + {resourceCount > 0 ? resourceCountString : t('provisioning.bootstrap-step.empty', 'Empty')} @@ -109,8 +92,8 @@ export function BootstrapStep({ onOptionSelect, settingsData, repoName }: Props) External storage - {state.fileCount > 0 - ? t('provisioning.bootstrap-step.files-count', '{{count}} files', { count: state.fileCount }) + {fileCount > 0 + ? t('provisioning.bootstrap-step.files-count', '{{count}} files', { count: fileCount }) : t('provisioning.bootstrap-step.empty', 'Empty')} @@ -120,16 +103,16 @@ export function BootstrapStep({ onOptionSelect, settingsData, repoName }: Props) ( + render={({ field: { ref, onChange, ...field } }) => ( <> - {state.actions.map((action, index) => ( + {options.map((action, index) => ( { - handleOptionSelect(action); + onChange(action.target); }} - autoFocus={index === 0} + {...field} > {action.label} @@ -144,54 +127,6 @@ export function BootstrapStep({ onOptionSelect, settingsData, repoName }: Props) )} /> - {/* Add migration options */} - {selectedOption?.operation === 'migrate' && ( - <> - {Boolean(state.resourceCount) && ( - - - Dashboards will be unavailable while running this process - - - )} - {Boolean(state.fileCount) && Boolean(state.resourceCount) && ( - - - - )} - {canIncludeHistory && ( -
- - {canIncludeHistory && ( - - - - Include history - - - - - - )} - -
- )} - - )} - {/* Only show title field if folder sync */} {selectedTarget === 'folder' && ( )} diff --git a/public/app/features/provisioning/Wizard/ConnectStep.tsx b/public/app/features/provisioning/Wizard/ConnectStep.tsx index a85321ec151..722445f20e3 100644 --- a/public/app/features/provisioning/Wizard/ConnectStep.tsx +++ b/public/app/features/provisioning/Wizard/ConnectStep.tsx @@ -107,10 +107,7 @@ export function ConnectStep() { 'This is the path to a subdirectory in your GitHub repository where dashboards will be stored and provisioned from' )} > - + )} diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx index e5e6d282ccf..dd0d5f167dd 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx @@ -1,75 +1,134 @@ -import { useCallback, useMemo, useState } from 'react'; +import { css } from '@emotion/css'; +import { useCallback, useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom-v5-compat'; -import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning'; +import { AppEvents, GrafanaTheme2 } from '@grafana/data'; +import { getAppEvents } from '@grafana/runtime'; +import { Alert, Box, Button, Stack, Text, useStyles2 } from '@grafana/ui'; +import { useDeleteRepositoryMutation, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning'; +import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt'; import { t } from 'app/core/internationalization'; import { getDefaultValues } from '../Config/ConfigForm'; import { PROVISIONING_URL } from '../constants'; +import { useCreateOrUpdateRepository } from '../hooks/useCreateOrUpdateRepository'; +import { dataToSpec } from '../utils/data'; -import { Step } from './Stepper'; -import { WizardContent } from './WizardContent'; -import { RepoType, WizardFormData, WizardStep } from './types'; +import { BootstrapStep } from './BootstrapStep'; +import { ConnectStep } from './ConnectStep'; +import { FinishStep } from './FinishStep'; +import { RequestErrorAlert } from './RequestErrorAlert'; +import { Step, Stepper } from './Stepper'; +import { SynchronizeStep } from './SynchronizeStep'; +import { RepoType, StepStatusInfo, WizardFormData, WizardStep } from './types'; + +const appEvents = getAppEvents(); + +const getSteps = (): Array> => { + return [ + { + id: 'connection', + name: t('provisioning.wizard.step-connect', 'Connect'), + title: t('provisioning.wizard.title-connect', 'Connect to external storage'), + submitOnNext: true, + }, + { + id: 'bootstrap', + name: t('provisioning.wizard.step-bootstrap', 'Choose what to synchronize'), + title: t('provisioning.wizard.title-bootstrap', 'Choose what to synchronize'), + submitOnNext: true, + }, + { + id: 'synchronize', + name: t('provisioning.wizard.step-synchronize', 'Synchronize'), + title: t('provisioning.wizard.title-synchronize', 'Synchronize with external storage'), + submitOnNext: false, + }, + { + id: 'finish', + name: t('provisioning.wizard.step-finish', 'Choose additional settings'), + title: t('provisioning.wizard.title-finish', 'Choose additional settings'), + submitOnNext: true, + }, + ]; +}; export function ProvisioningWizard({ type }: { type: RepoType }) { const [activeStep, setActiveStep] = useState('connection'); const [completedSteps, setCompletedSteps] = useState([]); - const [stepSuccess, setStepSuccess] = useState(false); const [requiresMigration, setRequiresMigration] = useState(false); + const [stepStatusInfo, setStepStatusInfo] = useState({ status: 'idle' }); + + const [isSubmitting, setIsSubmitting] = useState(false); + const [isCancelling, setIsCancelling] = useState(false); + const settingsQuery = useGetFrontendSettingsQuery(); const navigate = useNavigate(); + const steps = getSteps(); + const styles = useStyles2(getStyles); + const values = getDefaultValues(); - - const steps = useMemo>>( - () => [ - { - id: 'connection', - name: t('provisioning.wizard.step-connect', 'Connect'), - title: t('provisioning.wizard.title-connect', 'Connect to external storage'), - submitOnNext: true, - }, - { - id: 'bootstrap', - name: t('provisioning.wizard.step-bootstrap', 'Choose what to synchronize'), - title: t('provisioning.wizard.title-bootstrap', 'Choose what to synchronize'), - submitOnNext: true, - }, - { - id: 'synchronize', - name: t('provisioning.wizard.step-synchronize', 'Synchronize'), - title: t('provisioning.wizard.title-synchronize', 'Synchronize with external storage'), - submitOnNext: false, - }, - { - id: 'finish', - name: t('provisioning.wizard.step-finish', 'Choose additional settings'), - title: t('provisioning.wizard.title-finish', 'Choose additional settings'), - submitOnNext: true, - }, - ], - [] - ); - const methods = useForm({ defaultValues: { repository: { ...values, type }, migrate: { history: true, - identifier: true, // Keep the same URLs }, }, }); - const handleStatusChange = useCallback( - (success: boolean) => { - setStepSuccess(success); - if (success) { - setCompletedSteps((prev) => [...prev, activeStep]); - } - }, - [activeStep] - ); + const { + watch, + setValue, + getValues, + trigger, + formState: { isDirty }, + } = methods; + + const repoName = watch('repositoryName'); + const [submitData, saveRequest] = useCreateOrUpdateRepository(repoName); + const [deleteRepository] = useDeleteRepositoryMutation(); + + const currentStepIndex = steps.findIndex((s) => s.id === activeStep); + const currentStepConfig = steps[currentStepIndex]; + const isStepSuccess = stepStatusInfo.status === 'success'; + + // A different repository is marked with instance target -- nothing will succeed + useEffect(() => { + if (settingsQuery.data?.items.some((item) => item.target === 'instance' && item.name !== repoName)) { + appEvents.publish({ + type: AppEvents.alertError.name, + payload: [ + t('provisioning.wizard-content.error-instance-repository-exists', 'Instance repository already exists'), + ], + }); + + navigate(PROVISIONING_URL); + } + }, [navigate, repoName, settingsQuery.data?.items]); + + const handleRepositoryDeletion = async (name: string) => { + try { + await deleteRepository({ name }); + // Wait before redirecting to ensure deletion is processed + setTimeout(() => { + navigate(PROVISIONING_URL); + }, 1000); + } catch (error) { + setIsCancelling(false); + } + }; + + const handleCancel = async () => { + // For the first step, do not delete anything — just go back. + if (activeStep === 'connection' || !repoName) { + navigate(PROVISIONING_URL); + return; + } + setIsCancelling(true); + handleRepositoryDeletion(repoName); + }; // Calculate button text based on current step position const getNextButtonText = useCallback( @@ -87,61 +146,154 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { ); const handleNext = async () => { - const currentStepIndex = steps.findIndex((s) => s.id === activeStep); const isLastStep = currentStepIndex === steps.length - 1; - if (activeStep === 'connection') { - // Validate repository form data before proceeding - const isValid = await methods.trigger('repository'); - if (!isValid) { - return; - } - - // Pick a name nice name based on type+settings - const current = methods.getValues(); - switch (current.repository.type) { - case 'github': - const name = current.repository.url ?? 'github'; - methods.setValue('repository.title', name.replace('https://github.com/', '')); - break; - case 'local': - methods.setValue('repository.title', current.repository.path ?? 'local'); - break; - } - } - - // Only navigate to provisioning URL if we're on the actual last step and it's completed - if (isLastStep && stepSuccess) { - settingsQuery.refetch(); + // Only navigate to provisioning URL if we're on the actual last step + if (isLastStep) { navigate(PROVISIONING_URL); - return; - } - - // For all other cases, proceed to next step - if (currentStepIndex < steps.length - 1) { + } else { setActiveStep(steps[currentStepIndex + 1].id); - setStepSuccess(false); - // Update completed steps only if the current step was successful - if (stepSuccess) { - setCompletedSteps((prev) => [...prev, activeStep]); + setCompletedSteps((prev) => [...new Set([...prev, activeStep])]); + setStepStatusInfo({ status: 'idle' }); + } + }; + + const onSubmit = async () => { + if (currentStepConfig?.submitOnNext) { + // Validate form data before proceeding + if (activeStep === 'connection' || activeStep === 'bootstrap') { + const isValid = await trigger(['repository', 'repository.title']); + if (!isValid) { + return; + } + } + + setIsSubmitting(true); + try { + const formData = getValues(); + const spec = dataToSpec(formData.repository); + const rsp = await submitData(spec); + if (rsp.error) { + setStepStatusInfo({ + status: 'error', + error: 'Repository request failed', + }); + return; + } + + // Fill in the k8s name from the initial POST response + const name = rsp.data?.metadata?.name; + if (name) { + setValue('repositoryName', name); + setStepStatusInfo({ status: 'success' }); + handleNext(); + } else { + console.error('Saved repository without a name:', rsp); + } + } catch (error) { + setStepStatusInfo({ + status: 'error', + error: 'Repository connection failed', + }); + } finally { + setIsSubmitting(false); + } + } else { + // only proceed if the job was successful + if (isStepSuccess) { + handleNext(); } } }; + const isNextButtonDisabled = () => { + if (activeStep === 'synchronize') { + return stepStatusInfo.status !== 'success'; + } + return isSubmitting || isCancelling || stepStatusInfo.status === 'running' || stepStatusInfo.status === 'error'; + }; + return ( - + + +
+
+ + + + {/* eslint-disable-next-line @grafana/no-untranslated-strings */} + + {currentStepIndex + 1}. {currentStepConfig?.title} + + + + + +
+ {activeStep === 'connection' && } + {activeStep === 'bootstrap' && ( + + )} + {activeStep === 'synchronize' && ( + + )} + {activeStep === 'finish' && } +
+ + {stepStatusInfo.status === 'error' && ( + + )} + + + + + +
+ + ); } + +const getStyles = (theme: GrafanaTheme2) => ({ + form: css({ + maxWidth: '900px', + flexGrow: 1, + }), + divider: css({ + width: 1, + alignSelf: 'stretch', + backgroundColor: theme.colors.border.weak, + // align with the button row + marginBottom: theme.spacing(13), + }), + content: css({ + borderBottom: `1px solid ${theme.colors.border.weak}`, + paddingBottom: theme.spacing(4), + marginBottom: theme.spacing(4), + }), +}); diff --git a/public/app/features/provisioning/Wizard/SynchronizeStep.tsx b/public/app/features/provisioning/Wizard/SynchronizeStep.tsx index 207910c8eb3..2d860220380 100644 --- a/public/app/features/provisioning/Wizard/SynchronizeStep.tsx +++ b/public/app/features/provisioning/Wizard/SynchronizeStep.tsx @@ -6,29 +6,31 @@ import { Job, useCreateRepositoryJobsMutation } from 'app/api/clients/provisioni import { t, Trans } from 'app/core/internationalization'; import { JobStatus } from '../Job/JobStatus'; -import { StepStatus } from '../hooks/useStepStatus'; -import { WizardFormData } from './types'; +import { StepStatusInfo, WizardFormData } from './types'; export interface SynchronizeStepProps { - onStepUpdate: (status: StepStatus, error?: string) => void; + onStepStatusUpdate: (info: StepStatusInfo) => void; requiresMigration: boolean; } -export function SynchronizeStep({ onStepUpdate, requiresMigration }: SynchronizeStepProps) { +export function SynchronizeStep({ onStepStatusUpdate, requiresMigration }: SynchronizeStepProps) { const [createJob] = useCreateRepositoryJobsMutation(); const { getValues, register } = useFormContext(); - const [history, repoName] = getValues(['migrate.history', 'repositoryName']); const [job, setJob] = useState(); const startSynchronization = async () => { + const [history, repoName] = getValues(['migrate.history', 'repositoryName']); if (!repoName) { - onStepUpdate('error', t('provisioning.synchronize-step.error-no-repository-name', 'No repository name provided')); + onStepStatusUpdate({ + status: 'error', + error: t('provisioning.synchronize-step.error-no-repository-name', 'No repository name provided'), + }); return; } try { - onStepUpdate('running'); + onStepStatusUpdate({ status: 'running' }); const jobSpec = requiresMigration ? { migrate: { @@ -47,37 +49,22 @@ export function SynchronizeStep({ onStepUpdate, requiresMigration }: Synchronize }).unwrap(); if (!response?.metadata?.name) { - return onStepUpdate('error', t('provisioning.synchronize-step.error-no-job-id', 'Failed to start job')); + return onStepStatusUpdate({ + status: 'error', + error: t('provisioning.synchronize-step.error-no-job-id', 'Failed to start job'), + }); } setJob(response); } catch (error) { - onStepUpdate('error', t('provisioning.synchronize-step.error-starting-job', 'Error starting job')); + onStepStatusUpdate({ + status: 'error', + error: t('provisioning.synchronize-step.error-starting-job', 'Error starting job'), + }); } }; if (job) { - return ( - { - if (success) { - onStepUpdate('success'); - } else { - onStepUpdate('error', t('provisioning.synchronize-step.error-job-failed', 'Job failed')); - } - }} - onRunningChange={(isRunning) => { - if (isRunning) { - onStepUpdate('running'); - } - }} - onErrorChange={(error) => { - if (error) { - onStepUpdate('error', error); - } - }} - /> - ); + return ; } return ( diff --git a/public/app/features/provisioning/Wizard/WizardContent.tsx b/public/app/features/provisioning/Wizard/WizardContent.tsx deleted file mode 100644 index 25fe309730e..00000000000 --- a/public/app/features/provisioning/Wizard/WizardContent.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { css } from '@emotion/css'; -import { useCallback, useEffect, useState } from 'react'; -import { useFormContext } from 'react-hook-form'; -import { useNavigate } from 'react-router-dom-v5-compat'; - -import { AppEvents, GrafanaTheme2 } from '@grafana/data'; -import { getAppEvents } from '@grafana/runtime'; -import { Alert, Box, Button, Stack, Text, useStyles2 } from '@grafana/ui'; -import { - RepositoryViewList, - useDeleteRepositoryMutation, - useGetFrontendSettingsQuery, -} from 'app/api/clients/provisioning'; -import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt'; -import { t } from 'app/core/internationalization'; - -import { PROVISIONING_URL } from '../constants'; -import { useCreateOrUpdateRepository } from '../hooks/useCreateOrUpdateRepository'; -import { StepStatus } from '../hooks/useStepStatus'; -import { dataToSpec } from '../utils/data'; - -import { BootstrapStep } from './BootstrapStep'; -import { ConnectStep } from './ConnectStep'; -import { FinishStep } from './FinishStep'; -import { RequestErrorAlert } from './RequestErrorAlert'; -import { Step, Stepper } from './Stepper'; -import { SynchronizeStep } from './SynchronizeStep'; -import { WizardFormData, WizardStep } from './types'; - -const appEvents = getAppEvents(); - -interface WizardContentProps { - activeStep: WizardStep; - completedSteps: WizardStep[]; - availableSteps: Array>; - requiresMigration: boolean; - handleStatusChange: (success: boolean) => void; - handleNext: () => void; - getNextButtonText: (step: WizardStep) => string; - onOptionSelect: (requiresMigration: boolean) => void; - stepSuccess: boolean; - settingsData?: RepositoryViewList; -} - -export function WizardContent({ - activeStep, - completedSteps, - availableSteps, - requiresMigration, - handleStatusChange, - handleNext, - getNextButtonText, - onOptionSelect, - stepSuccess, - settingsData, -}: WizardContentProps) { - const { - watch, - setValue, - getValues, - trigger, - formState: { isDirty }, - } = useFormContext(); - const navigate = useNavigate(); - - const repoName = watch('repositoryName'); - const [submitData, saveRequest] = useCreateOrUpdateRepository(repoName); - const [deleteRepository] = useDeleteRepositoryMutation(); - const [isSubmitting, setIsSubmitting] = useState(false); - const [isCancelling, setIsCancelling] = useState(false); - const [stepStatus, setStepStatus] = useState('idle'); - const [stepError, setStepError] = useState(); - - const styles = useStyles2(getStyles); - const settingsQuery = useGetFrontendSettingsQuery(); - - const currentStep = availableSteps.find((s) => s.id === activeStep); - const currentStepIndex = availableSteps.findIndex((s) => s.id === activeStep); - - const handleStepUpdate = useCallback((status: StepStatus, error?: string) => { - setStepStatus(status); - setStepError(error); - }, []); - - // A different repository is marked with instance target -- nothing will succeed - if (settingsQuery.data?.items.some((item) => item.target === 'instance' && item.name !== repoName)) { - appEvents.publish({ - type: AppEvents.alertError.name, - payload: [ - t('provisioning.wizard-content.error-instance-repository-exists', 'Instance repository already exists'), - ], - }); - if (repoName) { - console.warn('Should we delete the pending repo?', repoName); - } - navigate(PROVISIONING_URL); - return null; - } - - const handleRepositoryDeletion = async (name: string) => { - try { - await deleteRepository({ name }); - // Wait before redirecting to ensure deletion is processed - setTimeout(() => { - navigate(PROVISIONING_URL); - }, 1000); - } catch (error) { - setIsCancelling(false); - } - }; - - const handleCancel = async () => { - // For the first step, do not delete anything—just go back. - if (activeStep === 'connection' || !repoName) { - navigate(PROVISIONING_URL); - return; - } - setIsCancelling(true); - void handleRepositoryDeletion(repoName); - }; - - const handleNextWithSubmit = async () => { - if (currentStep?.submitOnNext) { - // Validate form data before proceeding - if (activeStep === 'connection' || activeStep === 'bootstrap') { - const isValid = await trigger(['repository', 'repository.title']); - if (!isValid) { - return; - } - } - - setIsSubmitting(true); - try { - const formData = getValues(); - const spec = dataToSpec(formData.repository); - const rsp = await submitData(spec); - if (rsp.error) { - // Error is displayed in - return; - } - - // Fill in the k8s name from the initial POST response - const name = rsp.data?.metadata?.name; - if (name) { - setValue('repositoryName', name); - handleNext(); - } else { - console.error('Saved repository without a name:', rsp); - } - } catch (error) { - console.error('Repository connection failed:', error); - handleStatusChange(false); - } finally { - setIsSubmitting(false); - } - } else { - // only proceed if the job was successful - if (stepSuccess || stepStatus === 'success') { - handleNext(); - } - } - }; - - useEffect(() => { - if (saveRequest.isSuccess) { - const newName = saveRequest.data?.metadata?.name; - if (newName) { - setValue('repositoryName', newName); - handleStatusChange(true); - } - } else if (saveRequest.isError) { - handleStatusChange(false); - } - }, [saveRequest, setValue, handleStatusChange]); - - const isNextButtonDisabled = () => { - if (activeStep === 'synchronize') { - return stepStatus !== 'success'; - } - return isSubmitting || isCancelling || stepStatus === 'running' || stepStatus === 'error'; - }; - - return ( - - -
-
- - - - {/* eslint-disable-next-line @grafana/no-untranslated-strings */} - - {currentStepIndex + 1}. {currentStep?.title} - - - - - -
- {activeStep === 'connection' && } - {activeStep === 'bootstrap' && ( - - )} - {activeStep === 'synchronize' && ( - - )} - {activeStep === 'finish' && } -
- - {stepError && } - - - - - -
- - - ); -} - -const getStyles = (theme: GrafanaTheme2) => ({ - form: css({ - maxWidth: '900px', - flexGrow: 1, - }), - divider: css({ - width: 1, - alignSelf: 'stretch', - backgroundColor: theme.colors.border.weak, - // align with the button row - marginBottom: theme.spacing(13), - }), - content: css({ - borderBottom: `1px solid ${theme.colors.border.weak}`, - paddingBottom: theme.spacing(4), - marginBottom: theme.spacing(4), - }), -}); diff --git a/public/app/features/provisioning/Wizard/actions.ts b/public/app/features/provisioning/Wizard/actions.ts index 1add1f04fdb..a80e092bfae 100644 --- a/public/app/features/provisioning/Wizard/actions.ts +++ b/public/app/features/provisioning/Wizard/actions.ts @@ -1,59 +1,75 @@ +import { useMemo } from 'react'; + import { GetRepositoryFilesApiResponse, GetResourceStatsApiResponse, RepositoryViewList, } from 'app/api/clients/provisioning'; +import { t } from 'app/core/internationalization'; -import { ModeOption, SystemState } from './types'; +import { ModeOption } from './types'; -const migrateInstance: ModeOption = { - target: 'instance', - operation: 'migrate', - label: 'Sync all resources with external storage', - description: - 'Resources will be synced with external storage and provisioned into this instance. Existing Grafana resources will be migrated and merged if needed. After setup, all new resources and changes will be saved to external storage and automatically provisioned back into the instance.', - subtitle: 'Use this option if you want to sync and manage your entire Grafana instance through external storage.', -}; - -const pullFolder: ModeOption = { - target: 'folder', - operation: 'pull', - label: 'Sync external storage to a new Grafana folder', - description: - 'After setup, a new Grafana folder will be created and synced with external storage. If any resources are present in external storage, they will be provisioned to this new folder. All new resources created in this folder will be stored and versioned in external storage.', - subtitle: - 'Use this option to sync external resources into a new folder without affecting the rest of your instance. You can repeat this process for up to 10 folders.', -}; - -function getDisabledReason(action: ModeOption, resourceCount: number, folderConnected?: boolean) { - // Disable pull instance if there are existing dashboards or folders - if (action.target === 'instance' && action.operation === 'pull' && resourceCount > 0) { - return 'Cannot pull to instance when you have existing resources. Please migrate your existing resources first.'; - } - - if (!folderConnected) { - return undefined; - } - - if (action.operation === 'migrate') { - return 'Cannot migrate when a folder is already mounted.'; - } - - if (action.target === 'instance') { - return 'Instance-wide connection is disabled because folders are connected to repositories.'; - } - - return undefined; -} - -export function getState( - repoName: string, - settings?: RepositoryViewList, - files?: GetRepositoryFilesApiResponse, - stats?: GetResourceStatsApiResponse -): SystemState { +/** + * Filters available mode options based on system state + */ +function filterModeOptions(modeOptions: ModeOption[], repoName: string, settings?: RepositoryViewList): ModeOption[] { const folderConnected = settings?.items?.some((item) => item.target === 'folder' && item.name !== repoName); + return modeOptions.filter((option) => { + if (settings?.legacyStorage) { + return option.target === 'instance'; + } + + if (option.target === 'folder') { + return true; + } + + if (option.target === 'instance') { + return !folderConnected; + } + + return false; + }); +} + +/** + * Hook that provides filtered mode options + * This needs to be a hook, so we can add translations + */ +export function useModeOptions(repoName: string, settings?: RepositoryViewList) { + return useMemo(() => { + const modeOptions: ModeOption[] = [ + { + target: 'instance', + label: t('provisioning.mode-options.instance.label', 'Sync all resources with external storage'), + description: t( + 'provisioning.mode-options.instance.description', + 'Resources will be synced with external storage and provisioned into this instance. Existing Grafana resources will be migrated and merged if needed. After setup, all new resources and changes will be saved to external storage and automatically provisioned back into the instance.' + ), + subtitle: t( + 'provisioning.mode-options.instance.subtitle', + 'Use this option if you want to sync and manage your entire Grafana instance through external storage.' + ), + }, + { + target: 'folder', + label: t('provisioning.mode-options.folder.label', 'Sync external storage to a new Grafana folder'), + description: t( + 'provisioning.mode-options.folder.description', + 'After setup, a new Grafana folder will be created and synced with external storage. If any resources are present in external storage, they will be provisioned to this new folder. All new resources created in this folder will be stored and versioned in external storage.' + ), + subtitle: t( + 'provisioning.mode-options.folder.subtitle', + 'Use this option to sync external resources into a new folder without affecting the rest of your instance. You can repeat this process for up to 10 folders.' + ), + }, + ]; + + return filterModeOptions(modeOptions, repoName, settings); + }, [repoName, settings]); +} + +export function getResourceStats(files?: GetRepositoryFilesApiResponse, stats?: GetResourceStatsApiResponse) { const fileCount = files?.items?.reduce((count, file) => { const path = file.path ?? ''; @@ -62,9 +78,10 @@ export function getState( let counts: string[] = []; let resourceCount = 0; + stats?.instance?.forEach((stat) => { switch (stat.group) { - case 'folders': // fallthrough + case 'folders': case 'folder.grafana.app': resourceCount += stat.count; counts.push(`${stat.count} ${stat.count > 1 ? 'folders' : 'folder'}`); @@ -76,32 +93,9 @@ export function getState( } }); - const state: SystemState = { + return { + fileCount, resourceCount, resourceCountString: counts.join(',\n'), - fileCount, - actions: [], - disabled: [], - folderConnected, }; - - // Legacy storage can only migrate - if (settings?.legacyStorage) { - const disabledReason = 'Instance must be migrated first'; - state.actions = [migrateInstance]; - state.disabled = [{ ...pullFolder, disabledReason }]; - return state; - } - - const actionsToEvaluate = [migrateInstance, pullFolder]; - actionsToEvaluate.forEach((action) => { - const reason = getDisabledReason(action, resourceCount, folderConnected); - if (reason) { - state.disabled.push({ ...action, disabledReason: reason }); - } else { - state.actions.push(action); - } - }); - - return state; } diff --git a/public/app/features/provisioning/Wizard/types.ts b/public/app/features/provisioning/Wizard/types.ts index 39abf940ac5..67797a7c85a 100644 --- a/public/app/features/provisioning/Wizard/types.ts +++ b/public/app/features/provisioning/Wizard/types.ts @@ -18,23 +18,20 @@ export interface WizardFormData { } export type Target = SyncOptions['target']; -export type Operation = 'pull' | 'migrate'; export interface ModeOption { target: Target; - operation: Operation; label: string; description: string; - disabledReason?: string; subtitle: string; } export interface SystemState { resourceCount: number; resourceCountString: string; - fileCount: number; actions: ModeOption[]; - disabled: ModeOption[]; - folderConnected?: boolean; } + +export type StepStatus = 'idle' | 'running' | 'error' | 'success'; +export type StepStatusInfo = { status: StepStatus } | { status: 'error'; error: string }; diff --git a/public/app/features/provisioning/hooks/useStepStatus.ts b/public/app/features/provisioning/hooks/useStepStatus.ts deleted file mode 100644 index eb8e37451f2..00000000000 --- a/public/app/features/provisioning/hooks/useStepStatus.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useCallback } from 'react'; - -export type StepStatus = 'idle' | 'running' | 'error' | 'success'; - -export interface StepStatusProps { - onStepUpdate: (status: StepStatus, error?: string) => void; -} - -export interface StepStatusActions { - setRunning: () => void; - setError: (error: string) => void; - setSuccess: () => void; -} - -export function useStepStatus({ onStepUpdate }: StepStatusProps): StepStatusActions { - const setRunning = useCallback(() => onStepUpdate('running'), [onStepUpdate]); - const setError = useCallback((error: string) => onStepUpdate('error', error), [onStepUpdate]); - const setSuccess = useCallback(() => onStepUpdate('success'), [onStepUpdate]); - - return { - setRunning, - setError, - setSuccess, - }; -} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index b40b2e2a119..55989b22263 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -6424,7 +6424,6 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "Dashboards will be unavailable while running this process", "description-clear-repository-connection": "Add a clear name for this repository connection", "empty": "Empty", "error-field-required": "This field is required.", @@ -6432,15 +6431,9 @@ "files-count_one": "{{count}} files", "files-count_other": "{{count}} files", "grafana": "Grafana instance", - "include-history": "Include history", "label-display-name": "Display name", - "label-migrate-options": "Migrate options", "placeholder-my-repository-connection": "My repository connection", - "resources-will-be-added": "The {{count}} resources in grafana will be added to the repository. Grafana will then include both the current resources and anything from the repository when done.", - "text-loading-resource-information": "Loading resource information...", - "title-files-exist-in-the-target": "Files exist in the target", - "title-note": "Note", - "tooltip-include-history": "Include complete dashboard version history" + "text-loading-resource-information": "Loading resource information..." }, "check-repository": { "check": "Check" @@ -6480,7 +6473,6 @@ "placeholder-interval-seconds": "60", "placeholder-local-path": "/path/to/repo", "placeholder-my-config": "My config", - "placeholder-path": "grafana/", "placeholder-select-repository-type": "Select repository type" }, "config-form-github-collapse": { @@ -6516,7 +6508,6 @@ "label-path": "Path to subdirectory in repository", "label-repository-url": "GitHub repository URL", "placeholder-branch": "main", - "placeholder-github-path": "grafana/", "placeholder-github-token": "github_pat_yourTokenHere1234567890abcdEFGHijklMNOP", "placeholder-github-url": "https://github.com/username/repo", "placeholder-local-path": "/path/to/repo" @@ -6644,6 +6635,18 @@ }, "summary": "Summary" }, + "mode-options": { + "folder": { + "description": "After setup, a new Grafana folder will be created and synced with external storage. If any resources are present in external storage, they will be provisioned to this new folder. All new resources created in this folder will be stored and versioned in external storage.", + "label": "Sync external storage to a new Grafana folder", + "subtitle": "Use this option to sync external resources into a new folder without affecting the rest of your instance. You can repeat this process for up to 10 folders." + }, + "instance": { + "description": "Resources will be synced with external storage and provisioned into this instance. Existing Grafana resources will be migrated and merged if needed. After setup, all new resources and changes will be saved to external storage and automatically provisioned back into the instance.", + "label": "Sync all resources with external storage", + "subtitle": "Use this option if you want to sync and manage your entire Grafana instance through external storage." + } + }, "recent-jobs": { "active-jobs": "active jobs", "column-action": "Action", @@ -6752,7 +6755,6 @@ "tooltip-unhealthy-repository": "Unable to pull an unhealthy repository" }, "synchronize-step": { - "error-job-failed": "Job failed", "error-no-job-id": "Failed to start job", "error-no-repository-name": "No repository name provided", "error-starting-job": "Error starting job", From 05338838b04461c8df5ce89938173e9c0f0ca89e Mon Sep 17 00:00:00 2001 From: Sven Grossmann Date: Thu, 10 Apr 2025 22:04:12 +0200 Subject: [PATCH 12/37] Sidecar: Remove experimental Sidecar implementation (#103786) * Sidecar: remove `appSidecar` feature toggle * Sidecar: Remove sidecar implementation * lint --- .../feature-toggles/index.md | 1 - .../src/types/featureToggles.gen.ts | 4 - .../services/SidecarContext_EXPERIMENTAL.ts | 49 --- .../SidecarService_EXPERIMENTAL.test.ts | 152 -------- .../services/SidecarService_EXPERIMENTAL.ts | 342 ------------------ .../grafana-runtime/src/services/index.ts | 2 - pkg/services/featuremgmt/registry.go | 6 - pkg/services/featuremgmt/toggles-gitlog.csv | 1 - pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 - pkg/services/featuremgmt/toggles_gen.json | 12 - public/app/AppWrapper.tsx | 40 +- public/app/routes/RoutesWrapper.tsx | 127 +------ public/locales/en-US/grafana.json | 3 - 14 files changed, 16 insertions(+), 728 deletions(-) delete mode 100644 packages/grafana-runtime/src/services/SidecarContext_EXPERIMENTAL.ts delete mode 100644 packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.test.ts delete mode 100644 packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.ts diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 2b27329a8af..51df92d1ac0 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -190,7 +190,6 @@ Experimental features might be changed or removed without prior notice. | `exploreLogsAggregatedMetrics` | Used in Logs Drilldown to query by aggregated metrics | | `exploreLogsLimitedTimeRange` | Used in Logs Drilldown to limit the time range | | `homeSetupGuide` | Used in Home for users who want to return to the onboarding flow or quickly find popular config pages | -| `appSidecar` | Enable the app sidecar feature that allows rendering 2 apps at the same time | | `rolePickerDrawer` | Enables the new role picker drawer design | | `unifiedStorageBigObjectsSupport` | Enables to save big objects in blob storage | | `timeRangeProvider` | Enables time pickers sync | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 70bdc1a2911..154c070f860 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -725,10 +725,6 @@ export interface FeatureToggles { */ appPlatformGrpcClientAuth?: boolean; /** - * Enable the app sidecar feature that allows rendering 2 apps at the same time - */ - appSidecar?: boolean; - /** * Enable the groupsync extension for managing Group Attribute Sync feature */ groupAttributeSync?: boolean; diff --git a/packages/grafana-runtime/src/services/SidecarContext_EXPERIMENTAL.ts b/packages/grafana-runtime/src/services/SidecarContext_EXPERIMENTAL.ts deleted file mode 100644 index 05f4579d0ba..00000000000 --- a/packages/grafana-runtime/src/services/SidecarContext_EXPERIMENTAL.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { createContext, useContext } from 'react'; -import { useObservable } from 'react-use'; - -import { SidecarService_EXPERIMENTAL, sidecarServiceSingleton_EXPERIMENTAL } from './SidecarService_EXPERIMENTAL'; - -export const SidecarContext_EXPERIMENTAL = createContext( - sidecarServiceSingleton_EXPERIMENTAL -); - -/** - * This is the main way to interact with the sidecar service inside a react context. It provides a wrapper around the - * service props so that even though they are observables we just pass actual values to the components. - * - * @experimental - */ -export function useSidecar_EXPERIMENTAL() { - // As the sidecar service functionality is behind feature flag this does not need to be for now - const service = useContext(SidecarContext_EXPERIMENTAL); - - if (!service) { - throw new Error('No SidecarContext found'); - } - - const initialContext = useObservable(service.initialContextObservable, service.initialContext); - const activePluginId = useObservable(service.activePluginIdObservable, service.activePluginId); - const locationService = service.getLocationService(); - - return { - activePluginId, - initialContext, - locationService, - // TODO: currently this allows anybody to open any app, in the future we should probably scope this to the - // current app but that means we will need to incorporate this better into the plugin platform APIs which - // we will do once the functionality is reasonably stable - openApp: (pluginId: string, context?: unknown) => { - return service.openApp(pluginId, context); - }, - openAppV2: (pluginId: string, path?: string) => { - return service.openAppV2(pluginId, path); - }, - openAppV3: (options: { pluginId: string; path?: string; follow?: boolean }) => { - return service.openAppV3(options); - }, - closeApp: () => service.closeApp(), - isAppOpened: (pluginId: string) => { - return service.isAppOpened(pluginId); - }, - }; -} diff --git a/packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.test.ts b/packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.test.ts deleted file mode 100644 index 6e6c7a0d1a8..00000000000 --- a/packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.test.ts +++ /dev/null @@ -1,152 +0,0 @@ -import * as H from 'history'; - -import { config } from '../config'; - -import { HistoryWrapper } from './LocationService'; -import { SidecarService_EXPERIMENTAL } from './SidecarService_EXPERIMENTAL'; - -function setup() { - const mainLocationService = new HistoryWrapper(H.createMemoryHistory({ initialEntries: ['/explore'] })); - const sidecarService = new SidecarService_EXPERIMENTAL(mainLocationService); - return { - mainLocationService, - sidecarService, - }; -} - -describe('SidecarService_EXPERIMENTAL', () => { - beforeAll(() => { - config.featureToggles.appSidecar = true; - }); - - afterAll(() => { - config.featureToggles.appSidecar = false; - }); - - it('has the correct state after opening and closing an app', () => { - const { sidecarService } = setup(); - sidecarService.openApp('pluginId', { filter: 'test' }); - - expect(sidecarService.activePluginId).toBe('pluginId'); - expect(sidecarService.initialContext).toMatchObject({ filter: 'test' }); - expect(sidecarService.getLocationService().getLocation().pathname).toBe('/a/pluginId'); - - sidecarService.closeApp(); - expect(sidecarService.activePluginId).toBe(undefined); - expect(sidecarService.initialContext).toBe(undefined); - expect(sidecarService.getLocationService().getLocation().pathname).toBe('/'); - }); - - it('has the correct state after opening and closing an app v2', () => { - const { sidecarService } = setup(); - sidecarService.openAppV2('pluginId', '/test'); - - expect(sidecarService.activePluginId).toBe('pluginId'); - expect(sidecarService.getLocationService().getLocation().pathname).toBe('/a/pluginId/test'); - - sidecarService.closeApp(); - expect(sidecarService.activePluginId).toBe(undefined); - expect(sidecarService.initialContext).toBe(undefined); - expect(sidecarService.getLocationService().getLocation().pathname).toBe('/'); - }); - - it('has the correct state after opening and closing an app v3', () => { - const { sidecarService } = setup(); - sidecarService.openAppV3({ pluginId: 'pluginId', path: '/test' }); - - expect(sidecarService.activePluginId).toBe('pluginId'); - expect(sidecarService.getLocationService().getLocation().pathname).toBe('/a/pluginId/test'); - - sidecarService.closeApp(); - expect(sidecarService.activePluginId).toBe(undefined); - expect(sidecarService.initialContext).toBe(undefined); - expect(sidecarService.getLocationService().getLocation().pathname).toBe('/'); - }); - - it('reports correct opened state', () => { - const { sidecarService } = setup(); - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - - sidecarService.openApp('pluginId'); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - - sidecarService.closeApp(); - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('reports correct opened state v2', () => { - const { sidecarService } = setup(); - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - - sidecarService.openAppV2('pluginId'); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - - sidecarService.closeApp(); - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('reports correct opened state v3', () => { - const { sidecarService } = setup(); - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - - sidecarService.openAppV3({ pluginId: 'pluginId' }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - - sidecarService.closeApp(); - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('autocloses on not allowed routes', () => { - const { sidecarService, mainLocationService } = setup(); - sidecarService.openAppV3({ pluginId: 'pluginId' }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - mainLocationService.push('/config'); - - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('autocloses on when changing route', () => { - const { sidecarService, mainLocationService } = setup(); - sidecarService.openAppV3({ pluginId: 'pluginId' }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - mainLocationService.push('/a/other-app'); - - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('does not autocloses when set to follow', () => { - const { sidecarService, mainLocationService } = setup(); - sidecarService.openAppV3({ pluginId: 'pluginId', follow: true }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - mainLocationService.push('/a/other-app'); - - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - }); - - it('autocloses on not allowed routes when set to follow', () => { - const { sidecarService, mainLocationService } = setup(); - sidecarService.openAppV3({ pluginId: 'pluginId', follow: true }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - mainLocationService.push('/config'); - - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('autocloses on not allowed routes when set to follow', () => { - const { sidecarService, mainLocationService } = setup(); - sidecarService.openAppV3({ pluginId: 'pluginId', follow: true }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - mainLocationService.push('/config'); - - expect(sidecarService.isAppOpened('pluginId')).toBe(false); - }); - - it('opens sidecar even if starting route is not allowed and then it changes', () => { - const mainLocationService = new HistoryWrapper(H.createMemoryHistory({ initialEntries: ['/login'] })); - const sidecarService = new SidecarService_EXPERIMENTAL(mainLocationService); - mainLocationService.push('/explore'); - - sidecarService.openAppV3({ pluginId: 'pluginId', follow: true }); - expect(sidecarService.isAppOpened('pluginId')).toBe(true); - }); -}); diff --git a/packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.ts b/packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.ts deleted file mode 100644 index 8e00fbb42e0..00000000000 --- a/packages/grafana-runtime/src/services/SidecarService_EXPERIMENTAL.ts +++ /dev/null @@ -1,342 +0,0 @@ -import * as H from 'history'; -import { pick } from 'lodash'; -import { BehaviorSubject, map, Observable } from 'rxjs'; - -import { reportInteraction } from '../analytics/utils'; -import { config } from '../config'; - -import { HistoryWrapper, locationService as mainLocationService, LocationService } from './LocationService'; - -// Only allow sidecar to be opened on these routes. It does not seem to make sense to keep the sidecar opened on -// config/admin pages for example. -// At this moment let's be restrictive about where the sidecar can show and add more routes if there is a need. -const ALLOW_ROUTES = [ - /(^\/d\/)/, // dashboards - /^\/explore/, // explore + explore metrics - /^\/a\/[^\/]+/, // app plugins - /^\/alerting/, -]; - -/** - * This is a service that handles state and operation of a sidecar feature (sideview to render a second app in grafana). - * At this moment this is highly experimental and if used should be understood to break easily with newer versions. - * None of this functionality works without a feature toggle `appSidecar` being enabled. - * - * Right now this being in a single service is more of a practical tradeoff for easier isolation in the future these - * APIs may be integrated into other services or features like app extensions, plugin system etc. - * - * @experimental - */ -export class SidecarService_EXPERIMENTAL { - private _initialContext: BehaviorSubject; - - private sidecarLocationService: LocationService; - private mainLocationService: LocationService; - - // If true we don't close the sidecar when user navigates to another app or part of Grafana from where the sidecar - // was opened. - private follow = false; - - // Keep track of where the sidecar was originally opened for autoclose behaviour. - private mainLocationWhenOpened: string | undefined; - - private mainOnAllowedRoute = false; - - constructor(mainLocationService: LocationService) { - this._initialContext = new BehaviorSubject(undefined); - this.mainLocationService = mainLocationService; - this.sidecarLocationService = new HistoryWrapper( - createLocationStorageHistory({ storageKey: 'grafana.sidecar.history' }) - ); - this.handleMainLocationChanges(); - } - - private assertFeatureEnabled() { - if (!config.featureToggles.appSidecar) { - console.warn('The `appSidecar` feature toggle is not enabled, doing nothing.'); - return false; - } - - return true; - } - - private updateMainLocationWhenOpened() { - const pathname = this.mainLocationService.getLocation().pathname; - for (const route of ALLOW_ROUTES) { - const match = pathname.match(route)?.[0]; - if (match) { - this.mainLocationWhenOpened = match; - return; - } - } - } - - /** - * Every time the main location changes we check if we should keep the sidecar open or close it based on list - * of allowed routes and also based on the follow flag when opening the app. - */ - private handleMainLocationChanges() { - this.mainOnAllowedRoute = ALLOW_ROUTES.some((prefix) => - this.mainLocationService.getLocation().pathname.match(prefix) - ); - - this.mainLocationService.getLocationObservable().subscribe((location) => { - this.mainOnAllowedRoute = ALLOW_ROUTES.some((prefix) => location.pathname.match(prefix)); - - if (!this.activePluginId) { - return; - } - - if (!this.mainOnAllowedRoute) { - this.closeApp(); - return; - } - - // We check if we moved to some other app or part of grafana from where we opened the sidecar. - const isTheSameLocation = Boolean( - this.mainLocationWhenOpened && location.pathname.startsWith(this.mainLocationWhenOpened) - ); - - if (!(isTheSameLocation || this.follow)) { - this.closeApp(); - } - }); - } - - /** - * Get current app id of the app in sidecar. This is most probably provisional. In the future - * this should be driven by URL addressing so that routing for the apps don't change. Useful just internally - * to decide which app to render. - * - * @experimental - */ - get activePluginIdObservable() { - return this.sidecarLocationService.getLocationObservable().pipe( - map((val) => { - return getPluginIdFromUrl(val?.pathname || ''); - }) - ); - } - - /** - * Get initial context which is whatever data was passed when calling the 'openApp' function. This is meant as - * a way for the app to initialize it's state based on some context that is passed to it from the primary app. - * - * @experimental - */ - get initialContextObservable() { - return this._initialContext.asObservable(); - } - - // Get the current value of the subject, this is needed if we want the value immediately. For example if used in - // hook in react with useObservable first render would return undefined even if the behaviourSubject has some - // value which will be emitted in the next tick and thus next rerender. - get initialContext() { - return this._initialContext.getValue(); - } - - /** - * @experimental - */ - get activePluginId() { - return getPluginIdFromUrl(this.sidecarLocationService.getLocation().pathname); - } - - getLocationService() { - return this.sidecarLocationService; - } - - /** - * Opens an app in a sidecar. You can also pass some context object that will be then available to the app. - * @deprecated - * @experimental - */ - openApp(pluginId: string, context?: unknown) { - if (!(this.assertFeatureEnabled() && this.mainOnAllowedRoute)) { - return; - } - this._initialContext.next(context); - this.openAppV3({ pluginId, follow: false }); - } - - /** - * Opens an app in a sidecar. You can also relative path inside the app to open. - * @deprecated - * @experimental - */ - openAppV2(pluginId: string, path?: string) { - this.openAppV3({ pluginId, path, follow: false }); - } - - /** - * Opens an app in a sidecar. You can also relative path inside the app to open. - * @param options.pluginId Plugin ID of the app to open - * @param options.path Relative path inside the app to open - * @param options.follow If true, the sidecar will stay open even if the main location change to another app or - * Grafana section - * - * @experimental - */ - openAppV3(options: { pluginId: string; path?: string; follow?: boolean }) { - if (!(this.assertFeatureEnabled() && this.mainOnAllowedRoute)) { - return; - } - - this.follow = options.follow || false; - - this.updateMainLocationWhenOpened(); - this.sidecarLocationService.push({ pathname: `/a/${options.pluginId}${options.path || ''}` }); - reportInteraction('sidecar_service_open_app', { pluginId: options.pluginId, follow: options.follow }); - } - - /** - * @experimental - */ - closeApp() { - if (!this.assertFeatureEnabled()) { - return; - } - - this.follow = false; - this.mainLocationWhenOpened = undefined; - this._initialContext.next(undefined); - this.sidecarLocationService.replace({ pathname: '/' }); - - reportInteraction('sidecar_service_close_app'); - } - - /** - * This is mainly useful inside an app extensions which are executed outside the main app context but can work - * differently depending on whether their app is currently rendered or not. - * - * This is also true only in case a sidecar is opened. In other cases, just to check if a single app is opened - * probably does not make sense. - * - * This means these are the states and the result of this function: - * Single app is opened: false (may seem strange from considering the function name, but the main point of - * this is to recognize when the app needs to do specific alteration in context of running next to second app) - * 2 apps are opened and pluginId is the one in the main window: true - * 2 apps are opened and pluginId is the one in the sidecar window: true - * 2 apps are opened and pluginId is not one of those: false - * - * @experimental - */ - isAppOpened(pluginId: string) { - if (!this.assertFeatureEnabled()) { - return false; - } - - const result = !!(this.activePluginId && (this.activePluginId === pluginId || getMainAppPluginId() === pluginId)); - reportInteraction('sidecar_service_is_app_opened', { pluginId, isOpened: result }); - return result; - } -} - -const pluginIdUrlRegex = /a\/([^\/]+)/; -function getPluginIdFromUrl(url: string) { - return url.match(pluginIdUrlRegex)?.[1]; -} - -// The app plugin that is "open" in the main Grafana view -function getMainAppPluginId() { - // TODO: not great but we have to get a handle on the other locationService used for the main view and easiest way - // right now is through this global singleton - const { pathname } = mainLocationService.getLocation(); - - // A naive way to sort of simulate core features being an app and having an appID - let mainApp = getPluginIdFromUrl(pathname); - if (!mainApp && pathname.match(/\/explore/)) { - mainApp = 'explore'; - } - - if (!mainApp && pathname.match(/\/d\//)) { - mainApp = 'dashboards'; - } - - return mainApp || 'unknown'; -} - -type LocalStorageHistoryOptions = { - storageKey: string; -}; - -interface LocationStorageHistory extends H.MemoryHistory { - getLocationObservable(): Observable; -} - -/** - * Simple wrapper over the memory history that persists the location in the localStorage. - * - * @param options - */ -function createLocationStorageHistory(options: LocalStorageHistoryOptions): LocationStorageHistory { - const storedLocation = localStorage.getItem(options.storageKey); - const initialEntry = storedLocation ? JSON.parse(storedLocation) : '/'; - const locationSubject = new BehaviorSubject(initialEntry); - const memoryHistory = H.createMemoryHistory({ initialEntries: [initialEntry] }); - - let currentLocation = memoryHistory.location; - - function maybeUpdateLocation() { - if (memoryHistory.location !== currentLocation) { - localStorage.setItem( - options.storageKey, - JSON.stringify(pick(memoryHistory.location, 'pathname', 'search', 'hash')) - ); - currentLocation = memoryHistory.location; - locationSubject.next(memoryHistory.location); - } - } - - // This creates a sort of proxy over the memory location just to add the localStorage persistence and the location - // observer. We could achieve the same effect by a listener but that would create a memory leak as there would be no - // reasonable way to unsubcribe the listener later on. - // Another issue is that react router for some reason does not care about proper `this` binding and just calls these - // as normal functions. So if this were to be a class we would still need to bind each of these methods to the - // instance so at that moment this just seems easier. - return { - ...memoryHistory, - // Getter aren't destructured as getter but as values, so they have to be still here even though we are not - // modifying them. - get index() { - return memoryHistory.index; - }, - get entries() { - return memoryHistory.entries; - }, - get length() { - return memoryHistory.length; - }, - get action() { - return memoryHistory.action; - }, - get location() { - return memoryHistory.location; - }, - push(location: H.Path | H.LocationDescriptor, state?: H.LocationState) { - memoryHistory.push(location, state); - maybeUpdateLocation(); - }, - replace(location: H.Path | H.LocationDescriptor, state?: H.LocationState) { - memoryHistory.replace(location, state); - maybeUpdateLocation(); - }, - go(n: number) { - memoryHistory.go(n); - maybeUpdateLocation(); - }, - goBack() { - memoryHistory.goBack(); - maybeUpdateLocation(); - }, - goForward() { - memoryHistory.goForward(); - maybeUpdateLocation(); - }, - getLocationObservable() { - return locationSubject.asObservable(); - }, - }; -} - -export const sidecarServiceSingleton_EXPERIMENTAL = new SidecarService_EXPERIMENTAL(mainLocationService); diff --git a/packages/grafana-runtime/src/services/index.ts b/packages/grafana-runtime/src/services/index.ts index f1bf983ba1b..e9d2eeac852 100644 --- a/packages/grafana-runtime/src/services/index.ts +++ b/packages/grafana-runtime/src/services/index.ts @@ -6,8 +6,6 @@ export * from './templateSrv'; export * from './live'; export * from './LocationService'; export * from './appEvents'; -export * from './SidecarService_EXPERIMENTAL'; -export * from './SidecarContext_EXPERIMENTAL'; export { setPluginComponentHook, diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index ccd67927245..568a33f60ac 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1244,12 +1244,6 @@ var ( HideFromDocs: true, HideFromAdminPage: true, }, - { - Name: "appSidecar", - Description: "Enable the app sidecar feature that allows rendering 2 apps at the same time", - Stage: FeatureStageExperimental, - Owner: grafanaFrontendPlatformSquad, - }, { Name: "groupAttributeSync", Description: "Enable the groupsync extension for managing Group Attribute Sync feature", diff --git a/pkg/services/featuremgmt/toggles-gitlog.csv b/pkg/services/featuremgmt/toggles-gitlog.csv index 2d7324de32e..4e787933b8b 100644 --- a/pkg/services/featuremgmt/toggles-gitlog.csv +++ b/pkg/services/featuremgmt/toggles-gitlog.csv @@ -355,7 +355,6 @@ exploreLogsLimitedTimeRange,2024-08-29T13:55:59Z,,15a4ff992bd925f5714bc4e73fa976 exploreLogsShardSplitting,2024-08-29T13:55:59Z,,15a4ff992bd925f5714bc4e73fa9766a995b5711,Sven Grossmann newFiltersUI,2024-08-30T12:48:13Z,,00ae49a61adff11e5fbac3341941b35185d9e8d9,Sergej-Vlasov appPlatformAccessTokens,2024-09-05T16:18:44Z,2024-10-14T10:47:18Z,d5ebaa0ef92edecfb9511dc7c8080be25f8cc7e7,Claudiu Dragalina-Paraipan -appSidecar,2024-09-09T12:45:05Z,,5e2ac24890906e5070323d87730dd78a4f885963,Andrej Ocenas vizActions,2024-09-09T14:11:55Z,,af48d3db1eb2d8681843f5997e50fea5e5ea3096,Adela Almasan groupAttributeSync,2024-09-09T15:29:43Z,,6ded6a8872204a818b3795dc733cc5fe5db066a0,Aaron Godin alertingFilterV2,2024-09-11T11:29:26Z,,90ee52e8d9c14237f8a57b622c0def7512e657cd,Gilles De Mey diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index d7af7ac12bc..432f331d2f2 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -161,7 +161,6 @@ exploreLogsAggregatedMetrics,experimental,@grafana/observability-logs,false,fals exploreLogsLimitedTimeRange,experimental,@grafana/observability-logs,false,false,true homeSetupGuide,experimental,@grafana/growth-and-onboarding,false,false,true appPlatformGrpcClientAuth,experimental,@grafana/identity-access-team,false,false,false -appSidecar,experimental,@grafana/grafana-frontend-platform,false,false,false groupAttributeSync,privatePreview,@grafana/identity-access-team,false,false,false alertingQueryAndExpressionsStepMode,GA,@grafana/alerting-squad,false,false,true improvedExternalSessionHandling,preview,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 5fa99bec225..4b23d0076e6 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -655,10 +655,6 @@ const ( // Enables the gRPC client to authenticate with the App Platform by using ID & access tokens FlagAppPlatformGrpcClientAuth = "appPlatformGrpcClientAuth" - // FlagAppSidecar - // Enable the app sidecar feature that allows rendering 2 apps at the same time - FlagAppSidecar = "appSidecar" - // FlagGroupAttributeSync // Enable the groupsync extension for managing Group Attribute Sync feature FlagGroupAttributeSync = "groupAttributeSync" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 65737a0b3c0..542851adf3a 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -479,18 +479,6 @@ "hideFromDocs": true } }, - { - "metadata": { - "name": "appSidecar", - "resourceVersion": "1743693517832", - "creationTimestamp": "2024-09-09T12:45:05Z" - }, - "spec": { - "description": "Enable the app sidecar feature that allows rendering 2 apps at the same time", - "stage": "experimental", - "codeowner": "@grafana/grafana-frontend-platform" - } - }, { "metadata": { "name": "assetSriChecks", diff --git a/public/app/AppWrapper.tsx b/public/app/AppWrapper.tsx index fca1a0004e5..e3a8e59b257 100644 --- a/public/app/AppWrapper.tsx +++ b/public/app/AppWrapper.tsx @@ -4,13 +4,7 @@ import CacheProvider from 'react-inlinesvg/provider'; import { Provider } from 'react-redux'; import { Route, Routes } from 'react-router-dom-v5-compat'; -import { - config, - navigationLogger, - reportInteraction, - SidecarContext_EXPERIMENTAL, - sidecarServiceSingleton_EXPERIMENTAL, -} from '@grafana/runtime'; +import { config, navigationLogger, reportInteraction } from '@grafana/runtime'; import { ErrorBoundaryAlert, PortalContainer, TimeRangeProvider } from '@grafana/ui'; import { getAppRoutes } from 'app/routes/routes'; import { store } from 'app/store/store'; @@ -26,7 +20,7 @@ import { LiveConnectionWarning } from './features/live/LiveConnectionWarning'; import { ExtensionRegistriesProvider } from './features/plugins/extensions/ExtensionRegistriesContext'; import { pluginExtensionRegistries } from './features/plugins/extensions/registry/setup'; import { ScopesContextProvider } from './features/scopes/ScopesContextProvider'; -import { ExperimentalSplitPaneRouterWrapper, RouterWrapper } from './routes/RoutesWrapper'; +import { RouterWrapper } from './routes/RoutesWrapper'; interface AppWrapperProps { app: GrafanaApp; @@ -125,24 +119,18 @@ export class AppWrapper extends Component { options={{ enableHistory: true, callbacks: { onSelectAction: commandPaletteActionSelected } }} > - - - - - -
- {config.featureToggles.appSidecar ? ( - - ) : ( - - )} - - -
-
-
-
-
+ + + + +
+ + + +
+
+
+
diff --git a/public/app/routes/RoutesWrapper.tsx b/public/app/routes/RoutesWrapper.tsx index f0b0502519f..21a74256d74 100644 --- a/public/app/routes/RoutesWrapper.tsx +++ b/public/app/routes/RoutesWrapper.tsx @@ -1,22 +1,13 @@ -import { css } from '@emotion/css'; import { ComponentType, ReactNode } from 'react'; -// eslint-disable-next-line no-restricted-imports import { Router } from 'react-router-dom'; import { CompatRouter } from 'react-router-dom-v5-compat'; -import { GrafanaTheme2 } from '@grafana/data'; -import { - locationService, - LocationServiceProvider, - useChromeHeaderHeight, - useSidecar_EXPERIMENTAL, -} from '@grafana/runtime'; -import { GlobalStyles, IconButton, ModalRoot, Stack, useSplitter, useStyles2 } from '@grafana/ui'; +import { locationService, LocationServiceProvider } from '@grafana/runtime'; +import { ModalRoot, Stack } from '@grafana/ui'; import { AppChrome } from '../core/components/AppChrome/AppChrome'; import { AppNotificationList } from '../core/components/AppNotifications/AppNotificationList'; import { ModalsContextProvider } from '../core/context/ModalsContextProvider'; -import { t } from '../core/internationalization'; import { QueriesDrawerContextProvider } from '../features/explore/QueriesDrawer/QueriesDrawerContext'; function ExtraProviders(props: { children: ReactNode; providers: Array> }) { @@ -60,117 +51,3 @@ export function RouterWrapper(props: RouterWrapperProps) { ); } - -/** - * Renders both the main app tree and a secondary sidecar app tree to show 2 apps at the same time in a resizable split - * view. - * @param props - * @constructor - */ -export function ExperimentalSplitPaneRouterWrapper(props: RouterWrapperProps) { - const { closeApp, locationService, activePluginId } = useSidecar_EXPERIMENTAL(); - - let { containerProps, primaryProps, secondaryProps, splitterProps } = useSplitter({ - direction: 'row', - initialSize: 0.6, - dragPosition: 'end', - handleSize: 'sm', - }); - - // The style changes allow the resizing to be more flexible and not constrained by the content dimensions. In the - // future this could be a switch in the useSplitter but for now it's here until this feature is more final. - function alterStyles(props: T): T { - return { - ...props, - style: { ...props.style, overflow: 'auto', minWidth: 'unset', minHeight: 'unset' }, - }; - } - primaryProps = alterStyles(primaryProps); - secondaryProps = alterStyles(secondaryProps); - - const headerHeight = useChromeHeaderHeight(); - const styles = useStyles2(getStyles, headerHeight); - - // Right now we consider only app plugin to be opened here but in the future we might want to just open any kind - // of url and so this should check whether there is a location in the sidecar locationService. - const sidecarOpen = Boolean(activePluginId); - - return ( - // Why do we need these 2 wrappers here? We want for one app case to render very similar as if there was no split - // wrapper at all but the split wrapper needs to have these wrappers to attach its container props to. At the same - // time we don't want to rerender the main app when going from 2 apps render to single app render which would happen - // if we removed the wrappers. So the solution is to keep those 2 divs but make them no actually do anything in - // case we are rendering a single app. -
-
- -
- {/* Sidecar */} - {sidecarOpen && ( - <> -
-
- - - - -
-
- closeApp()} - /> -
-
- {/*We don't render anything other than app plugin but we want to keep the same routing layout so*/} - {/*there are is no difference with matching relative routes between main and sidecar view.*/} - {props.routes} -
-
-
-
-
-
- - )} -
- ); -} - -const getStyles = (theme: GrafanaTheme2, headerHeight: number | undefined) => { - return { - secondAppChrome: css({ - label: 'secondAppChrome', - display: 'flex', - height: '100%', - width: '100%', - paddingTop: headerHeight || 0, - flexGrow: 1, - flexDirection: 'column', - }), - - secondAppToolbar: css({ - label: 'secondAppToolbar', - display: 'flex', - justifyContent: 'flex-end', - }), - - secondAppWrapper: css({ - label: 'secondAppWrapper', - overflow: 'auto', - flex: '1', - }), - - // This is basically the same as grafana-app class. This means the 2 additional wrapper divs that are in between - // grafana-app div and the main app component don't actually change anything in the layout. - dummyWrapper: css({ - label: 'dummyWrapper', - display: 'flex', - height: '100vh', - flexDirection: 'column', - }), - }; -}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 55989b22263..c2ecec89b32 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -7135,9 +7135,6 @@ "reload-button": "Reload", "title": "Unable to find application file" }, - "routes-wrapper": { - "close-aria-label": "Close" - }, "sandbox": { "test-stuff-page": { "application-notifications-toasts-testing": "Application notifications (toasts) testing", From 3a71a48a8805f5136d0db7be29a6b1b4d85bb27f Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Thu, 10 Apr 2025 21:11:00 +0100 Subject: [PATCH 13/37] AzureMonitor: Improve selection of Basic Logs tables in the query builder (#103820) * Add function for retrieving logs table plan - Add URL builder method - Add types * Add auto-switching for basic logs tables * Set dashboardTime property * Ensure useEffect doesn't run on every query change * Fix basicLogs property * Add isLoading for schema --- .../azure_monitor/azure_monitor_datasource.ts | 45 ++++++++++++-- .../azuremonitor/azure_monitor/url_builder.ts | 9 +++ .../LogsQueryBuilder/LogsQueryBuilder.tsx | 14 ++++- .../LogsQueryBuilder/TableSection.tsx | 14 ++++- .../components/LogsQueryBuilder/utils.ts | 9 +-- .../LogsQueryEditor/LogsQueryEditor.tsx | 41 +++++++++++-- .../components/QueryEditor/QueryHeader.tsx | 2 + .../types/logAnalyticsMetadata.ts | 4 ++ .../datasource/azuremonitor/types/types.ts | 61 ++++++++++++++++++- 9 files changed, 178 insertions(+), 21 deletions(-) diff --git a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts index 4f0bbb9fd8e..cd8a21476c7 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts @@ -7,23 +7,26 @@ import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/run import { getCredentials } from '../credentials'; import TimegrainConverter from '../time_grain_converter'; import { + AzureAPIResponse, + AzureMetricQuery, AzureMonitorDataSourceInstanceSettings, AzureMonitorDataSourceJsonData, + AzureMonitorLocations, AzureMonitorMetricsMetadataResponse, + AzureMonitorProvidersResponse, AzureMonitorQuery, AzureQueryType, DatasourceValidationResult, + GetLogAnalyticsTableResponse, + GetMetricMetadataQuery, GetMetricNamespacesQuery, GetMetricNamesQuery, - GetMetricMetadataQuery, - AzureMetricQuery, - AzureMonitorLocations, - AzureMonitorProvidersResponse, - AzureAPIResponse, - Subscription, + instanceOfLogAnalyticsTableError, Location, Metric, MetricNamespace, + Subscription, + TablePlan, } from '../types'; import { replaceTemplateVariables, routeNames } from '../utils/common'; import migrateQuery from '../utils/migrateQuery'; @@ -274,6 +277,36 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend< return undefined; } + async getWorkspaceTablePlan(resources: string[], tableName: string): Promise { + let workspaceUri = ''; + + if (resources) { + workspaceUri = resources[0]; + } + + if (!workspaceUri) { + return TablePlan.Analytics; + } + + if (workspaceUri && !workspaceUri.toLowerCase().includes('microsoft.operationalinsights/workspaces')) { + // Not a Log Analytics workspace so default to Analytics + return TablePlan.Analytics; + } + + const url = UrlBuilder.buildAzureMonitorGetLogsTableUrl( + this.resourcePath, + this.templateSrv.replace(workspaceUri), + this.templateSrv.replace(tableName) + ); + const tableResult = await this.getResource(url); + + if (!tableResult || instanceOfLogAnalyticsTableError(tableResult)) { + return TablePlan.Analytics; + } + + return tableResult.properties.plan || TablePlan.Analytics; + } + private isValidConfigField(field?: string): boolean { return typeof field === 'string' && field.length > 0; } diff --git a/public/app/plugins/datasource/azuremonitor/azure_monitor/url_builder.ts b/public/app/plugins/datasource/azuremonitor/azure_monitor/url_builder.ts index 295fcb85ea3..148a8a94baf 100644 --- a/public/app/plugins/datasource/azuremonitor/azure_monitor/url_builder.ts +++ b/public/app/plugins/datasource/azuremonitor/azure_monitor/url_builder.ts @@ -113,4 +113,13 @@ export default class UrlBuilder { return url; } + + static buildAzureMonitorGetLogsTableUrl( + baseUrl: string, + resourceUri: string, + tableName: string, + apiVersion = '2025-02-01' + ) { + return `${baseUrl}${resourceUri}/tables/${tableName}?api-version=${apiVersion}`; + } } diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LogsQueryBuilder.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LogsQueryBuilder.tsx index fe9624ed5ef..1d08f93cc52 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LogsQueryBuilder.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/LogsQueryBuilder.tsx @@ -42,10 +42,11 @@ interface LogsQueryBuilderProps { templateVariableOptions: SelectableValue; datasource: Datasource; timeRange?: TimeRange; + isLoadingSchema: boolean; } export const LogsQueryBuilder: React.FC = (props) => { - const { query, onQueryChange, schema, datasource, timeRange } = props; + const { query, onQueryChange, schema, datasource, timeRange, isLoadingSchema } = props; const [isKQLPreviewHidden, setIsKQLPreviewHidden] = useState(true); const tables: AzureLogAnalyticsMetadataTable[] = useMemo(() => { @@ -70,6 +71,7 @@ export const LogsQueryBuilder: React.FC = (props) => { orderBy, columns, from, + basicLogsQuery, }: { limit?: number; reduce?: BuilderQueryEditorReduceExpression[]; @@ -79,6 +81,7 @@ export const LogsQueryBuilder: React.FC = (props) => { orderBy?: BuilderQueryEditorOrderByExpression[]; columns?: string[]; from?: BuilderQueryEditorPropertyExpression; + basicLogsQuery?: boolean; }) => { const datetimeColumn = allColumns.find((col) => col.type === 'datetime')?.name || 'TimeGenerated'; @@ -122,6 +125,7 @@ export const LogsQueryBuilder: React.FC = (props) => { ...query.azureLogAnalytics, builderQuery: updatedBuilderQuery, query: updatedQueryString, + basicLogsQuery: from ? basicLogsQuery : query.azureLogAnalytics?.basicLogsQuery, }, }); }, @@ -134,7 +138,13 @@ export const LogsQueryBuilder: React.FC = (props) => { {schema && tables.length === 0 && ( )} - + ) => void; templateVariableOptions?: SelectableValue; + onQueryChange: (newQuery: AzureMonitorQuery) => void; + isLoadingSchema: boolean; } export const TableSection: React.FC = (props) => { - const { allColumns, query, tables, buildAndUpdateQuery, templateVariableOptions } = props; + const { allColumns, query, tables, buildAndUpdateQuery, templateVariableOptions, isLoadingSchema } = props; const builderQuery = query.azureLogAnalytics?.builderQuery; const selectedColumns = query.azureLogAnalytics?.builderQuery?.columns?.columns || []; const tableOptions: Array> = tables.map((t) => ({ label: t.name, value: t.name, + description: t.plan === TablePlan.Basic ? 'Selecting this table will switch the query mode to Basic Logs' : '', })); const columnOptions: Array> = allColumns.map((col) => ({ @@ -68,6 +76,7 @@ export const TableSection: React.FC = (props) => { groupBy: [], orderBy: [], columns: [], + basicLogsQuery: selectedTable.plan === TablePlan.Basic, }); }; @@ -138,6 +147,7 @@ export const TableSection: React.FC = (props) => { placeholder="Select a table" onChange={handleTableChange} width={inputFieldSize} + isLoading={isLoadingSchema} /> diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/utils.ts b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/utils.ts index c59771f71f3..3c72a880040 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/utils.ts +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryBuilder/utils.ts @@ -3,14 +3,14 @@ import { escapeRegExp } from 'lodash'; import { SelectableValue } from '@grafana/data'; import { - BuilderQueryExpression, BuilderQueryEditorExpressionType, - BuilderQueryEditorPropertyType, - BuilderQueryEditorReduceExpression, - BuilderQueryEditorWhereExpression, BuilderQueryEditorGroupByExpression, BuilderQueryEditorOrderByExpression, BuilderQueryEditorPropertyExpression, + BuilderQueryEditorPropertyType, + BuilderQueryEditorReduceExpression, + BuilderQueryEditorWhereExpression, + BuilderQueryExpression, } from '../../dataquery.gen'; import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; @@ -88,6 +88,7 @@ export interface BuildAndUpdateOptions { orderBy?: BuilderQueryEditorOrderByExpression[]; columns?: string[]; from?: BuilderQueryEditorPropertyExpression; + basicLogsQuery?: boolean; } export const aggregateOptions = [ diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx index 0970842e25d..655f96705d4 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -8,7 +8,14 @@ import { Alert, LinkButton, Space, Text, TextLink } from '@grafana/ui'; import { LogsEditorMode } from '../../dataquery.gen'; import Datasource from '../../datasource'; import { selectors } from '../../e2e/selectors'; -import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery, ResultFormat, EngineSchema } from '../../types'; +import { + AzureMonitorErrorish, + AzureMonitorOption, + AzureMonitorQuery, + ResultFormat, + EngineSchema, + AzureLogAnalyticsMetadataTable, +} from '../../types'; import { LogsQueryBuilder } from '../LogsQueryBuilder/LogsQueryBuilder'; import ResourceField from '../ResourceField'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types'; @@ -60,6 +67,7 @@ const LogsQueryEditor = ({ const to = templateSrv?.replace('$__to'); const templateVariableOptions = templateSrv.getVariables(); const isBasicLogsQuery = (basicLogsEnabled && query.azureLogAnalytics?.basicLogsQuery) ?? false; + const [isLoadingSchema, setIsLoadingSchema] = useState(false); const disableRow = (row: ResourceRow, selectedRows: ResourceRowGroup) => { if (selectedRows.length === 0) { @@ -83,12 +91,34 @@ const LogsQueryEditor = ({ const [schema, setSchema] = useState(); useEffect(() => { - if (query.azureLogAnalytics?.resources && query.azureLogAnalytics.resources.length) { - datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.resources[0]).then((schema) => { - setSchema(schema); + const resources = query.azureLogAnalytics?.resources; + if (resources) { + setIsLoadingSchema(true); + const fetchAllPlans = async (tables: AzureLogAnalyticsMetadataTable[]) => { + const promises = []; + for (const table of tables) { + promises.push({ + ...table, + plan: await datasource.azureMonitorDatasource.getWorkspaceTablePlan(resources, table.name), + }); + } + + const tablesWithPlan = await Promise.all(promises); + return tablesWithPlan; + }; + datasource.azureLogAnalyticsDatasource.getKustoSchema(resources[0]).then((schema) => { + if (schema?.database?.tables) { + fetchAllPlans(schema?.database?.tables).then(async (t) => { + if (schema.database?.tables) { + schema.database.tables = t; + } + setSchema(schema); + }); + } + setIsLoadingSchema(false); }); } - }, [query.azureLogAnalytics?.resources, datasource.azureLogAnalyticsDatasource]); + }, [query.azureLogAnalytics?.resources, datasource.azureLogAnalyticsDatasource, datasource.azureMonitorDatasource]); useEffect(() => { if (shouldShowBasicLogsToggle(query.azureLogAnalytics?.resources || [], basicLogsEnabled)) { @@ -254,6 +284,7 @@ const LogsQueryEditor = ({ templateVariableOptions={templateVariableOptions} datasource={datasource} timeRange={timeRange} + isLoadingSchema={isLoadingSchema} /> ) : ( ; + retentionInDaysAsDefault: boolean; + totalRetentionInDaysAsDefault: boolean; + schema: { + tableSubType: string; + name: string; + tableType: string; + columns: Array>; + standardColumns: Array>; + solutions: string[]; + isTroubleshootingAllowed: boolean; + description?: string; + displayName?: string; + labels?: string[]; + source?: string; + }; + resultStatistics: Record; + provisioningState: string; + retentionInDays: number; + searchResults?: Record; + systemData?: Record; + }; + id: string; + name: string; + type?: string; +} + +export interface GetLogAnalyticsTableErrorResponse { + error: { + target: string; + message: string; + code: string; + }; +} + +export type GetLogAnalyticsTableResponse = GetLogAnalyticsTableSuccessResponse | GetLogAnalyticsTableErrorResponse; + +export function instanceOfLogAnalyticsTableError( + response: GetLogAnalyticsTableSuccessResponse | GetLogAnalyticsTableErrorResponse +): response is GetLogAnalyticsTableErrorResponse { + if (!response) { + return false; + } + return response.hasOwnProperty('error'); +} From 032299011a26483567cce9b31c149dcd295e23d2 Mon Sep 17 00:00:00 2001 From: Moustafa Baiou Date: Thu, 10 Apr 2025 19:58:37 -0400 Subject: [PATCH 14/37] Alerting: Relax permissions for access a rule (#103664) This makes it so that it is: - No longer required to have datasource permissions to delete a rule. - No longer required to have datasource permissions to update non-query related fields of a rule. --- pkg/services/ngalert/accesscontrol/rules.go | 17 +- .../ngalert/accesscontrol/rules_test.go | 187 ++++++++++++++++-- pkg/services/ngalert/api/api_ruler.go | 18 -- pkg/services/ngalert/api/api_ruler_test.go | 40 +++- pkg/services/ngalert/store/deltas.go | 18 ++ pkg/services/ngalert/store/deltas_test.go | 76 +++++++ 6 files changed, 304 insertions(+), 52 deletions(-) diff --git a/pkg/services/ngalert/accesscontrol/rules.go b/pkg/services/ngalert/accesscontrol/rules.go index 8aa64c479e9..fdbd243c62a 100644 --- a/pkg/services/ngalert/accesscontrol/rules.go +++ b/pkg/services/ngalert/accesscontrol/rules.go @@ -180,13 +180,6 @@ func (r *RuleService) AuthorizeRuleChanges(ctx context.Context, user identity.Re }); err != nil { return err } - for _, rule := range change.Delete { - if err := r.HasAccessOrError(ctx, user, r.getRulesQueryEvaluator(rule), func() string { - return fmt.Sprintf("delete an alert rule '%s'", rule.UID) - }); err != nil { - return err - } - } } var addAuthorized, updateAuthorized bool // these are needed to check authorization for the rule create\update only once @@ -217,10 +210,12 @@ func (r *RuleService) AuthorizeRuleChanges(ctx context.Context, user identity.Re } for _, rule := range change.Update { - if err := r.HasAccessOrError(ctx, user, r.getRulesQueryEvaluator(rule.New), func() string { - return fmt.Sprintf("update alert rule '%s' (UID: %s)", rule.Existing.Title, rule.Existing.UID) - }); err != nil { - return err + if rule.AffectsQuery() { + if err := r.HasAccessOrError(ctx, user, r.getRulesQueryEvaluator(rule.New), func() string { + return fmt.Sprintf("update alert rule query '%s' (UID: %s)", rule.Existing.Title, rule.Existing.UID) + }); err != nil { + return err + } } // Check if the rule is moved from one folder to the current. If yes, then the user must have the authorization to delete rules from the source folder and add rules to the target folder. diff --git a/pkg/services/ngalert/accesscontrol/rules_test.go b/pkg/services/ngalert/accesscontrol/rules_test.go index eb15ffac8bb..a9006f5ef0f 100644 --- a/pkg/services/ngalert/accesscontrol/rules_test.go +++ b/pkg/services/ngalert/accesscontrol/rules_test.go @@ -2,6 +2,7 @@ package accesscontrol import ( "context" + "fmt" "math" "math/rand" "testing" @@ -23,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/cmputil" ) func createAllCombinationsOfPermissions(permissions map[string][]string) []map[string][]string { @@ -108,6 +110,30 @@ func createUserWithPermissions(permissions map[string][]string) identity.Request }} } +func getShallowQueryDiffs(queries []models.AlertQuery) []cmputil.Diff { + result := make([]cmputil.Diff, 0, len(queries)) + for i := range queries { + result = append(result, []cmputil.Diff{ + { + Path: fmt.Sprintf("Data[%d].DatasourceUID", i), + }, + { + Path: fmt.Sprintf("Data[%d].Model", i), + }, + { + Path: fmt.Sprintf("Data[%d].RelativeTimeRange", i), + }, + { + Path: fmt.Sprintf("Data[%d].RefID", i), + }, + { + Path: fmt.Sprintf("Data[%d].QueryType", i), + }, + }...) + } + return result +} + func TestAuthorizeRuleChanges(t *testing.T) { groupKey := models.GenerateGroupKey(rand.Int63()) namespaceIdScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(groupKey.NamespaceUID) @@ -146,7 +172,7 @@ func TestAuthorizeRuleChanges(t *testing.T) { }, }, { - name: "if there are rules to delete it should check delete action and query for datasource", + name: "if there are rules to delete it should check delete action and NOT query for datasource", changes: func() *store.GroupDelta { rules := genWithGroupKey.GenerateManyRef(1, 5) rules2 := genWithGroupKey.GenerateManyRef(1, 5) @@ -171,12 +197,11 @@ func TestAuthorizeRuleChanges(t *testing.T) { ruleDelete: { namespaceIdScope, }, - datasources.ActionQuery: getDatasourceScopesForRules(c.Delete), } }, }, { - name: "if there are rules to update within the same namespace it should check update action and access to datasource", + name: "if there are rules with query updates within the same namespace it should check update action and access to datasource", changes: func() *store.GroupDelta { rules1 := genWithGroupKey.GenerateManyRef(1, 5) rules := genWithGroupKey.GenerateManyRef(1, 5) @@ -188,7 +213,7 @@ func TestAuthorizeRuleChanges(t *testing.T) { updates = append(updates, store.RuleDelta{ Existing: rule, New: cp, - Diff: nil, + Diff: getShallowQueryDiffs(cp.Data), }) } @@ -220,6 +245,55 @@ func TestAuthorizeRuleChanges(t *testing.T) { } }, }, + { + name: "if there are rules w/o query updates to update within the same namespace it should check update action", + changes: func() *store.GroupDelta { + rules1 := genWithGroupKey.GenerateManyRef(1, 5) + rules := genWithGroupKey.GenerateManyRef(1, 5) + updates := make([]store.RuleDelta, 0, len(rules)) + + for _, rule := range rules { + cp := models.CopyRule(rule) + cp.IsPaused = !rule.IsPaused + cp.Title = rule.Title + " updated" + updates = append(updates, store.RuleDelta{ + Existing: rule, + New: cp, + Diff: []cmputil.Diff{ + { + Path: "IsPaused", + }, + { + Path: "Title", + }, + }, + }) + } + + return &store.GroupDelta{ + GroupKey: groupKey, + AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ + groupKey: append(rules, rules1...), + }, + New: nil, + Update: updates, + Delete: nil, + } + }, + permissions: func(c *store.GroupDelta) map[string][]string { + return map[string][]string{ + ruleRead: { + namespaceIdScope, + }, + dashboards.ActionFoldersRead: { + namespaceIdScope, + }, + ruleUpdate: { + namespaceIdScope, + }, + } + }, + }, { name: "if there are rules that are moved between namespaces it should check delete+add action and access to group where rules come from", changes: func() *store.GroupDelta { @@ -230,11 +304,22 @@ func TestAuthorizeRuleChanges(t *testing.T) { updates := make([]store.RuleDelta, 0, len(rules)) for _, rule := range rules { - cp := models.CopyRule(rule, gen.WithGroupKey(targetGroupKey), gen.WithQuery(gen.GenerateQuery())) + cp := models.CopyRule(rule, gen.WithGroupKey(targetGroupKey)) updates = append(updates, store.RuleDelta{ Existing: rule, New: cp, + Diff: []cmputil.Diff{ + { + Path: "OrgID", + }, + { + Path: "NamespaceUID", + }, + { + Path: "RuleGroup", + }, + }, }) } @@ -249,12 +334,6 @@ func TestAuthorizeRuleChanges(t *testing.T) { } }, permissions: func(c *store.GroupDelta) map[string][]string { - dsScopes := getDatasourceScopesForRules( - mapUpdates(c.Update, func(update store.RuleDelta) *models.AlertRule { - return update.New - }), - ) - var deleteScopes []string for key := range c.AffectedGroups { deleteScopes = append(deleteScopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(key.NamespaceUID)) @@ -265,7 +344,6 @@ func TestAuthorizeRuleChanges(t *testing.T) { ruleCreate: { dashboards.ScopeFoldersProvider.GetResourceScopeUID(c.GroupKey.NamespaceUID), }, - datasources.ActionQuery: dsScopes, } }, }, @@ -280,6 +358,68 @@ func TestAuthorizeRuleChanges(t *testing.T) { sourceGroup := genWithGroupKey.GenerateManyRef(1, 5) targetGroup := gen.With(gen.WithGroupKey(targetGroupKey)).GenerateManyRef(1, 5) + updates := make([]store.RuleDelta, 0, len(sourceGroup)) + toCopy := len(sourceGroup) + if toCopy > 1 { + toCopy = rand.Intn(toCopy-1) + 1 + } + for i := 0; i < toCopy; i++ { + rule := sourceGroup[0] + cp := models.CopyRule(rule, gen.WithGroupKey(targetGroupKey)) + + updates = append(updates, store.RuleDelta{ + Existing: rule, + New: cp, + Diff: []cmputil.Diff{ + { + Path: "OrgID", + }, + { + Path: "NamespaceUID", + }, + { + Path: "RuleGroup", + }, + }, + }) + } + + return &store.GroupDelta{ + GroupKey: targetGroupKey, + AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{ + groupKey: sourceGroup, + targetGroupKey: targetGroup, + }, + New: nil, + Update: updates, + Delete: nil, + } + }, + permissions: func(c *store.GroupDelta) map[string][]string { + return map[string][]string{ + ruleRead: { + dashboards.ScopeFoldersProvider.GetResourceScopeUID(c.GroupKey.NamespaceUID), + }, + dashboards.ActionFoldersRead: { + dashboards.ScopeFoldersProvider.GetResourceScopeUID(c.GroupKey.NamespaceUID), + }, + ruleUpdate: { + dashboards.ScopeFoldersProvider.GetResourceScopeUID(c.GroupKey.NamespaceUID), + }, + } + }, + }, + { + name: "if there are rules that are moved between groups in the same namespace AND the query is changed it should check update action and access to all groups (source+target) and datasources", + changes: func() *store.GroupDelta { + targetGroupKey := models.AlertRuleGroupKey{ + OrgID: groupKey.OrgID, + NamespaceUID: groupKey.NamespaceUID, + RuleGroup: util.GenerateShortUID(), + } + sourceGroup := genWithGroupKey.GenerateManyRef(1, 5) + targetGroup := gen.With(gen.WithGroupKey(targetGroupKey)).GenerateManyRef(1, 5) + updates := make([]store.RuleDelta, 0, len(sourceGroup)) toCopy := len(sourceGroup) if toCopy > 1 { @@ -292,6 +432,20 @@ func TestAuthorizeRuleChanges(t *testing.T) { updates = append(updates, store.RuleDelta{ Existing: rule, New: cp, + Diff: append( + []cmputil.Diff{ + { + Path: "OrgID", + }, + { + Path: "NamespaceUID", + }, + { + Path: "RuleGroup", + }, + }, + getShallowQueryDiffs(cp.Data)..., + ), }) } @@ -372,7 +526,11 @@ func TestAuthorizeRuleChanges(t *testing.T) { updates = append(updates, store.RuleDelta{ Existing: rule, New: cp, - Diff: nil, + Diff: []cmputil.Diff{ + { + Path: "NotificationSettings[0].Receiver", + }, + }, }) } @@ -397,9 +555,6 @@ func TestAuthorizeRuleChanges(t *testing.T) { ruleUpdate: { namespaceIdScope, }, - datasources.ActionQuery: getDatasourceScopesForRules(mapUpdates(c.Update, func(update store.RuleDelta) *models.AlertRule { - return update.New - })), accesscontrol.ActionAlertingReceiversRead: getReceiverScopesForRules(mapUpdates(c.Update, func(update store.RuleDelta) *models.AlertRule { return update.New })), diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index f7da9657a7e..7d3927a0f77 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -145,23 +145,12 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceU } rulesToDelete := make([]string, 0) provisioned := false - auth := true for groupKey, rules := range deletionCandidates { if containsProvisionedAlerts(provenances, rules) { logger.Debug("Alert group cannot be deleted because it is provisioned", "group", groupKey.RuleGroup) provisioned = true continue } - // XXX: Currently delete requires data source query access to all rules in the group. - if err := srv.authz.AuthorizeDatasourceAccessForRuleGroup(ctx, c.SignedInUser, rules); err != nil { - if errors.Is(err, authz.ErrAuthorizationBase) { - logger.Debug("User is not authorized to delete rules in the group", "group", groupKey.RuleGroup) - auth = false - continue - } else { - return err - } - } uid := make([]string, 0, len(rules)) for _, rule := range rules { uid = append(uid, rule.UID) @@ -177,17 +166,10 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceU return nil } // if none rules were deleted return an error. - // Check whether provisioned check failed first because if it is true, then all rules that the user can access (actually read via GET API) are provisioned. if provisioned { return errProvisionedResource } - - // If auth is false, then the user is not authorized to delete any of the rules. - if !auth { - return authz.NewAuthorizationErrorGeneric("delete any existing rules in the namespace") - } - logger.Info("No alert rules were deleted") return nil }) diff --git a/pkg/services/ngalert/api/api_ruler_test.go b/pkg/services/ngalert/api/api_ruler_test.go index 4b3c87910ab..8fc7e7212da 100644 --- a/pkg/services/ngalert/api/api_ruler_test.go +++ b/pkg/services/ngalert/api/api_ruler_test.go @@ -81,6 +81,24 @@ func TestRouteDeleteAlertRules(t *testing.T) { t.Run("when fine-grained access is enabled", func(t *testing.T) { t.Run("and group argument is empty", func(t *testing.T) { + t.Run("allow deleting without access to datasource", func(t *testing.T) { + ruleStore := initFakeRuleStore(t) + provisioningStore := fakes.NewFakeProvisioningStore() + + folderGen := gen.With(gen.WithNamespace(folder.ToFolderReference())) + + authorizedRulesInFolder := folderGen.With(gen.WithGroupPrefix("authz-")).GenerateManyRef(1, 5) + + ruleStore.PutRule(context.Background(), authorizedRulesInFolder...) + + permissions := createPermissionsForRulesWithoutDS(authorizedRulesInFolder, orgID) + requestCtx := createRequestContextWithPerms(orgID, permissions, nil) + + response := createServiceWithProvenanceStore(ruleStore, provisioningStore).RouteDeleteAlertRules(requestCtx, folder.UID, "") + + require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body())) + assertRulesDeleted(t, authorizedRulesInFolder, ruleStore) + }) t.Run("return Forbidden if user is not authorized to access any group in the folder", func(t *testing.T) { ruleStore := initFakeRuleStore(t) ruleStore.PutRule(context.Background(), gen.With(gen.WithNamespace(folder.ToFolderReference())).GenerateManyRef(1, 5)...) @@ -108,8 +126,6 @@ func TestRouteDeleteAlertRules(t *testing.T) { ruleStore.PutRule(context.Background(), authorizedRulesInFolder...) ruleStore.PutRule(context.Background(), provisionedRulesInFolder...) - // more rules in the same namespace but user does not have access to them - ruleStore.PutRule(context.Background(), folderGen.With(gen.WithGroupPrefix("unauthz")).GenerateManyRef(1, 5)...) permissions := createPermissionsForRules(append(authorizedRulesInFolder, provisionedRulesInFolder...), orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) @@ -130,8 +146,6 @@ func TestRouteDeleteAlertRules(t *testing.T) { require.NoError(t, err) ruleStore.PutRule(context.Background(), provisionedRulesInFolder...) - // more rules in the same namespace but user does not have access to them - ruleStore.PutRule(context.Background(), folderGen.With(gen.WithSameGroup()).GenerateManyRef(1, 5)...) permissions := createPermissionsForRules(provisionedRulesInFolder, orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) @@ -159,10 +173,8 @@ func TestRouteDeleteAlertRules(t *testing.T) { authorizedRulesInGroup := groupGen.GenerateManyRef(1, 5) ruleStore.PutRule(context.Background(), authorizedRulesInGroup...) - // more rules in the same group but user is not authorized to access them - ruleStore.PutRule(context.Background(), groupGen.GenerateManyRef(1, 5)...) - permissions := createPermissionsForRules(authorizedRulesInGroup, orgID) + permissions := createPermissionsForRules([]*models.AlertRule{}, orgID) requestCtx := createRequestContextWithPerms(orgID, permissions, nil) response := createService(ruleStore, nil).RouteDeleteAlertRules(requestCtx, folder.UID, authorizedRulesInGroup[0].RuleGroup) @@ -1014,3 +1026,17 @@ func createPermissionsForRules(rules []*models.AlertRule, orgID int64) map[int64 } return map[int64]map[string][]string{orgID: permissions} } + +func createPermissionsForRulesWithoutDS(rules []*models.AlertRule, orgID int64) map[int64]map[string][]string { + ns := map[string]any{} + permissions := map[string][]string{} + for _, rule := range rules { + if _, ok := ns[rule.NamespaceUID]; !ok { + scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(rule.NamespaceUID) + permissions[dashboards.ActionFoldersRead] = append(permissions[dashboards.ActionFoldersRead], scope) + permissions[ac.ActionAlertingRuleRead] = append(permissions[ac.ActionAlertingRuleRead], scope) + ns[rule.NamespaceUID] = struct{}{} + } + } + return map[int64]map[string][]string{orgID: permissions} +} diff --git a/pkg/services/ngalert/store/deltas.go b/pkg/services/ngalert/store/deltas.go index d59980604f1..f4fe12c98f8 100644 --- a/pkg/services/ngalert/store/deltas.go +++ b/pkg/services/ngalert/store/deltas.go @@ -3,6 +3,7 @@ package store import ( "context" "fmt" + "strings" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/util/cmputil" @@ -11,12 +12,29 @@ import ( // AlertRuleFieldsToIgnoreInDiff contains fields that are ignored when calculating the RuleDelta.Diff. var AlertRuleFieldsToIgnoreInDiff = [...]string{"ID", "Version", "Updated", "UpdatedBy"} +// AlertRuleFieldsWhichAffectQuery contains fields which affect the rule's query(s) +var AlertRuleFieldsWhichAffectQuery = [...]string{"Data", "IntervalSeconds"} + type RuleDelta struct { Existing *models.AlertRule New *models.AlertRule Diff cmputil.DiffReport } +func (d *RuleDelta) AffectsQuery() bool { + if len(d.Diff) == 0 { + return false + } + for _, path := range d.Diff.Paths() { + for _, field := range AlertRuleFieldsWhichAffectQuery { + if strings.HasPrefix(path, field) { + return true + } + } + } + return false +} + type GroupDelta struct { GroupKey models.AlertRuleGroupKey // AffectedGroups contains all rules of all groups that are affected by these changes. diff --git a/pkg/services/ngalert/store/deltas_test.go b/pkg/services/ngalert/store/deltas_test.go index dbfeb556ce4..153fb7cadde 100644 --- a/pkg/services/ngalert/store/deltas_test.go +++ b/pkg/services/ngalert/store/deltas_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "reflect" "testing" "time" @@ -15,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/util" + "github.com/grafana/grafana/pkg/util/cmputil" ) func TestCalculateChanges(t *testing.T) { @@ -732,6 +734,80 @@ func TestCalculateRuleCreate(t *testing.T) { }) } +func TestDeltaAffectsQuery(t *testing.T) { + t.Run("returns false when there are no diffs", func(t *testing.T) { + delta := RuleDelta{ + Diff: cmputil.DiffReport{}, + } + assert.False(t, delta.AffectsQuery()) + }) + t.Run("returns true when diff contains a field that affects query", func(t *testing.T) { + delta := RuleDelta{ + Diff: cmputil.DiffReport{ + { + Path: "Data", + Left: reflect.ValueOf("old value"), + Right: reflect.ValueOf("new value"), + }, + }, + } + assert.True(t, delta.AffectsQuery()) + }) + t.Run("returns false when diff contains only fields that do not affect query", func(t *testing.T) { + delta := RuleDelta{ + Diff: cmputil.DiffReport{ + { + Path: "Title", + Left: reflect.ValueOf("old title"), + Right: reflect.ValueOf("new title"), + }, + }, + } + assert.False(t, delta.AffectsQuery()) + }) + t.Run("returns true when diff contains multiple fields, including one that affects query", func(t *testing.T) { + delta := RuleDelta{ + Diff: cmputil.DiffReport{ + { + Path: "Title", + Left: reflect.ValueOf("old title"), + Right: reflect.ValueOf("new title"), + }, + { + Path: "IntervalSeconds", + Left: reflect.ValueOf(10), + Right: reflect.ValueOf(20), + }, + }, + } + assert.True(t, delta.AffectsQuery()) + }) + t.Run("handles nested paths in diff", func(t *testing.T) { + delta := RuleDelta{ + Diff: cmputil.DiffReport{ + { + Path: "Data[0].Query", + Left: reflect.ValueOf("old query"), + Right: reflect.ValueOf("new query"), + }, + }, + } + assert.True(t, delta.AffectsQuery()) + }) + t.Run("returns false for empty diff paths", func(t *testing.T) { + delta := RuleDelta{ + Diff: cmputil.DiffReport{ + { + Path: "", + Left: reflect.ValueOf("old value"), + Right: reflect.ValueOf("new value"), + }, + }, + } + assert.False(t, delta.AffectsQuery()) + }) +} + // simulateSubmitted resets some fields of the structure that are not populated by API model to model conversion func simulateSubmitted(rule *models.AlertRule) { rule.ID = 0 From 7aa862897145081d110dc85d8b73e5f7c5a80219 Mon Sep 17 00:00:00 2001 From: Andreas Christou Date: Fri, 11 Apr 2025 01:31:45 +0100 Subject: [PATCH 15/37] AzureMonitor: Only fetch table plans in the builder (#103833) * Only fetch table plans in the builder * Appropriately set schema --- .../components/LogsQueryEditor/LogsQueryEditor.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx index 655f96705d4..c3cb8d9beca 100644 --- a/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/LogsQueryEditor.tsx @@ -107,18 +107,25 @@ const LogsQueryEditor = ({ return tablesWithPlan; }; datasource.azureLogAnalyticsDatasource.getKustoSchema(resources[0]).then((schema) => { - if (schema?.database?.tables) { + if (schema?.database?.tables && query.azureLogAnalytics?.mode === LogsEditorMode.Builder) { fetchAllPlans(schema?.database?.tables).then(async (t) => { if (schema.database?.tables) { schema.database.tables = t; } setSchema(schema); }); + } else { + setSchema(schema); } setIsLoadingSchema(false); }); } - }, [query.azureLogAnalytics?.resources, datasource.azureLogAnalyticsDatasource, datasource.azureMonitorDatasource]); + }, [ + query.azureLogAnalytics?.resources, + datasource.azureLogAnalyticsDatasource, + datasource.azureMonitorDatasource, + query.azureLogAnalytics?.mode, + ]); useEffect(() => { if (shouldShowBasicLogsToggle(query.azureLogAnalytics?.resources || [], basicLogsEnabled)) { From 04fae8bd4616249739bd2e0f70a261fcce5177c2 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 11 Apr 2025 08:33:11 +0300 Subject: [PATCH 16/37] Provisioning: Update ConfigForm (#103846) * Provisioning: Fix token permissions alignment * Update config form * Extract getDefaultValues --- .../provisioning/Config/ConfigForm.tsx | 34 +---- .../Config/ConfigFormGithubCollapse.tsx | 128 +++++++----------- .../features/provisioning/Config/defaults.ts | 25 ++++ .../Shared/TokenPermissionsInfo.tsx | 2 +- .../Wizard/ProvisioningWizard.tsx | 4 +- public/locales/en-US/grafana.json | 15 +- 6 files changed, 85 insertions(+), 123 deletions(-) create mode 100644 public/app/features/provisioning/Config/defaults.ts diff --git a/public/app/features/provisioning/Config/ConfigForm.tsx b/public/app/features/provisioning/Config/ConfigForm.tsx index 780cdca848e..73eeea1252a 100644 --- a/public/app/features/provisioning/Config/ConfigForm.tsx +++ b/public/app/features/provisioning/Config/ConfigForm.tsx @@ -14,39 +14,19 @@ import { Stack, Switch, } from '@grafana/ui'; -import { Repository, RepositorySpec } from 'app/api/clients/provisioning'; +import { Repository } from 'app/api/clients/provisioning'; import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt'; import { t, Trans } from 'app/core/internationalization'; import { TokenPermissionsInfo } from '../Shared/TokenPermissionsInfo'; import { useCreateOrUpdateRepository } from '../hooks/useCreateOrUpdateRepository'; import { RepositoryFormData } from '../types'; -import { dataToSpec, specToData } from '../utils/data'; +import { dataToSpec } from '../utils/data'; import { ConfigFormGithubCollapse } from './ConfigFormGithubCollapse'; +import { getDefaultValues } from './defaults'; -export function getDefaultValues(repository?: RepositorySpec): RepositoryFormData { - if (!repository) { - return { - type: 'github', - title: 'Repository', - token: '', - url: '', - branch: 'main', - generateDashboardPreviews: false, - readOnly: false, - prWorkflow: true, - path: 'grafana/', - sync: { - enabled: false, - target: 'instance', - intervalSeconds: 60, - }, - }; - } - return specToData(repository); -} - +// This needs to be a function for translations to work const getOptions = () => { const typeOptions = [ { value: 'github', label: t('provisioning.config-form.option-github', 'GitHub') }, @@ -254,11 +234,7 @@ export function ConfigForm({ data }: ConfigFormProps) { } /> - {type === 'github' && ( - } - /> - )} + {type === 'github' && } ; } -export function ConfigFormGithubCollapse({ previews }: ConfigFormGithubCollapseProps) { - const navigate = useNavigate(); + +export function ConfigFormGithubCollapse({ register }: ConfigFormGithubCollapseProps) { + const isPublic = checkPublicAccess(); + const hasImageRenderer = checkImageRenderer(); return ( -

- Realtime feedback -

- {checkPublicAccess() ? ( -
- - - Changes in git will be quickly pulled into grafana. Pull requests can be processed. - - -
- ) : ( - Instructions} - onRemove={() => navigate(GETTING_STARTED_URL)} - > - - Changes in git will eventually be pulled depending on the synchronization interval. Pull requests will not - be processed - - - )} - -

- - Pull Request image previews - -

- {!config.rendererAvailable && ( - Instructions} - onRemove={() => window.open('https://grafana.com/grafana/plugins/grafana-image-renderer/', '_blank')} - > - - When the image renderer is configured, pull requests can see preview images - - - )} - - - - Render before/after images and link them to the pull request. -
- NOTE: This will render dashboards into an image that can be access by a public URL -
- - } - > - {previews} + + + + Adds an image preview of dashboard changes in pull requests. Images of your Grafana dashboards will be + shared in your Git repository and visible to anyone with repository access. + {' '} + + + Requires image rendering.{' '} + + Set up image rendering + + + + + } + {...register('generateDashboardPreviews')} + /> + + {!isPublic && ( + + + + + Configure webhooks + {' '} + to get instant updates in Grafana as soon as changes are committed. Review and approve changes using pull + requests before they go live. + + + + )}
); } diff --git a/public/app/features/provisioning/Config/defaults.ts b/public/app/features/provisioning/Config/defaults.ts new file mode 100644 index 00000000000..4c7d77731ef --- /dev/null +++ b/public/app/features/provisioning/Config/defaults.ts @@ -0,0 +1,25 @@ +import { RepositorySpec } from '../../../api/clients/provisioning'; +import { RepositoryFormData } from '../types'; +import { specToData } from '../utils/data'; + +export function getDefaultValues(repository?: RepositorySpec): RepositoryFormData { + if (!repository) { + return { + type: 'github', + title: 'Repository', + token: '', + url: '', + branch: 'main', + generateDashboardPreviews: false, + readOnly: false, + prWorkflow: true, + path: 'grafana/', + sync: { + enabled: false, + target: 'instance', + intervalSeconds: 60, + }, + }; + } + return specToData(repository); +} diff --git a/public/app/features/provisioning/Shared/TokenPermissionsInfo.tsx b/public/app/features/provisioning/Shared/TokenPermissionsInfo.tsx index c97bea79c18..2c995490821 100644 --- a/public/app/features/provisioning/Shared/TokenPermissionsInfo.tsx +++ b/public/app/features/provisioning/Shared/TokenPermissionsInfo.tsx @@ -11,7 +11,7 @@ export function TokenPermissionsInfo() {
{/* GitHub UI is English only, so these strings are not translated */} {/* eslint-disable-next-line @grafana/no-untranslated-strings */} - + Go to GitHub Personal Access Tokens diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx index dd0d5f167dd..bdc91f790f1 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx @@ -10,7 +10,7 @@ import { useDeleteRepositoryMutation, useGetFrontendSettingsQuery } from 'app/ap import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt'; import { t } from 'app/core/internationalization'; -import { getDefaultValues } from '../Config/ConfigForm'; +import { getDefaultValues } from '../Config/defaults'; import { PROVISIONING_URL } from '../constants'; import { useCreateOrUpdateRepository } from '../hooks/useCreateOrUpdateRepository'; import { dataToSpec } from '../utils/data'; @@ -41,7 +41,7 @@ const getSteps = (): Array> => { }, { id: 'synchronize', - name: t('provisioning.wizard.step-synchronize', 'Synchronize'), + name: t('provisioning.wizard.step-synchronize', 'Synchronize with external storage'), title: t('provisioning.wizard.title-synchronize', 'Synchronize with external storage'), submitOnNext: false, }, diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index c2ecec89b32..d9037ed2ddd 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -6476,18 +6476,9 @@ "placeholder-select-repository-type": "Select repository type" }, "config-form-github-collapse": { - "description-attach-dashboard-previews": "Render before/after images and link them to the pull request.<1>NOTE: This will render dashboards into an image that can be access by a public URL", - "instructions": "Instructions", - "label-attach-dashboard-previews": "Attach dashboard previews to pull requests", + "description-realtime-feedback": "<0>Configure webhooks to get instant updates in Grafana as soon as changes are committed. Review and approve changes using pull requests before they go live.", "label-git-hub-features": "GitHub features", - "pull-request-image-previews": "Pull Request image previews", - "realtime-feedback": "Realtime feedback", - "text-changes-in-git-eventually-pulled": "Changes in git will eventually be pulled depending on the synchronization interval. Pull requests will not be processed", - "text-changes-in-git-quick-pull": "Changes in git will be quickly pulled into grafana. Pull requests can be processed.", - "text-when-image-renderer-configured": "When the image renderer is configured, pull requests can see preview images", - "title-image-renderer-not-configured": "Image renderer not configured", - "title-public-url-not-configured": "Public URL not configured", - "title-webhook-will-be-created": "Webhook will be created" + "label-realtime-feedback": "Realtime feedback" }, "connect-repository-button": { "configure": "Configure", @@ -6777,7 +6768,7 @@ "step-bootstrap": "Choose what to synchronize", "step-connect": "Connect", "step-finish": "Choose additional settings", - "step-synchronize": "Synchronize", + "step-synchronize": "Synchronize with external storage", "sync-description": "Sync resources with external storage. After this one-time step, all future updates will be automatically saved to the repository and provisioned back into the instance.", "sync-option-history": "History", "title-bootstrap": "Choose what to synchronize", From 1e669cbb456d1766cddcc39c9a32623c3fc11222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 11 Apr 2025 09:25:09 +0200 Subject: [PATCH 17/37] AppChrome: Sidebar extensions button update (#103791) * AppChrome: Sidebar app icon button update * Update * remove unused files * Update public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx Co-authored-by: Sven Grossmann * update --------- Co-authored-by: Sven Grossmann --- packages/grafana-data/src/types/icon.ts | 1 + .../ExtensionSidebar/ExtensionToolbarItem.tsx | 40 ++++++++++++++----- public/app/core/icons/cached.json | 7 +++- public/img/icons/unicons/ai-sparkle.svg | 4 ++ public/locales/en-US/grafana.json | 3 ++ 5 files changed, 45 insertions(+), 10 deletions(-) create mode 100644 public/img/icons/unicons/ai-sparkle.svg diff --git a/packages/grafana-data/src/types/icon.ts b/packages/grafana-data/src/types/icon.ts index 6c53ddcb48a..d3dea16ff5c 100644 --- a/packages/grafana-data/src/types/icon.ts +++ b/packages/grafana-data/src/types/icon.ts @@ -269,6 +269,7 @@ export const availableIconsIndex = { 'add-user': true, attach: true, 'dollar-alt': true, + 'ai-sparkle': true, }; export type IconName = keyof typeof availableIconsIndex; diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx index d7af125d894..be49778db62 100644 --- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx +++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx @@ -1,13 +1,15 @@ -import { useState } from 'react'; +import { css, cx } from '@emotion/css'; -import { Dropdown, Menu, ToolbarButton } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { Dropdown, Menu, ToolbarButton, useTheme2 } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator'; import { getComponentIdFromComponentMeta, useExtensionSidebarContext } from './ExtensionSidebarProvider'; export function ExtensionToolbarItem() { - const [isMenuOpen, setIsMenuOpen] = useState(false); + const styles = getStyles(useTheme2()); const { availableComponents, dockedComponentId, setDockedComponentId, isOpen, isEnabled } = useExtensionSidebarContext(); @@ -28,9 +30,9 @@ export function ExtensionToolbarItem() { return ( <> { if (isOpen) { @@ -68,15 +70,35 @@ export function ExtensionToolbarItem() { ); return ( <> - + ); } + +function getStyles(theme: GrafanaTheme2) { + return { + button: css({ + width: '28px', + height: '28px', + padding: 0, + justifyContent: 'center', + borderRadius: theme.shape.radius.circle, + margin: theme.spacing(0, 0.25), + }), + buttonActive: css({ + borderRadius: theme.shape.radius.circle, + backgroundColor: theme.colors.primary.transparent, + border: `1px solid ${theme.colors.primary.borderTransparent}`, + color: theme.colors.text.primary, + }), + }; +} diff --git a/public/app/core/icons/cached.json b/public/app/core/icons/cached.json index 28cfb2d352c..fa49afb6bf4 100644 --- a/public/app/core/icons/cached.json +++ b/public/app/core/icons/cached.json @@ -142,6 +142,7 @@ "unicons/hourglass", "unicons/layer-group", "unicons/layers-alt", + "unicons/layers-slash", "unicons/line-alt", "unicons/list-ui-alt", "unicons/message", @@ -173,5 +174,9 @@ "mono/panel-add", "mono/library-panel", "unicons/record-audio", - "solid/bookmark" + "solid/bookmark", + "unicons/ai-sparkle", + "unicons/dollar-alt", + "unicons/window-grid", + "unicons/ban" ] diff --git a/public/img/icons/unicons/ai-sparkle.svg b/public/img/icons/unicons/ai-sparkle.svg new file mode 100644 index 00000000000..8ec596330fc --- /dev/null +++ b/public/img/icons/unicons/ai-sparkle.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index d9037ed2ddd..4c99d311a6b 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5994,6 +5994,9 @@ "breadcrumbs": { "aria-label": "Breadcrumbs" }, + "extension-sidebar": { + "button-tooltip": "Open AI assistants and sidebar apps" + }, "help": { "aria-label": "Help" }, From 0a8dccc19aff649728f5d7c2e4fce97f08faa3c7 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 11 Apr 2025 10:02:34 +0200 Subject: [PATCH 18/37] Alerting: New alert list filter improvements (#103107) * Move filtering code to generators for performance reasons Discarding rules and groups early in the iterable chain limits the number of promises we need to wait for which improves performance significantly * Add error handling for generators * Add support for data source filter for GMA rules * search WIP fix * Fix datasource filter * Move filtering back to filtered rules hook, use paged groups for improved performance * Add queriedDatasources field to grafana managed rules and update filtering logic to rely on it - Introduced a new field `queriedDatasources` in the AlertingRule struct to track data sources used in rules. - Updated the Prometheus API to populate `queriedDatasources` when creating alerting rules. - Modified filtering logic in the ruleFilter function to utilize the new `queriedDatasources` field for improved data source matching. - Adjusted related tests to reflect changes in rule structure and filtering behavior. * Add FilterView performance logging * Improve GMA Prometheus types, rename queried datasources property * Use custom generator helpers for flattening and filtering rule groups * Fix lint errors, add missing translations * Revert test condition * Refactor api prom changes * Fix lint errors * Update backend tests * Refactor rule list components to improve error handling and data source management - Enhanced error handling in FilterViewResults by logging errors before returning an empty iterable. - Simplified conditional rendering in GrafanaRuleLoader for better readability. - Updated data source handling in PaginatedDataSourceLoader and PaginatedGrafanaLoader to use new individual rule group generator. - Renamed toPageless function to toIndividualRuleGroups for clarity in prometheusGroupsGenerator. - Improved filtering logic in useFilteredRulesIterator to utilize a dedicated function for data source type validation. - Added isRulesDataSourceType utility function for better data source type checks. - Removed commented-out code in PromRuleDTOBase for cleaner interface definition. * Fix abort controller on FilterView * Improve generators filtering * fix abort controller * refactor cancelSearch * make states exclusive * Load full page in one loadResultPage call * Update tests, update translations * Refactor filter status into separate component * hoist hook * Use the new function for supported rules source type --------- Co-authored-by: Gilles De Mey --- .../ngalert/api/api_prometheus_test.go | 3 + .../ngalert/api/prometheus/api_prometheus.go | 29 +- .../ngalert/api/tooling/definitions/prom.go | 15 +- .../features/alerting/unified/Analytics.ts | 30 +- .../unified/PanelAlertTabContent.test.tsx | 3 - .../alerting/unified/api/alertmanagerApi.ts | 2 +- .../rules/Filter/RulesFilter.v1.tsx | 12 +- .../unified/rule-list/FilterView.test.tsx | 15 +- .../alerting/unified/rule-list/FilterView.tsx | 125 +++++---- .../unified/rule-list/FilterViewStatus.tsx | 41 +++ .../unified/rule-list/GrafanaRuleLoader.tsx | 31 ++- .../rule-list/PaginatedDataSourceLoader.tsx | 6 +- .../rule-list/PaginatedGrafanaLoader.tsx | 4 +- .../unified/rule-list/hooks/filters.test.ts | 257 ++++++++++++++++++ .../unified/rule-list/hooks/filters.ts | 144 ++++++++++ .../hooks/prometheusGroupsGenerator.ts | 23 +- .../hooks/useFilteredRulesIterator.ts | 195 ++++++------- .../hooks/usePaginatedPrometheusGroups.tsx | 2 +- .../features/alerting/unified/utils/rules.ts | 20 +- .../BrowseFolderAlertingPage.test.tsx | 2 +- .../fixtures/alertRules.fixture.ts | 10 +- .../PanelDataAlertingTab.test.tsx | 3 - .../DashboardQueryRunner.test.ts | 3 - public/app/types/unified-alerting-dto.ts | 31 ++- public/locales/en-US/grafana.json | 9 +- public/test/setupTests.ts | 42 +++ 26 files changed, 814 insertions(+), 243 deletions(-) create mode 100644 public/app/features/alerting/unified/rule-list/FilterViewStatus.tsx create mode 100644 public/app/features/alerting/unified/rule-list/hooks/filters.test.ts create mode 100644 public/app/features/alerting/unified/rule-list/hooks/filters.ts diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index 1ba3422cecb..f1429bb01af 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -368,6 +368,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { "folderUid": "namespaceUID", "uid": "RuleUID", "query": "vector(1)", + "queriedDatasourceUIDs": ["AUID"], "alerts": [{ "labels": { "job": "prometheus" @@ -433,6 +434,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { "state": "inactive", "name": "AlwaysFiring", "query": "vector(1)", + "queriedDatasourceUIDs": ["AUID"], "folderUid": "namespaceUID", "uid": "RuleUID", "alerts": [{ @@ -499,6 +501,7 @@ func TestRouteGetRuleStatuses(t *testing.T) { "state": "inactive", "name": "AlwaysFiring", "query": "vector(1) | vector(1)", + "queriedDatasourceUIDs": ["AUID", "BUID"], "folderUid": "namespaceUID", "uid": "RuleUID", "alerts": [{ diff --git a/pkg/services/ngalert/api/prometheus/api_prometheus.go b/pkg/services/ngalert/api/prometheus/api_prometheus.go index d130274ced0..076fa35a171 100644 --- a/pkg/services/ngalert/api/prometheus/api_prometheus.go +++ b/pkg/services/ngalert/api/prometheus/api_prometheus.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/folder" @@ -544,13 +545,16 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, sr StatusRe } } + queriedDatasourceUIDs := extractDatasourceUIDs(rule) + alertingRule := apimodels.AlertingRule{ - State: "inactive", - Name: rule.Title, - Query: ruleToQuery(log, rule), - Duration: rule.For.Seconds(), - KeepFiringFor: rule.KeepFiringFor.Seconds(), - Annotations: apimodels.LabelsFromMap(rule.Annotations), + State: "inactive", + Name: rule.Title, + Query: ruleToQuery(log, rule), + QueriedDatasourceUIDs: queriedDatasourceUIDs, + Duration: rule.For.Seconds(), + KeepFiringFor: rule.KeepFiringFor.Seconds(), + Annotations: apimodels.LabelsFromMap(rule.Annotations), } newRule := apimodels.Rule{ @@ -663,6 +667,19 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, sr StatusRe return newGroup, rulesTotals } +// extractDatasourceUIDs extracts datasource UIDs from a rule +func extractDatasourceUIDs(rule *ngmodels.AlertRule) []string { + queriedDatasourceUIDs := make([]string, 0, len(rule.Data)) + for _, query := range rule.Data { + // Skip expression datasources (UID -100 or __expr__) + if expr.IsDataSource(query.DatasourceUID) { + continue + } + queriedDatasourceUIDs = append(queriedDatasourceUIDs, query.DatasourceUID) + } + return queriedDatasourceUIDs +} + // ruleToQuery attempts to extract the datasource queries from the alert query model. // Returns the whole JSON model as a string if it fails to extract a minimum of 1 query. func ruleToQuery(logger log.Logger, rule *ngmodels.AlertRule) string { diff --git a/pkg/services/ngalert/api/tooling/definitions/prom.go b/pkg/services/ngalert/api/tooling/definitions/prom.go index 108d5e3cf0b..20130c9f331 100644 --- a/pkg/services/ngalert/api/tooling/definitions/prom.go +++ b/pkg/services/ngalert/api/tooling/definitions/prom.go @@ -152,9 +152,10 @@ type AlertingRule struct { // required: true Name string `json:"name,omitempty"` // required: true - Query string `json:"query,omitempty"` - Duration float64 `json:"duration,omitempty"` - KeepFiringFor float64 `json:"keepFiringFor,omitempty"` + Query string `json:"query,omitempty"` + QueriedDatasourceUIDs []string `json:"queriedDatasourceUIDs,omitempty"` + Duration float64 `json:"duration,omitempty"` + KeepFiringFor float64 `json:"keepFiringFor,omitempty"` // required: true Annotations promlabels.Labels `json:"annotations,omitempty"` // required: true @@ -168,12 +169,10 @@ type AlertingRule struct { // adapted from cortex // swagger:model type Rule struct { + UID string `json:"uid,omitempty"` // required: true - UID string `json:"uid"` - // required: true - Name string `json:"name"` - // required: true - FolderUID string `json:"folderUid"` + Name string `json:"name"` + FolderUID string `json:"folderUid,omitempty"` // required: true Query string `json:"query"` Labels promlabels.Labels `json:"labels,omitempty"` diff --git a/public/app/features/alerting/unified/Analytics.ts b/public/app/features/alerting/unified/Analytics.ts index e5f0ce7e597..ee7ba077400 100644 --- a/public/app/features/alerting/unified/Analytics.ts +++ b/public/app/features/alerting/unified/Analytics.ts @@ -34,23 +34,27 @@ const { logInfo, logError, logMeasurement, logWarning } = createMonitoringLogger export { logError, logInfo, logMeasurement, logWarning }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function withPerformanceLogging Promise>( - type: string, - func: TFunc, - context: Record -): (...args: Parameters) => Promise>> { - return async function (...args) { - const startLoadingTs = performance.now(); +/** + * Utility function to measure performance of async operations + * @param func Function to measure + * @param measurementName Name of the measurement for logging + * @param context Context for logging + */ +export function withPerformanceLogging( + func: (...args: TArgs) => Promise, + measurementName: string, + context: Record = {} +): (...args: TArgs) => Promise { + return async function (...args: TArgs): Promise { + const startMark = `${measurementName}:start`; + performance.mark(startMark); const response = await func(...args); - const loadTimesMs = performance.now() - startLoadingTs; + const loadTimeMeasure = performance.measure(measurementName, startMark); logMeasurement( - type, - { - loadTimesMs, - }, + measurementName, + { duration: loadTimeMeasure.duration, loadTimesMs: loadTimeMeasure.duration }, context ); diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx index d48f5de0341..9e01d77d060 100644 --- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx +++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx @@ -117,9 +117,6 @@ const promResponse: PromRulesResponse = { interval: 20, }, ], - totals: { - alerting: 2, - }, }, }; const rulerResponse = { diff --git a/public/app/features/alerting/unified/api/alertmanagerApi.ts b/public/app/features/alerting/unified/api/alertmanagerApi.ts index a6d1a718d83..0d57ea8b9ee 100644 --- a/public/app/features/alerting/unified/api/alertmanagerApi.ts +++ b/public/app/features/alerting/unified/api/alertmanagerApi.ts @@ -197,8 +197,8 @@ export const alertmanagerApi = alertingApi.injectEndpoints({ // wrap our fetchConfig function with some performance logging functions const fetchAMconfigWithLogging = withPerformanceLogging( - 'unifiedalerting/fetchAmConfig', fetchAlertManagerConfig, + 'unifiedalerting/fetchAmConfig', { dataSourceName: alertmanagerSourceName, thunk: 'unifiedalerting/fetchAmConfig', diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx index f2848617a65..d2039aa1d96 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx +++ b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx @@ -18,6 +18,7 @@ import { trackRulesSearchComponentInteraction, trackRulesSearchInputInteraction, } from '../../../Analytics'; +import { shouldUseAlertingListViewV2 } from '../../../featureToggles'; import { useRulesFilter } from '../../../hooks/useFilteredRules'; import { useAlertingHomePageExtensions } from '../../../plugins/useAlertingHomePageExtensions'; import { RuleHealth } from '../../../search/rulesSearchParser'; @@ -40,6 +41,13 @@ const RuleHealthOptions: SelectableValue[] = [ { label: 'Error', value: RuleHealth.Error }, ]; +// Contact point selector is not supported in Alerting ListView V2 yet +const canRenderContactPointSelector = + (contextSrv.hasPermission(AccessControlAction.AlertingReceiversRead) && + config.featureToggles.alertingSimplifiedRouting && + shouldUseAlertingListViewV2() === false) ?? + false; + interface RulesFilerProps { onClear?: () => void; } @@ -122,10 +130,6 @@ const RulesFilter = ({ onClear = () => undefined }: RulesFilerProps) => { trackRulesSearchComponentInteraction('contactPoint'); }; - const canRenderContactPointSelector = - (contextSrv.hasPermission(AccessControlAction.AlertingReceiversRead) && - config.featureToggles.alertingSimplifiedRouting) ?? - false; const searchIcon = ; return ( diff --git a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx index 307954bf189..b9120e57eb1 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.test.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.test.tsx @@ -40,17 +40,18 @@ beforeEach(() => { const io = mockIntersectionObserver(); describe('RuleList - FilterView', () => { - jest.setTimeout(60 * 1000); - jest.retryTimes(2); - it('should render multiple pages of results', async () => { render(); await loadMoreResults(); - expect(await screen.findAllByRole('treeitem')).toHaveLength(100); + const onePageResults = await screen.findAllByRole('treeitem'); + // FilterView loads rules in batches so it can load more than 100 rules for one page + expect(onePageResults.length).toBeGreaterThanOrEqual(100); await loadMoreResults(); - expect(await screen.findAllByRole('treeitem')).toHaveLength(200); + const twoPageResults = await screen.findAllByRole('treeitem'); + expect(twoPageResults.length).toBeGreaterThanOrEqual(200); + expect(twoPageResults.length).toBeGreaterThan(onePageResults.length); }); it('should filter results by group and rule name ', async () => { @@ -89,7 +90,7 @@ describe('RuleList - FilterView', () => { expect(matchingPrometheusRule).toBeInTheDocument(); expect(await screen.findByText(/No more results/)).toBeInTheDocument(); - }, 90000); + }); it('should display empty state when no rules are found', async () => { render(); @@ -104,7 +105,7 @@ async function loadMoreResults() { act(() => { io.enterNode(screen.getByTestId('load-more-helper')); }); - await waitForElementToBeRemoved(screen.queryAllByTestId('alert-rule-list-item-loader'), { timeout: 80000 }); + await waitForElementToBeRemoved(screen.queryAllByTestId('alert-rule-list-item-loader')); } function getFilter(overrides: Partial = {}): RulesFilter { diff --git a/public/app/features/alerting/unified/rule-list/FilterView.tsx b/public/app/features/alerting/unified/rule-list/FilterView.tsx index 900106ff86d..1e7253457b9 100644 --- a/public/app/features/alerting/unified/rule-list/FilterView.tsx +++ b/public/app/features/alerting/unified/rule-list/FilterView.tsx @@ -1,15 +1,17 @@ -import { empty } from 'ix/asynciterable'; -import { catchError, take, tap, withAbort } from 'ix/asynciterable/operators'; -import { useEffect, useRef, useState, useTransition } from 'react'; +import { bufferCountOrTime, tap } from 'ix/asynciterable/operators'; +import { useCallback, useMemo, useRef, useState, useTransition } from 'react'; +import { useUnmount } from 'react-use'; -import { Card, EmptyState, Stack, Text } from '@grafana/ui'; +import { EmptyState, Stack } from '@grafana/ui'; import { Trans, t } from 'app/core/internationalization'; +import { withPerformanceLogging } from '../Analytics'; import { isLoading, useAsync } from '../hooks/useAsync'; import { RulesFilter } from '../search/rulesSearchParser'; import { hashRule } from '../utils/rule-id'; import { DataSourceRuleLoader } from './DataSourceRuleLoader'; +import { FilterProgressState, FilterStatus } from './FilterViewStatus'; import { GrafanaRuleLoader } from './GrafanaRuleLoader'; import LoadMoreHelper from './LoadMoreHelper'; import { UnknownRuleListItem } from './components/AlertRuleListItem'; @@ -54,54 +56,85 @@ function FilterViewResults({ filterState }: FilterViewProps) { const [transitionPending, startTransition] = useTransition(); /* this hook returns a function that creates an AsyncIterable which we will use to populate the front-end */ - const { getFilteredRulesIterator } = useFilteredRulesIteratorProvider(); + const getFilteredRulesIterator = useFilteredRulesIteratorProvider(); - /* this is the abort controller that allows us to stop an AsyncIterable */ - const controller = useRef(new AbortController()); - - /** - * This an iterator that we can use to populate the search results. - * It also uses the signal from the AbortController above to cancel retrieving more results and sets up a - * callback function to detect when we've exhausted the source. - * This is the main AsyncIterable we will use for the search results */ - const rulesIterator = useRef( - getFilteredRulesIterator(filterState, API_PAGE_SIZE).pipe( - withAbort(controller.current.signal), - onFinished(() => setDoneSearching(true)) - ) - ); + const iteration = useRef<{ + rulesBatchIterator: AsyncIterator; + abortController: AbortController; + } | null>(null); const [rules, setRules] = useState([]); const [doneSearching, setDoneSearching] = useState(false); - /* This function will fetch a page of results from the iterable */ - const [{ execute: loadResultPage }, state] = useAsync(async () => { - for await (const rule of rulesIterator.current.pipe( - // grab from the rules iterable - take(FRONTENT_PAGE_SIZE), - // if an error occurs trying to fetch a page, return an empty iterable so the front-end isn't caught in an infinite loop - catchError(() => empty()) - )) { - startTransition(() => { - // Rule key could be computed on the fly, but we do it here to avoid recalculating it with each render - // It's a not trivial computation because it involves hashing the rule - setRules((rules) => rules.concat({ key: getRuleKey(rule), ...rule })); - }); + // Lazy initialization of useRef + // https://18.react.dev/reference/react/useRef#how-to-avoid-null-checks-when-initializing-use-ref-later + const getRulesBatchIterator = useCallback(() => { + if (!iteration.current) { + /** + * This an iterator that we can use to populate the search results. + * It also uses the signal from the AbortController above to cancel retrieving more results and sets up a + * callback function to detect when we've exhausted the source. + * This is the main AsyncIterable we will use for the search results + * + * ⚠️ Make sure we are returning / using a "iterator" and not an "iterable" since the iterable is only a blueprint + * and the iterator will allow us to exhaust the iterable in a stateful way + */ + const { iterable, abortController } = getFilteredRulesIterator(filterState, API_PAGE_SIZE); + const rulesBatchIterator = iterable + .pipe( + bufferCountOrTime(FRONTENT_PAGE_SIZE, 1000), + onFinished(() => setDoneSearching(true)) + ) + [Symbol.asyncIterator](); + iteration.current = { rulesBatchIterator: rulesBatchIterator, abortController }; } - }); + return iteration.current.rulesBatchIterator; + }, [filterState, getFilteredRulesIterator]); - /* When we unmount the component we make sure to abort all iterables */ - useEffect(() => { - const currentAbortController = controller.current; + /* This function will fetch a page of results from the iterable */ + const [{ execute: loadResultPage }, state] = useAsync( + withPerformanceLogging(async () => { + const rulesIterator = getRulesBatchIterator(); - return () => { - currentAbortController.abort(); - }; - }, [controller]); + let loadedRulesCount = 0; + + while (loadedRulesCount < FRONTENT_PAGE_SIZE) { + const nextRulesBatch = await rulesIterator.next(); + if (nextRulesBatch.done) { + return; + } + if (nextRulesBatch.value) { + startTransition(() => { + setRules((rules) => rules.concat(nextRulesBatch.value.map((rule) => ({ key: getRuleKey(rule), ...rule })))); + }); + } + loadedRulesCount += nextRulesBatch.value.length; + } + }, 'alerting.rule-list.filter-view.load-result-page') + ); const loading = isLoading(state) || transitionPending; const numberOfRules = rules.length; const noRulesFound = numberOfRules === 0 && !loading; + const loadingAborted = iteration.current?.abortController.signal.aborted; + const cancelSearch = useCallback(() => { + iteration.current?.abortController.abort(); + }, []); + + /* When we unmount the component we make sure to abort all iterables and stop making HTTP requests */ + useUnmount(() => { + cancelSearch(); + }); + + // track the state of the filter progress, which is either searching, done or aborted + const filterProgressState = useMemo(() => { + if (loadingAborted) { + return 'aborted'; + } else if (doneSearching) { + return 'done'; + } + return 'searching'; + }, [doneSearching, loadingAborted]); /* If we don't have any rules and have exhausted all sources, show a EmptyState */ if (noRulesFound && doneSearching) { @@ -150,16 +183,10 @@ function FilterViewResults({ filterState }: FilterViewProps) { )} - {doneSearching && !noRulesFound && ( - - - - No more results – showing {{ numberOfRules }} rules - - - + {!noRulesFound && ( + )} - {!doneSearching && !loading && } + {!doneSearching && !loading && !loadingAborted && } ); } diff --git a/public/app/features/alerting/unified/rule-list/FilterViewStatus.tsx b/public/app/features/alerting/unified/rule-list/FilterViewStatus.tsx new file mode 100644 index 00000000000..c72a5a72ce8 --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/FilterViewStatus.tsx @@ -0,0 +1,41 @@ +import { Button, Card, Text } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; + +export type FilterProgressState = 'searching' | 'done' | 'aborted'; +interface FilterStatusProps { + numberOfRules: number; + state: FilterProgressState; + onCancel: () => void; +} + +export function FilterStatus({ state, numberOfRules, onCancel }: FilterStatusProps) { + return ( + + + {/* done searching everything and found some results */} + {state === 'done' && ( + + No more results – found {{ numberOfRules }} rules + + )} + {/* user has cancelled the search */} + {state === 'aborted' && ( + + Search cancelled – found {{ numberOfRules }} rules + + )} + {/* search is in progress */} + {state === 'searching' && ( + + Searching – found {{ numberOfRules }} rules + + )} + + {state === 'searching' && ( + + )} + + ); +} diff --git a/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx b/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx index 7444e303fb8..5cb1b7ebf83 100644 --- a/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/GrafanaRuleLoader.tsx @@ -1,3 +1,5 @@ +import { Alert } from '@grafana/ui'; +import { Trans, t } from 'app/core/internationalization'; import { GrafanaRuleGroupIdentifier } from 'app/types/unified-alerting'; import { GrafanaPromRuleDTO, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto'; @@ -26,21 +28,40 @@ interface GrafanaRuleLoaderProps { } export function GrafanaRuleLoader({ rule, groupIdentifier, namespaceName }: GrafanaRuleLoaderProps) { - const { data: rulerRuleGroup, isError } = useGetGrafanaRulerGroupQuery({ + const { + data: rulerRuleGroup, + isError, + isLoading, + } = useGetGrafanaRulerGroupQuery({ folderUid: groupIdentifier.namespace.uid, groupName: groupIdentifier.groupName, }); const rulerRule = rulerRuleGroup?.rules.find((rulerRule) => rulerRule.grafana_alert.uid === rule.uid); - if (!rulerRule) { - if (isError) { - return ; - } + if (isError) { + return ; + } + if (isLoading) { return ; } + if (!rulerRule) { + return ( + + + Cannot find rule details for {{ uid: rule.uid ?? '' }} + + + ); + } + return ( { const currentGenerator = groupsGenerator.current; diff --git a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx index 59ecd1f31ac..755937c2773 100644 --- a/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx +++ b/public/app/features/alerting/unified/rule-list/PaginatedGrafanaLoader.tsx @@ -14,7 +14,7 @@ import { LazyPagination } from './components/LazyPagination'; import { ListGroup } from './components/ListGroup'; import { ListSection } from './components/ListSection'; import { RuleGroupActionsMenu } from './components/RuleGroupActionsMenu'; -import { useGrafanaGroupsGenerator } from './hooks/prometheusGroupsGenerator'; +import { toIndividualRuleGroups, useGrafanaGroupsGenerator } from './hooks/prometheusGroupsGenerator'; import { usePaginatedPrometheusGroups } from './hooks/usePaginatedPrometheusGroups'; const GRAFANA_GROUP_PAGE_SIZE = 40; @@ -22,7 +22,7 @@ const GRAFANA_GROUP_PAGE_SIZE = 40; export function PaginatedGrafanaLoader() { const grafanaGroupsGenerator = useGrafanaGroupsGenerator({ populateCache: true }); - const groupsGenerator = useRef(grafanaGroupsGenerator(GRAFANA_GROUP_PAGE_SIZE)); + const groupsGenerator = useRef(toIndividualRuleGroups(grafanaGroupsGenerator(GRAFANA_GROUP_PAGE_SIZE))); useEffect(() => { const currentGenerator = groupsGenerator.current; diff --git a/public/app/features/alerting/unified/rule-list/hooks/filters.test.ts b/public/app/features/alerting/unified/rule-list/hooks/filters.test.ts new file mode 100644 index 00000000000..18e4af4f40f --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/hooks/filters.test.ts @@ -0,0 +1,257 @@ +import { PromAlertingRuleState, PromRuleGroupDTO, PromRuleType } from 'app/types/unified-alerting-dto'; + +import { mockGrafanaPromAlertingRule, mockPromAlertingRule, mockPromRecordingRule } from '../../mocks'; +import { RuleHealth } from '../../search/rulesSearchParser'; +import { Annotation } from '../../utils/constants'; +import * as datasourceUtils from '../../utils/datasource'; +import { getFilter } from '../../utils/search'; + +import { groupFilter, ruleFilter } from './filters'; + +describe('groupFilter', () => { + it('should filter by namespace (file path)', () => { + const group: PromRuleGroupDTO = { + name: 'Test Group', + file: 'production/alerts', + rules: [], + interval: 60, + }; + + expect(groupFilter(group, getFilter({ namespace: 'production' }))).toBe(true); + expect(groupFilter(group, getFilter({ namespace: 'staging' }))).toBe(false); + }); + + it('should filter by group name', () => { + const group: PromRuleGroupDTO = { + name: 'CPU Usage Alerts', + file: 'production/alerts', + rules: [], + interval: 60, + }; + + expect(groupFilter(group, getFilter({ groupName: 'cpu' }))).toBe(true); + expect(groupFilter(group, getFilter({ groupName: 'memory' }))).toBe(false); + }); + + it('should return true when no filters are applied', () => { + const group: PromRuleGroupDTO = { + name: 'Test Group', + file: 'production/alerts', + rules: [], + interval: 60, + }; + + expect(groupFilter(group, getFilter({}))).toBe(true); + }); +}); + +describe('ruleFilter', () => { + it('should filter by free form words in rule name', () => { + const rule = mockPromAlertingRule({ name: 'High CPU Usage' }); + + expect(ruleFilter(rule, getFilter({ freeFormWords: ['cpu'] }))).toBe(true); + expect(ruleFilter(rule, getFilter({ freeFormWords: ['memory'] }))).toBe(false); + }); + + it('should filter by rule name', () => { + const rule = mockPromAlertingRule({ name: 'High CPU Usage' }); + + expect(ruleFilter(rule, getFilter({ ruleName: 'cpu' }))).toBe(true); + expect(ruleFilter(rule, getFilter({ ruleName: 'memory' }))).toBe(false); + }); + + it('should filter by labels', () => { + const rule = mockPromAlertingRule({ + labels: { severity: 'critical', team: 'ops' }, + alerts: [], + }); + + expect(ruleFilter(rule, getFilter({ labels: ['severity=critical'] }))).toBe(true); + expect(ruleFilter(rule, getFilter({ labels: ['severity=warning'] }))).toBe(false); + expect(ruleFilter(rule, getFilter({ labels: ['team=ops'] }))).toBe(true); + }); + + it('should filter by alert instance labels', () => { + const rule = mockPromAlertingRule({ + labels: { severity: 'critical' }, + alerts: [ + { + labels: { instance: 'server-1', env: 'production' }, + state: PromAlertingRuleState.Firing, + value: '100', + activeAt: '', + annotations: {}, + }, + ], + }); + + expect(ruleFilter(rule, getFilter({ labels: ['instance=server-1'] }))).toBe(true); + expect(ruleFilter(rule, getFilter({ labels: ['env=production'] }))).toBe(true); + expect(ruleFilter(rule, getFilter({ labels: ['instance=server-2'] }))).toBe(false); + }); + + it('should filter by rule type', () => { + const alertingRule = mockPromAlertingRule({ name: 'Test Alert' }); + const recordingRule = mockPromRecordingRule({ name: 'Test Recording' }); + + expect(ruleFilter(alertingRule, getFilter({ ruleType: PromRuleType.Alerting }))).toBe(true); + expect(ruleFilter(alertingRule, getFilter({ ruleType: PromRuleType.Recording }))).toBe(false); + expect(ruleFilter(recordingRule, getFilter({ ruleType: PromRuleType.Recording }))).toBe(true); + expect(ruleFilter(recordingRule, getFilter({ ruleType: PromRuleType.Alerting }))).toBe(false); + }); + + it('should filter by rule state', () => { + const firingRule = mockPromAlertingRule({ + name: 'Firing Alert', + state: PromAlertingRuleState.Firing, + }); + + const pendingRule = mockPromAlertingRule({ + name: 'Pending Alert', + state: PromAlertingRuleState.Pending, + }); + + expect(ruleFilter(firingRule, getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(true); + expect(ruleFilter(firingRule, getFilter({ ruleState: PromAlertingRuleState.Pending }))).toBe(false); + expect(ruleFilter(pendingRule, getFilter({ ruleState: PromAlertingRuleState.Pending }))).toBe(true); + }); + + it('should filter out recording rules when filtering by rule state', () => { + const recordingRule = mockPromRecordingRule({ + name: 'Recording Rule', + }); + + // Recording rules should always be filtered out when any rule state filter is applied as they don't have a state + expect(ruleFilter(recordingRule, getFilter({ ruleState: PromAlertingRuleState.Firing }))).toBe(false); + expect(ruleFilter(recordingRule, getFilter({ ruleState: PromAlertingRuleState.Pending }))).toBe(false); + expect(ruleFilter(recordingRule, getFilter({ ruleState: PromAlertingRuleState.Inactive }))).toBe(false); + }); + + it('should filter by rule health', () => { + const healthyRule = mockPromAlertingRule({ + name: 'Healthy Rule', + health: RuleHealth.Ok, + }); + + const errorRule = mockPromAlertingRule({ + name: 'Error Rule', + health: RuleHealth.Error, + }); + + expect(ruleFilter(healthyRule, getFilter({ ruleHealth: RuleHealth.Ok }))).toBe(true); + expect(ruleFilter(healthyRule, getFilter({ ruleHealth: RuleHealth.Error }))).toBe(false); + expect(ruleFilter(errorRule, getFilter({ ruleHealth: RuleHealth.Error }))).toBe(true); + }); + + it('should filter by dashboard UID', () => { + const ruleDashboardA = mockPromAlertingRule({ + name: 'Dashboard A Rule', + annotations: { [Annotation.dashboardUID]: 'dashboard-a' }, + }); + + const ruleDashboardB = mockPromAlertingRule({ + name: 'Dashboard B Rule', + annotations: { [Annotation.dashboardUID]: 'dashboard-b' }, + }); + + expect(ruleFilter(ruleDashboardA, getFilter({ dashboardUid: 'dashboard-a' }))).toBe(true); + expect(ruleFilter(ruleDashboardA, getFilter({ dashboardUid: 'dashboard-b' }))).toBe(false); + expect(ruleFilter(ruleDashboardB, getFilter({ dashboardUid: 'dashboard-b' }))).toBe(true); + }); + + it('should filter out recording rules when filtering by dashboard UID', () => { + const recordingRule = mockPromRecordingRule({ + name: 'Recording Rule', + // Recording rules cannot have dashboard UIDs because they don't have annotations + }); + + // Dashboard UID filter should filter out recording rules + expect(ruleFilter(recordingRule, getFilter({ dashboardUid: 'any-dashboard' }))).toBe(false); + }); + + describe('dataSourceNames filter', () => { + let getDataSourceUIDSpy: jest.SpyInstance; + + beforeEach(() => { + getDataSourceUIDSpy = jest.spyOn(datasourceUtils, 'getDatasourceAPIUid').mockImplementation((ruleSourceName) => { + if (ruleSourceName === 'prometheus') { + return 'datasource-uid-1'; + } + if (ruleSourceName === 'loki') { + return 'datasource-uid-3'; + } + throw new Error(`Unknown datasource name: ${ruleSourceName}`); + }); + }); + + afterEach(() => { + // Clean up + getDataSourceUIDSpy.mockRestore(); + }); + + it('should match rules that use the filtered datasource', () => { + // Create a Grafana rule with matching datasource + const ruleWithMatchingDatasource = mockGrafanaPromAlertingRule({ + queriedDatasourceUIDs: ['datasource-uid-1'], + }); + + // 'prometheus' resolves to 'datasource-uid-1' which is in the rule + expect(ruleFilter(ruleWithMatchingDatasource, getFilter({ dataSourceNames: ['prometheus'] }))).toBe(true); + }); + + it("should filter out rules that don't use the filtered datasource", () => { + // Create a Grafana rule without the target datasource + const ruleWithoutMatchingDatasource = mockGrafanaPromAlertingRule({ + queriedDatasourceUIDs: ['datasource-uid-1', 'datasource-uid-2'], + }); + + // 'loki' resolves to 'datasource-uid-3' which is not in the rule + expect(ruleFilter(ruleWithoutMatchingDatasource, getFilter({ dataSourceNames: ['loki'] }))).toBe(false); + }); + + it('should return false when there is an error parsing the query', () => { + const ruleWithInvalidQuery = mockGrafanaPromAlertingRule({ + query: 'not-valid-json', + }); + + expect(ruleFilter(ruleWithInvalidQuery, getFilter({ dataSourceNames: ['prometheus'] }))).toBe(false); + }); + }); + + it('should combine multiple filters with AND logic', () => { + const rule = mockPromAlertingRule({ + name: 'High CPU Usage Production', + labels: { severity: 'critical', environment: 'production' }, + state: PromAlertingRuleState.Firing, + health: RuleHealth.Ok, + }); + + const filter = getFilter({ + ruleName: 'cpu', + labels: ['severity=critical', 'environment=production'], + ruleState: PromAlertingRuleState.Firing, + ruleHealth: RuleHealth.Ok, + }); + + expect(ruleFilter(rule, filter)).toBe(true); + }); + + it('should return false if any filter does not match', () => { + const rule = mockPromAlertingRule({ + name: 'High CPU Usage Production', + labels: { severity: 'critical', environment: 'production' }, + state: PromAlertingRuleState.Firing, + health: RuleHealth.Ok, + alerts: [], + }); + + const filter = getFilter({ + ruleName: 'cpu', + labels: ['severity=warning'], + ruleState: PromAlertingRuleState.Firing, + ruleHealth: RuleHealth.Ok, + }); + + expect(ruleFilter(rule, filter)).toBe(false); + }); +}); diff --git a/public/app/features/alerting/unified/rule-list/hooks/filters.ts b/public/app/features/alerting/unified/rule-list/hooks/filters.ts new file mode 100644 index 00000000000..89eb4c96f6b --- /dev/null +++ b/public/app/features/alerting/unified/rule-list/hooks/filters.ts @@ -0,0 +1,144 @@ +import { attempt, compact, isString } from 'lodash'; +import memoize from 'micro-memoize'; + +import { Matcher } from 'app/plugins/datasource/alertmanager/types'; +import { PromRuleDTO, PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; + +import { RulesFilter } from '../../search/rulesSearchParser'; +import { labelsMatchMatchers } from '../../utils/alertmanager'; +import { Annotation } from '../../utils/constants'; +import { getDatasourceAPIUid } from '../../utils/datasource'; +import { parseMatcher } from '../../utils/matchers'; +import { isPluginProvidedRule, prometheusRuleType } from '../../utils/rules'; + +/** + * @returns True if the group matches the filter, false otherwise. Keeps rules intact + */ +export function groupFilter(group: PromRuleGroupDTO, filterState: RulesFilter): boolean { + const { name, file } = group; + + // Add fuzzy search for namespace + if (filterState.namespace && !file.toLowerCase().includes(filterState.namespace)) { + return false; + } + + // Add fuzzy search for group name + if (filterState.groupName && !name.toLowerCase().includes(filterState.groupName)) { + return false; + } + + return true; +} + +/** + * @returns True if the rule matches the filter, false otherwise + */ +export function ruleFilter(rule: PromRuleDTO, filterState: RulesFilter) { + const { name, labels = {}, health, type } = rule; + + const nameLower = name.toLowerCase(); + + // Free form words filter (matches if any word is part of the rule name) + if (filterState.freeFormWords.length > 0 && !filterState.freeFormWords.some((word) => nameLower.includes(word))) { + return false; + } + + // Rule name filter (exact match) + if (filterState.ruleName && !nameLower.includes(filterState.ruleName)) { + return false; + } + + // Labels filter + if (filterState.labels.length > 0) { + const matchers = compact(filterState.labels.map(looseParseMatcher)); + const doRuleLabelsMatchQuery = matchers.length > 0 && labelsMatchMatchers(labels, matchers); + + // Also check alerts if they exist + const doAlertsContainMatchingLabels = + matchers.length > 0 && + prometheusRuleType.alertingRule(rule) && + rule.alerts && + rule.alerts.some((alert) => labelsMatchMatchers(alert.labels || {}, matchers)); + + if (!doRuleLabelsMatchQuery && !doAlertsContainMatchingLabels) { + return false; + } + } + + // Rule type filter + if (filterState.ruleType && type !== filterState.ruleType) { + return false; + } + + // Rule state filter (for alerting rules only) + if (filterState.ruleState) { + if (!prometheusRuleType.alertingRule(rule)) { + return false; + } + if (rule.state !== filterState.ruleState) { + return false; + } + } + + // Rule health filter + if (filterState.ruleHealth && health !== filterState.ruleHealth) { + return false; + } + + // Dashboard UID filter + if (filterState.dashboardUid) { + if (!prometheusRuleType.alertingRule(rule)) { + return false; + } + + const dashboardAnnotation = rule.annotations?.[Annotation.dashboardUID]; + if (dashboardAnnotation !== filterState.dashboardUid) { + return false; + } + } + + // Plugins filter - hide plugin-provided rules when set to 'hide' + if (filterState.plugins === 'hide' && isPluginProvidedRule(rule)) { + return false; + } + + // Note: We can't implement these filters from reduceGroups because they rely on rulerRule property + // which is not available in PromRuleDTO: + // - contactPoint filter + // - dataSourceNames filter + if (filterState.dataSourceNames.length > 0) { + const isGrafanaRule = prometheusRuleType.grafana.rule(rule); + if (isGrafanaRule) { + try { + const filterDatasourceUids = mapDataSourceNamesToUids(filterState.dataSourceNames); + const queriedDatasourceUids = rule.queriedDatasourceUIDs || []; + + const queryIncludesDataSource = queriedDatasourceUids.some((uid) => filterDatasourceUids.includes(uid)); + if (!queryIncludesDataSource) { + return false; + } + } catch (error) { + return false; + } + } + } + + return true; +} + +function looseParseMatcher(matcherQuery: string): Matcher | undefined { + try { + return parseMatcher(matcherQuery); + } catch { + // Try to createa a matcher than matches all values for a given key + return { name: matcherQuery, value: '', isRegex: true, isEqual: true }; + } +} + +// Memoize the function to avoid calling getDatasourceAPIUid for the filter values multiple times +const mapDataSourceNamesToUids = memoize( + (names: string[]): string[] => { + return names.map((name) => attempt(getDatasourceAPIUid, name)).filter(isString); + }, + { maxSize: 1 } +); diff --git a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts index 39339caa91e..2087e31c1c4 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/prometheusGroupsGenerator.ts @@ -2,6 +2,7 @@ import { useCallback } from 'react'; import { useDispatch } from 'app/types/store'; import { DataSourceRulesSourceIdentifier } from 'app/types/unified-alerting'; +import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto'; import { alertRuleApi } from '../../api/alertRuleApi'; import { PromRulesResponse, prometheusApi } from '../../api/prometheusApi'; @@ -95,6 +96,23 @@ export function useGrafanaGroupsGenerator(hookOptions: UseGeneratorHookOptions = ); } +/** + * Converts a Prometheus groups generator yielding arrays of groups to a generator yielding groups one by one + * @param generator - The paginated generator to convert + * @returns A non-paginated generator that yields all groups from the original generator one by one + */ +export function toIndividualRuleGroups( + generator: AsyncGenerator +): AsyncGenerator { + return (async function* () { + for await (const batch of generator) { + for (const item of batch) { + yield item; + } + } + })(); +} + // Generator lazily provides groups one by one only when needed // This might look a bit complex but it allows us to have one API for paginated and non-paginated Prometheus data sources // For unpaginated data sources we fetch everything in one go @@ -104,14 +122,13 @@ async function* genericGroupsGenerator( groupLimit: number ) { let response = await fetchGroups({ groupLimit }); - yield* response.data.groups; + yield response.data.groups; let lastToken: string | undefined = response.data?.groupNextToken; while (lastToken) { response = await fetchGroups({ groupNextToken: lastToken, groupLimit: groupLimit }); - - yield* response.data.groups; + yield response.data.groups; lastToken = response.data?.groupNextToken; } } diff --git a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts index 8c494c0545a..50be4d9054d 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts +++ b/public/app/features/alerting/unified/rule-list/hooks/useFilteredRulesIterator.ts @@ -1,9 +1,8 @@ import { AsyncIterableX, empty, from } from 'ix/asynciterable'; import { merge } from 'ix/asynciterable/merge'; -import { catchError, filter, flatMap, map } from 'ix/asynciterable/operators'; -import { compact } from 'lodash'; +import { catchError, concatMap, withAbort } from 'ix/asynciterable/operators'; +import { isEmpty } from 'lodash'; -import { Matcher } from 'app/plugins/datasource/alertmanager/types'; import { DataSourceRuleGroupIdentifier, DataSourceRulesSourceIdentifier, @@ -17,12 +16,14 @@ import { } from 'app/types/unified-alerting-dto'; import { RulesFilter } from '../../search/rulesSearchParser'; -import { labelsMatchMatchers } from '../../utils/alertmanager'; -import { Annotation } from '../../utils/constants'; -import { getDatasourceAPIUid, getExternalRulesSources } from '../../utils/datasource'; -import { parseMatcher } from '../../utils/matchers'; -import { prometheusRuleType } from '../../utils/rules'; +import { + getDataSourceByUid, + getDatasourceAPIUid, + getExternalRulesSources, + isSupportedExternalRulesSourceType, +} from '../../utils/datasource'; +import { groupFilter, ruleFilter } from './filters'; import { useGrafanaGroupsGenerator, usePrometheusGroupsGenerator } from './prometheusGroupsGenerator'; export type RuleWithOrigin = PromRuleWithOrigin | GrafanaRuleWithOrigin; @@ -44,54 +45,97 @@ export interface PromRuleWithOrigin { origin: 'datasource'; } +interface GetIteratorResult { + iterable: AsyncIterableX; + abortController: AbortController; +} + export function useFilteredRulesIteratorProvider() { const allExternalRulesSources = getExternalRulesSources(); const prometheusGroupsGenerator = usePrometheusGroupsGenerator(); const grafanaGroupsGenerator = useGrafanaGroupsGenerator(); - const getFilteredRulesIterator = (filterState: RulesFilter, groupLimit: number): AsyncIterableX => { + const getFilteredRulesIterable = (filterState: RulesFilter, groupLimit: number): GetIteratorResult => { + /* this is the abort controller that allows us to stop an AsyncIterable */ + const abortController = new AbortController(); + const normalizedFilterState = normalizeFilterState(filterState); + const hasDataSourceFilterActive = Boolean(filterState.dataSourceNames.length); - const ruleSourcesToFetchFrom = filterState.dataSourceNames.length - ? filterState.dataSourceNames.map((ds) => ({ - name: ds, - uid: getDatasourceAPIUid(ds), - ruleSourceType: 'datasource', - })) - : allExternalRulesSources; - - const grafanaIterator = from(grafanaGroupsGenerator(groupLimit)).pipe( - filter((group) => groupFilter(group, normalizedFilterState)), - flatMap((group) => group.rules.map((rule) => [group, rule] as const)), - filter(([_, rule]) => ruleFilter(rule, normalizedFilterState)), - map(([group, rule]) => mapGrafanaRuleToRuleWithOrigin(group, rule)), + const grafanaRulesGenerator = from(grafanaGroupsGenerator(groupLimit)).pipe( + withAbort(abortController.signal), + concatMap((groups) => + groups + .filter((group) => groupFilter(group, normalizedFilterState)) + .flatMap((group) => group.rules.map((rule) => [group, rule] as const)) + .filter(([, rule]) => ruleFilter(rule, normalizedFilterState)) + .map(([group, rule]) => mapGrafanaRuleToRuleWithOrigin(group, rule)) + ), catchError(() => empty()) ); - const sourceIterables = ruleSourcesToFetchFrom.map((ds) => { - const generator = prometheusGroupsGenerator(ds, groupLimit); - return from(generator).pipe( - map((group) => [ds, group] as const), + // Determine which data sources to use + const externalRulesSourcesToFetchFrom = hasDataSourceFilterActive + ? getRulesSourcesFromFilter(filterState) + : allExternalRulesSources; + + // If no data sources, just return Grafana rules + if (isEmpty(externalRulesSourcesToFetchFrom)) { + return { iterable: grafanaRulesGenerator, abortController }; + } + + // Create a generator for each data source + const dataSourceGenerators = externalRulesSourcesToFetchFrom.map((dataSourceIdentifier) => { + const promGroupsGenerator = from(prometheusGroupsGenerator(dataSourceIdentifier, groupLimit)).pipe( + withAbort(abortController.signal), + concatMap((groups) => + groups + .filter((group) => groupFilter(group, normalizedFilterState)) + .flatMap((group) => group.rules.map((rule) => [group, rule] as const)) + .filter(([, rule]) => ruleFilter(rule, normalizedFilterState)) + .map(([group, rule]) => mapRuleToRuleWithOrigin(dataSourceIdentifier, group, rule)) + ), catchError(() => empty()) ); + + return promGroupsGenerator; }); - // if we have no prometheus data sources, use an empty async iterable - const source = sourceIterables.at(0) ?? empty(); - const otherIterables = sourceIterables.slice(1); - - const dataSourcesIterator = merge(source, ...otherIterables).pipe( - filter(([_, group]) => groupFilter(group, normalizedFilterState)), - flatMap(([rulesSource, group]) => group.rules.map((rule) => [rulesSource, group, rule] as const)), - filter(([_, __, rule]) => ruleFilter(rule, filterState)), - map(([rulesSource, group, rule]) => mapRuleToRuleWithOrigin(rulesSource, group, rule)) - ); - - return merge(grafanaIterator, dataSourcesIterator); + // Merge all generators + return { + iterable: merge(grafanaRulesGenerator, ...dataSourceGenerators), + abortController, + }; }; - return { getFilteredRulesIterator }; + return getFilteredRulesIterable; +} + +/** + * Finds all data sources that the user might want to filter by. + * Only allows Prometheus and Loki data source types. + */ +function getRulesSourcesFromFilter(filter: RulesFilter): DataSourceRulesSourceIdentifier[] { + return filter.dataSourceNames.reduce((acc, dataSourceName) => { + // since "getDatasourceAPIUid" can throw we'll omit any non-existing data sources + try { + const uid = getDatasourceAPIUid(dataSourceName); + const type = getDataSourceByUid(uid)?.type; + + if (type === undefined || isSupportedExternalRulesSourceType(type) === false) { + return acc; + } + + acc.push({ + name: dataSourceName, + uid, + ruleSourceType: 'datasource', + }); + } catch {} + + return acc; + }, []); } function mapRuleToRuleWithOrigin( @@ -127,70 +171,6 @@ function mapGrafanaRuleToRuleWithOrigin( }; } -/** - * Returns a new group with only the rules that match the filter. - * @returns A new group with filtered rules, or undefined if the group does not match the filter or all rules are filtered out. - */ -function groupFilter(group: PromRuleGroupDTO, filterState: RulesFilter): boolean { - const { name, file } = group; - - // TODO Add fuzzy filtering or not - if (filterState.namespace && !file.toLowerCase().includes(filterState.namespace)) { - return false; - } - - if (filterState.groupName && !name.toLowerCase().includes(filterState.groupName)) { - return false; - } - - return true; -} - -function ruleFilter(rule: PromRuleDTO, filterState: RulesFilter) { - const { name, labels = {}, health, type } = rule; - - const nameLower = name.toLowerCase(); - - if (filterState.freeFormWords.length > 0 && !filterState.freeFormWords.some((word) => nameLower.includes(word))) { - return false; - } - - if (filterState.ruleName && !nameLower.includes(filterState.ruleName)) { - return false; - } - - if (filterState.labels.length > 0) { - const matchers = compact(filterState.labels.map(looseParseMatcher)); - const doRuleLabelsMatchQuery = matchers.length > 0 && labelsMatchMatchers(labels, matchers); - if (!doRuleLabelsMatchQuery) { - return false; - } - } - - if (filterState.ruleType && type !== filterState.ruleType) { - return false; - } - - if (filterState.ruleState) { - if (!prometheusRuleType.alertingRule(rule)) { - return false; - } - if (rule.state !== filterState.ruleState) { - return false; - } - } - - if (filterState.ruleHealth && health !== filterState.ruleHealth) { - return false; - } - - if (filterState.dashboardUid) { - return rule.labels ? rule.labels[Annotation.dashboardUID] === filterState.dashboardUid : false; - } - - return true; -} - /** * Lowercase free form words, rule name, group name and namespace */ @@ -203,12 +183,3 @@ function normalizeFilterState(filterState: RulesFilter): RulesFilter { namespace: filterState.namespace?.toLowerCase(), }; } - -function looseParseMatcher(matcherQuery: string): Matcher | undefined { - try { - return parseMatcher(matcherQuery); - } catch { - // Try to createa a matcher than matches all values for a given key - return { name: matcherQuery, value: '', isRegex: true, isEqual: true }; - } -} diff --git a/public/app/features/alerting/unified/rule-list/hooks/usePaginatedPrometheusGroups.tsx b/public/app/features/alerting/unified/rule-list/hooks/usePaginatedPrometheusGroups.tsx index 81e269396f4..18da7186fb5 100644 --- a/public/app/features/alerting/unified/rule-list/hooks/usePaginatedPrometheusGroups.tsx +++ b/public/app/features/alerting/unified/rule-list/hooks/usePaginatedPrometheusGroups.tsx @@ -14,7 +14,7 @@ import { isLoading, useAsync } from '../../hooks/useAsync'; * @returns Pagination state and controls for navigating through rule groups */ export function usePaginatedPrometheusGroups( - groupsGenerator: AsyncGenerator, + groupsGenerator: AsyncIterator, pageSize: number ) { const [currentPage, setCurrentPage] = useState(1); diff --git a/public/app/features/alerting/unified/utils/rules.ts b/public/app/features/alerting/unified/utils/rules.ts index c0eb608e2d1..88bc70a2cb8 100644 --- a/public/app/features/alerting/unified/utils/rules.ts +++ b/public/app/features/alerting/unified/utils/rules.ts @@ -26,9 +26,12 @@ import { GrafanaAlertState, GrafanaAlertStateWithReason, GrafanaAlertingRuleDefinition, + GrafanaPromAlertingRuleDTO, + GrafanaPromRecordingRuleDTO, GrafanaRecordingRuleDefinition, PostableRuleDTO, PromAlertingRuleState, + PromRuleDTO, PromRuleType, RulerAlertingRuleDTO, RulerCloudRuleDTO, @@ -97,6 +100,14 @@ function isRecordingRule(rule?: Rule): rule is RecordingRule { return typeof rule === 'object' && rule.type === PromRuleType.Recording; } +function isGrafanaPromAlertingRule(rule?: Rule): rule is GrafanaPromAlertingRuleDTO { + return isAlertingRule(rule) && 'folderUid' in rule && 'uid' in rule; +} + +function isGrafanaPromRecordingRule(rule?: Rule): rule is GrafanaPromRecordingRuleDTO { + return isRecordingRule(rule) && 'folderUid' in rule && 'uid' in rule; +} + export const rulerRuleType = { grafana: { rule: isGrafanaRulerRule, @@ -118,6 +129,11 @@ export const prometheusRuleType = { rule: (rule?: Rule) => isAlertingRule(rule) || isRecordingRule(rule), alertingRule: isAlertingRule, recordingRule: isRecordingRule, + grafana: { + rule: (rule?: Rule) => isGrafanaPromAlertingRule(rule) || isGrafanaPromRecordingRule(rule), + alertingRule: isGrafanaPromAlertingRule, + recordingRule: isGrafanaPromRecordingRule, + }, }; export function alertInstanceKey(alert: Alert): string { @@ -212,7 +228,7 @@ export interface RulePluginOrigin { pluginId: string; } -export function getRulePluginOrigin(rule?: Rule | RulerRuleDTO): RulePluginOrigin | undefined { +export function getRulePluginOrigin(rule?: Rule | PromRuleDTO | RulerRuleDTO): RulePluginOrigin | undefined { if (!rule) { return undefined; } @@ -245,7 +261,7 @@ export function isPluginProvidedGroup(group: RulerRuleGroupDTO): boolean { return group.rules.some((rule) => isPluginProvidedRule(rule)); } -export function isPluginProvidedRule(rule?: Rule | RulerRuleDTO): boolean { +export function isPluginProvidedRule(rule?: Rule | PromRuleDTO | RulerRuleDTO): boolean { return Boolean(getRulePluginOrigin(rule)); } diff --git a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx index d730e70ebc4..727793d04d2 100644 --- a/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderAlertingPage.test.tsx @@ -34,7 +34,7 @@ const mockFolderUid = '12345'; const random = Chance(1); const rule_uid = random.guid(); const mockRulerRulesResponse = getRulerRulesResponse(mockFolderName, mockFolderUid, rule_uid); -const mockPrometheusRulesResponse = getPrometheusRulesResponse(mockFolderName, rule_uid); +const mockPrometheusRulesResponse = getPrometheusRulesResponse(mockFolderName, mockFolderUid, rule_uid); describe('browse-dashboards BrowseFolderAlertingPage', () => { (useParams as jest.Mock).mockReturnValue({ uid: mockFolderUid }); diff --git a/public/app/features/browse-dashboards/fixtures/alertRules.fixture.ts b/public/app/features/browse-dashboards/fixtures/alertRules.fixture.ts index fca05a8c63f..be447f4fa6c 100644 --- a/public/app/features/browse-dashboards/fixtures/alertRules.fixture.ts +++ b/public/app/features/browse-dashboards/fixtures/alertRules.fixture.ts @@ -2,8 +2,8 @@ import { Chance } from 'chance'; import { GrafanaAlertStateDecision, + GrafanaPromRulesResponse, PromAlertingRuleState, - PromRulesResponse, PromRuleType, RulerRulesConfigDTO, } from 'app/types/unified-alerting-dto'; @@ -57,7 +57,11 @@ export function getRulerRulesResponse(folderName: string, folderUid: string, rul }; } -export function getPrometheusRulesResponse(folderName: string, rule_uid: string): PromRulesResponse { +export function getPrometheusRulesResponse( + folderName: string, + folderUid: string, + rule_uid: string +): GrafanaPromRulesResponse { const random = Chance(1); return { status: 'success', @@ -66,6 +70,7 @@ export function getPrometheusRulesResponse(folderName: string, rule_uid: string) { name: 'foo', file: folderName, + folderUid: folderUid, rules: [ { alerts: [], @@ -80,6 +85,7 @@ export function getPrometheusRulesResponse(folderName: string, rule_uid: string) lastEvaluation: '0001-01-01T00:00:00Z', evaluationTime: 0, uid: rule_uid, + folderUid: folderUid, }, ], interval: 60, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx index c7a1d1fec86..fb0d3eb0524 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataAlertingTab.test.tsx @@ -134,9 +134,6 @@ const promResponse: PromRulesResponse = { interval: 20, }, ], - totals: { - alerting: 2, - }, }, }; diff --git a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts index cc4ae72f2cd..3c07c21bf5c 100644 --- a/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts +++ b/public/app/features/query/state/DashboardQueryRunner/DashboardQueryRunner.test.ts @@ -115,9 +115,6 @@ function getTestContext() { file: 'my-namespace', }, ], - totals: { - alerting: 2, - }, }, }; diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts index 14e350d26b6..c96d35b3736 100644 --- a/public/app/types/unified-alerting-dto.ts +++ b/public/app/types/unified-alerting-dto.ts @@ -124,7 +124,12 @@ interface PromRuleDTOBase { evaluationTime?: number; lastEvaluation?: string; lastError?: string; - uid?: string; +} + +interface GrafanaPromRuleDTOBase extends PromRuleDTOBase { + uid: string; + folderUid: string; + queriedDatasourceUIDs?: string[]; } export interface PromAlertingRuleDTO extends PromRuleDTOBase { @@ -162,15 +167,10 @@ export interface PromRuleGroupDTO { lastEvaluation?: string; } -export interface GrafanaPromAlertingRuleDTO extends PromAlertingRuleDTO { - uid: string; - folderUid: string; -} +export interface GrafanaPromAlertingRuleDTO extends GrafanaPromRuleDTOBase, PromAlertingRuleDTO {} + +export interface GrafanaPromRecordingRuleDTO extends GrafanaPromRuleDTOBase, PromRecordingRuleDTO {} -export interface GrafanaPromRecordingRuleDTO extends PromRecordingRuleDTO { - uid: string; - folderUid: string; -} export type GrafanaPromRuleDTO = GrafanaPromAlertingRuleDTO | GrafanaPromRecordingRuleDTO; export interface GrafanaPromRuleGroupDTO extends PromRuleGroupDTO { @@ -185,11 +185,14 @@ export interface PromResponse { warnings?: string[]; } -export type PromRulesResponse = PromResponse<{ - groups: PromRuleGroupDTO[]; - groupNextToken?: string; - totals?: AlertGroupTotals; -}>; +export interface PromRulesResponse extends PromResponse<{ groups: PromRuleGroupDTO[]; groupNextToken?: string }> {} + +export interface GrafanaPromRulesResponse + extends PromResponse<{ + groups: GrafanaPromRuleGroupDTO[]; + groupNextToken?: string; + totals?: AlertGroupTotals; + }> {} // Ruler rule DTOs interface RulerRuleBaseDTO { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 4c99d311a6b..823fddaec55 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1729,6 +1729,8 @@ "title-inspect-alert-rule": "Inspect Alert rule" }, "rule-list": { + "cannot-find-rule-details-for": "Cannot find rule details for {{uid}}", + "cannot-load-rule-details-for": "Cannot load rule details for {{name}}", "configure-datasource": "Configure", "draft-new-rule": "Draft a new rule", "ds-error-boundary": { @@ -1736,8 +1738,11 @@ "title": "Unable to load rules from this data source" }, "filter-view": { - "no-more-results": "No more results – showing {{numberOfRules}} rules", - "no-rules-found": "No alert or recording rules matched your current set of filters." + "cancel-search": "Cancel search", + "no-more-results": "No more results – found {{numberOfRules}} rules", + "no-rules-found": "No alert or recording rules matched your current set of filters.", + "results-loading": "Searching – found {{numberOfRules}} rules", + "results-with-cancellation": "Search cancelled – found {{numberOfRules}} rules" }, "import-to-gma": { "new-badge": "New!", diff --git a/public/test/setupTests.ts b/public/test/setupTests.ts index 242236ece98..3f053609138 100644 --- a/public/test/setupTests.ts +++ b/public/test/setupTests.ts @@ -37,3 +37,45 @@ jest.mock('app/features/dashboard-scene/saving/createDetectChangesWorker.ts'); // our tests are heavy in CI due to parallelisation and monaco and kusto // so we increase the default timeout to 2secs to avoid flakiness configure({ asyncUtilTimeout: 2000 }); + +// Mock Performance API methods not implemented in jsdom +if (window.performance) { + // Type-safe spies with proper return type definitions + if (!window.performance.mark) { + window.performance.mark = jest.mocked((markName: string) => { + return { + name: markName, + entryType: 'mark', + startTime: 0, + duration: 0, + detail: null, + toJSON: () => ({}), + }; + }); + } + + if (!window.performance.measure) { + window.performance.measure = jest.mocked((measureName: string) => { + return { + name: measureName, + entryType: 'measure', + startTime: 0, + duration: 100, + detail: null, + toJSON: () => ({}), + }; + }); + } + + if (!window.performance.getEntriesByName) { + window.performance.getEntriesByName = jest.mocked(() => []); + } + + if (!window.performance.clearMarks) { + window.performance.clearMarks = jest.mocked(() => {}); + } + + if (!window.performance.clearMeasures) { + window.performance.clearMeasures = jest.mocked(() => {}); + } +} From 45d6bfe7cfdff0302b51734f353e05ce2615ae39 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Fri, 11 Apr 2025 10:09:47 +0200 Subject: [PATCH 19/37] AuthZ: Make cache ttl configurable (#103769) * AuthZ: Configure cache ttl Co-authored-by: Eric Leijonmarck * Client side conf Co-authored-by: Ieva Co-authored-by: Eric Leijonmarck * 0 -> No caching * Make it possible to disable cache on the remote client as well * Comment * Move ttl parsing up for in-proc to have it --------- Co-authored-by: Eric Leijonmarck Co-authored-by: Ieva --- pkg/services/authz/rbac.go | 20 +++++++++++--------- pkg/services/authz/rbac/cache.go | 29 ++++++++++++++++++++++++----- pkg/services/authz/rbac/service.go | 25 ++++++++++++++----------- pkg/services/authz/rbac_settings.go | 10 +++++++++- 4 files changed, 58 insertions(+), 26 deletions(-) diff --git a/pkg/services/authz/rbac.go b/pkg/services/authz/rbac.go index d0ce26f0a9e..3822596d6ba 100644 --- a/pkg/services/authz/rbac.go +++ b/pkg/services/authz/rbac.go @@ -70,7 +70,7 @@ func ProvideAuthZClient( default: sql := legacysql.NewDatabaseProvider(db) - rbacSettings := rbac.Settings{} + rbacSettings := rbac.Settings{CacheTTL: authCfg.cacheTTL} if cfg != nil { rbacSettings.AnonOrgRole = cfg.Anonymous.OrgRole } @@ -163,14 +163,16 @@ func newRemoteRBACClient(clientCfg *authzClientSettings, tracer trace.Tracer) (a return nil, fmt.Errorf("failed to create authz client to remote server: %w", err) } - client := authzlib.NewClient( - conn, - authzlib.WithCacheClientOption(cache.NewLocalCache(cache.Config{ - Expiry: 30 * time.Second, + // Client side cache + var authzCache cache.Cache = &NoopCache{} + if clientCfg.cacheTTL != 0 { + authzCache = cache.NewLocalCache(cache.Config{ + Expiry: clientCfg.cacheTTL, CleanupInterval: 2 * time.Minute, - })), - authzlib.WithTracerClientOption(tracer), - ) + }) + } + + client := authzlib.NewClient(conn, authzlib.WithCacheClientOption(authzCache), authzlib.WithTracerClientOption(tracer)) return client, nil } @@ -215,7 +217,7 @@ func RegisterRBACAuthZService( tracer, reg, cache, - rbac.Settings{}, // anonymous org role can only be set in-proc + rbac.Settings{CacheTTL: cfg.CacheTTL}, // anonymous org role can only be set in-proc ) srv := handler.GetServer() diff --git a/pkg/services/authz/rbac/cache.go b/pkg/services/authz/rbac/cache.go index a8a67ec43cb..5a37051c499 100644 --- a/pkg/services/authz/rbac/cache.go +++ b/pkg/services/authz/rbac/cache.go @@ -43,7 +43,11 @@ func folderCacheKey(namespace string) string { return namespace + ".folders" } -type cacheWrap[T any] struct { +type cacheWrap[T any] interface { + Get(ctx context.Context, key string) (T, bool) + Set(ctx context.Context, key string, value T) +} +type cacheWrapImpl[T any] struct { cache cache.Cache logger log.Logger ttl time.Duration @@ -51,11 +55,15 @@ type cacheWrap[T any] struct { // cacheWrap is a wrapper around the authlib Cache that provides typed Get and Set methods // it handles encoding/decoding for a specific type. -func newCacheWrap[T any](cache cache.Cache, logger log.Logger, ttl time.Duration) *cacheWrap[T] { - return &cacheWrap[T]{cache: cache, logger: logger, ttl: ttl} +func newCacheWrap[T any](cache cache.Cache, logger log.Logger, ttl time.Duration) cacheWrap[T] { + if ttl == 0 { + logger.Info("cache ttl is 0, using noop cache") + return &noopCache[T]{} + } + return &cacheWrapImpl[T]{cache: cache, logger: logger, ttl: ttl} } -func (c *cacheWrap[T]) Get(ctx context.Context, key string) (T, bool) { +func (c *cacheWrapImpl[T]) Get(ctx context.Context, key string) (T, bool) { logger := c.logger.FromContext(ctx) var value T @@ -76,7 +84,7 @@ func (c *cacheWrap[T]) Get(ctx context.Context, key string) (T, bool) { return value, true } -func (c *cacheWrap[T]) Set(ctx context.Context, key string, value T) { +func (c *cacheWrapImpl[T]) Set(ctx context.Context, key string, value T) { logger := c.logger.FromContext(ctx) data, err := json.Marshal(value) @@ -90,3 +98,14 @@ func (c *cacheWrap[T]) Set(ctx context.Context, key string, value T) { logger.Warn("failed to set to cache", "key", key, "error", err) } } + +type noopCache[T any] struct{} + +func (lc *noopCache[T]) Get(ctx context.Context, key string) (T, bool) { + var value T + return value, false +} + +func (lc *noopCache[T]) Set(ctx context.Context, key string, value T) { + // no-op +} diff --git a/pkg/services/authz/rbac/service.go b/pkg/services/authz/rbac/service.go index 46a24006fdf..f805197d292 100644 --- a/pkg/services/authz/rbac/service.go +++ b/pkg/services/authz/rbac/service.go @@ -54,16 +54,19 @@ type Service struct { sf *singleflight.Group // Cache for user permissions, user team memberships and user basic roles - idCache *cacheWrap[store.UserIdentifiers] - permCache *cacheWrap[map[string]bool] - permDenialCache *cacheWrap[bool] - teamCache *cacheWrap[[]int64] - basicRoleCache *cacheWrap[store.BasicRole] - folderCache *cacheWrap[folderTree] + idCache cacheWrap[store.UserIdentifiers] + permCache cacheWrap[map[string]bool] + permDenialCache cacheWrap[bool] + teamCache cacheWrap[[]int64] + basicRoleCache cacheWrap[store.BasicRole] + folderCache cacheWrap[folderTree] } type Settings struct { AnonOrgRole string + // CacheTTL is the time to live for the permission cache entries. + // Set to 0 to disable caching. + CacheTTL time.Duration } func NewService( @@ -91,11 +94,11 @@ func NewService( metrics: newMetrics(reg), mapper: newMapper(), idCache: newCacheWrap[store.UserIdentifiers](cache, logger, longCacheTTL), - permCache: newCacheWrap[map[string]bool](cache, logger, shortCacheTTL), - permDenialCache: newCacheWrap[bool](cache, logger, shortCacheTTL), - teamCache: newCacheWrap[[]int64](cache, logger, shortCacheTTL), - basicRoleCache: newCacheWrap[store.BasicRole](cache, logger, shortCacheTTL), - folderCache: newCacheWrap[folderTree](cache, logger, shortCacheTTL), + permCache: newCacheWrap[map[string]bool](cache, logger, settings.CacheTTL), + permDenialCache: newCacheWrap[bool](cache, logger, settings.CacheTTL), + teamCache: newCacheWrap[[]int64](cache, logger, settings.CacheTTL), + basicRoleCache: newCacheWrap[store.BasicRole](cache, logger, settings.CacheTTL), + folderCache: newCacheWrap[folderTree](cache, logger, settings.CacheTTL), sf: new(singleflight.Group), } } diff --git a/pkg/services/authz/rbac_settings.go b/pkg/services/authz/rbac_settings.go index ea0417e5041..a4a48a2e7f1 100644 --- a/pkg/services/authz/rbac_settings.go +++ b/pkg/services/authz/rbac_settings.go @@ -2,6 +2,7 @@ package authz import ( "fmt" + "time" "github.com/grafana/grafana/pkg/setting" ) @@ -29,6 +30,8 @@ type authzClientSettings struct { token string tokenExchangeURL string tokenNamespace string + + cacheTTL time.Duration } func readAuthzClientSettings(cfg *setting.Cfg) (*authzClientSettings, error) { @@ -41,6 +44,9 @@ func readAuthzClientSettings(cfg *setting.Cfg) (*authzClientSettings, error) { } s := &authzClientSettings{} + // Cache duration applies to the server cache in proc, so it's relevant for both modes. + s.cacheTTL = authzSection.Key("cache_ttl").MustDuration(30 * time.Second) + s.mode = mode if s.mode == clientModeInproc { return s, nil @@ -48,6 +54,7 @@ func readAuthzClientSettings(cfg *setting.Cfg) (*authzClientSettings, error) { s.remoteAddress = authzSection.Key("remote_address").MustString("") s.certFile = authzSection.Key("cert_file").MustString("") + s.token = grpcClientAuthSection.Key("token").MustString("") s.tokenNamespace = grpcClientAuthSection.Key("token_namespace").MustString("stacks-" + cfg.StackID) s.tokenExchangeURL = grpcClientAuthSection.Key("token_exchange_url").MustString("") @@ -61,7 +68,8 @@ func readAuthzClientSettings(cfg *setting.Cfg) (*authzClientSettings, error) { } type RBACServerSettings struct { - Folder FolderAPISettings + Folder FolderAPISettings + CacheTTL time.Duration } type FolderAPISettings struct { From 50fcc053c260de55cfc79465d3b6157a740c551f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Fri, 11 Apr 2025 10:10:13 +0200 Subject: [PATCH 20/37] feat(unified-storage): provide delete function for bucket (#103825) --- pkg/storage/unified/resource/cdk_bucket.go | 25 +++++++++++++++++ .../unified/resource/cdk_bucket_test.go | 28 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/pkg/storage/unified/resource/cdk_bucket.go b/pkg/storage/unified/resource/cdk_bucket.go index 7a789c26eb1..e148326f8a9 100644 --- a/pkg/storage/unified/resource/cdk_bucket.go +++ b/pkg/storage/unified/resource/cdk_bucket.go @@ -19,10 +19,12 @@ type CDKBucket interface { ListPage(context.Context, []byte, int, *blob.ListOptions) ([]*blob.ListObject, []byte, error) WriteAll(context.Context, string, []byte, *blob.WriterOptions) error ReadAll(context.Context, string) ([]byte, error) + Delete(context.Context, string) error SignedURL(context.Context, string, *blob.SignedURLOptions) (string, error) } var _ CDKBucket = (*blob.Bucket)(nil) +var _ CDKBucket = (*InstrumentedBucket)(nil) const ( cdkBucketOperationLabel = "operation" @@ -159,6 +161,29 @@ func (b *InstrumentedBucket) WriteAll(ctx context.Context, key string, p []byte, return err } +func (b *InstrumentedBucket) Delete(ctx context.Context, key string) error { + ctx, span := b.tracer.Start(ctx, "InstrumentedBucket/Delete") + defer span.End() + start := time.Now() + err := b.bucket.Delete(ctx, key) + end := time.Since(start).Seconds() + labels := prometheus.Labels{ + cdkBucketOperationLabel: "Delete", + } + if err != nil { + labels[cdkBucketStatusLabel] = cdkBucketStatusError + b.requests.With(labels).Inc() + b.latency.With(labels).Observe(end) + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return err + } + labels[cdkBucketStatusLabel] = cdkBucketStatusSuccess + b.requests.With(labels).Inc() + b.latency.With(labels).Observe(end) + return nil +} + func (b *InstrumentedBucket) SignedURL(ctx context.Context, key string, opts *blob.SignedURLOptions) (string, error) { ctx, span := b.tracer.Start(ctx, "InstrumentedBucket/SignedURL") defer span.End() diff --git a/pkg/storage/unified/resource/cdk_bucket_test.go b/pkg/storage/unified/resource/cdk_bucket_test.go index 71949d94f80..9a577a23864 100644 --- a/pkg/storage/unified/resource/cdk_bucket_test.go +++ b/pkg/storage/unified/resource/cdk_bucket_test.go @@ -12,6 +12,8 @@ import ( "gocloud.dev/blob" ) +var _ CDKBucket = (*fakeCDKBucket)(nil) + type fakeCDKBucket struct { attributesFunc func(ctx context.Context, key string) (*blob.Attributes, error) writeAllFunc func(ctx context.Context, key string, p []byte, opts *blob.WriterOptions) error @@ -19,6 +21,7 @@ type fakeCDKBucket struct { signedURLFunc func(ctx context.Context, key string, opts *blob.SignedURLOptions) (string, error) listFunc func(opts *blob.ListOptions) *blob.ListIterator listPageFunc func(ctx context.Context, pageToken []byte, pageSize int, opts *blob.ListOptions) ([]*blob.ListObject, []byte, error) + deleteFunc func(ctx context.Context, key string) error } func (f *fakeCDKBucket) Attributes(ctx context.Context, key string) (*blob.Attributes, error) { @@ -63,6 +66,13 @@ func (f *fakeCDKBucket) ListPage(ctx context.Context, pageToken []byte, pageSize return nil, nil, nil } +func (f *fakeCDKBucket) Delete(ctx context.Context, key string) error { + if f.deleteFunc != nil { + return f.deleteFunc(ctx, key) + } + return nil +} + func TestInstrumentedBucket(t *testing.T) { operations := []struct { name string @@ -108,6 +118,24 @@ func TestInstrumentedBucket(t *testing.T) { return err }, }, + { + name: "Delete", + operation: "Delete", + setup: func(fakeBucket *fakeCDKBucket, success bool) { + if success { + fakeBucket.deleteFunc = func(ctx context.Context, key string) error { + return nil + } + } else { + fakeBucket.deleteFunc = func(ctx context.Context, key string) error { + return fmt.Errorf("some error") + } + } + }, + call: func(instrumentedBucket *InstrumentedBucket) error { + return instrumentedBucket.Delete(context.Background(), "key") + }, + }, { name: "ReadAll", operation: "ReadAll", From 79400018a4ae39117eb991b5abc8e2a9a3bf8db8 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 11 Apr 2025 09:21:06 +0100 Subject: [PATCH 21/37] Preferences: Allow setting `lang` and `theme` from URL (#103587) * allow setting theme/language via url * set in backend instead * add locale as well --- pkg/api/index.go | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index 9697fc3a02f..68423b8d0f8 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -23,6 +23,25 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +type URLPrefs struct { + Language string + Locale string + Theme string +} + +// URL prefs take precedence over any saved user preferences +func getURLPrefs(c *contextmodel.ReqContext) URLPrefs { + language := c.Query("lang") + theme := c.Query("theme") + locale := c.Query("locale") + + return URLPrefs{ + Language: language, + Locale: locale, + Theme: theme, + } +} + func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexViewData, error) { c, span := hs.injectSpan(c, "api.setIndexViewData") defer span.End() @@ -52,8 +71,11 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV acceptLangHeader := c.Req.Header.Get("Accept-Language") locale := "en-US" // default to en-US formatting, but use the accept-lang header or user's preference language := "" // frontend will set the default language + urlPrefs := getURLPrefs(c) - if prefs.JSONData.Language != "" { + if urlPrefs.Language != "" { + language = urlPrefs.Language + } else if prefs.JSONData.Language != "" { language = prefs.JSONData.Language } @@ -63,7 +85,9 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV } if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagLocaleFormatPreference) { - if prefs.JSONData.Locale != "" { + if urlPrefs.Locale != "" { + locale = urlPrefs.Locale + } else if prefs.JSONData.Locale != "" { locale = prefs.JSONData.Locale } } @@ -88,7 +112,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV weekStart = *prefs.WeekStart } - theme := hs.getThemeForIndexData(prefs.Theme, c.Query("theme")) + theme := hs.getThemeForIndexData(prefs.Theme, urlPrefs.Theme) assets, err := webassets.GetWebAssets(c.Req.Context(), hs.Cfg, hs.License) if err != nil { return nil, err From 4648ba396b5d4a4a328e3ae9ff1afe3f9c429669 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:25:21 +0100 Subject: [PATCH 22/37] DashboardScenePage: Correct slug in self referencing data links (#100048) * switch to useLayoutEffect so queries run after url correction * add comment * suparate useEffects to avoid update on slug change * add url correction for missing slug * account for slug change during dashboard rename * simplify fix * add e2e test for data link without slug * remove old comment * remove newly added path from useEffect dependencies --- .../dashboard-links-without-slug.spec.ts | 47 ++++ e2e/dashboards/DataLinkWithoutSlugTest.json | 256 ++++++++++++++++++ .../pages/DashboardScenePage.tsx | 33 ++- 3 files changed, 332 insertions(+), 4 deletions(-) create mode 100644 e2e/dashboards-suite/dashboard-links-without-slug.spec.ts create mode 100644 e2e/dashboards/DataLinkWithoutSlugTest.json diff --git a/e2e/dashboards-suite/dashboard-links-without-slug.spec.ts b/e2e/dashboards-suite/dashboard-links-without-slug.spec.ts new file mode 100644 index 00000000000..defc7678d3b --- /dev/null +++ b/e2e/dashboards-suite/dashboard-links-without-slug.spec.ts @@ -0,0 +1,47 @@ +import testDashboard from '../dashboards/DataLinkWithoutSlugTest.json'; +import { e2e } from '../utils'; + +describe('Dashboard with data links that have no slug', () => { + beforeEach(() => { + e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); + }); + + it('Should not reload if linking to same dashboard', () => { + cy.intercept({ + pathname: '/api/ds/query', + }).as('query'); + + e2e.flows.importDashboard(testDashboard, 1000, true); + cy.wait('@query'); + + e2e.components.Panels.Panel.title('Data links without slug').should('exist'); + + e2e.components.DataLinksContextMenu.singleLink().contains('9yy21uzzxypg').click(); + cy.contains('Loading', { timeout: 500 }) + .should(() => {}) // prevent test from failing if it does not find loading + .then(throwIfLoadingFound); + cy.url().should('include', urlShouldContain); + + e2e.components.DataLinksContextMenu.singleLink().contains('dr199bpvpcru').click(); + cy.contains('Loading', { timeout: 500 }) + .should(() => {}) // prevent test from failing if it does not find loading + .then(throwIfLoadingFound); + cy.url().should('include', urlShouldContain); + + e2e.components.DataLinksContextMenu.singleLink().contains('dre33fzyxcrz').click(); + cy.contains('Loading', { timeout: 500 }) + .should(() => {}) // prevent test from failing if it does not find loading + .then(throwIfLoadingFound); + cy.url().should('include', urlShouldContain); + }); +}); + +const urlShouldContain = '/d/data-link-no-slug/data-link-without-slug-test'; + +const throwIfLoadingFound = (el: JQuery) => { + if (el.length) { + // This means dashboard refreshes when clicking self-referencing data link + // that has no slug in it + throw new Error('Should not contain Loading'); + } +}; diff --git a/e2e/dashboards/DataLinkWithoutSlugTest.json b/e2e/dashboards/DataLinkWithoutSlugTest.json new file mode 100644 index 00000000000..afcd6c0c4e3 --- /dev/null +++ b/e2e/dashboards/DataLinkWithoutSlugTest.json @@ -0,0 +1,256 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 135, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "links": [ + { + "title": "", + "url": "/d/${__dashboard.uid}?var-instance=${__data.fields.test1}&${__url_time_range}" + } + ], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 4, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": ["sum"], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.6.0-pre", + "targets": [ + { + "alias": "test1", + "datasource": { + "type": "grafana-testdata-datasource", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "9wvfgzurfzb, 9yy21uzzxypg, dr199bpvpcru, dre33fzyxcrz, gc6j7crvrcpf, u6g9zuxvxypv" + } + ], + "title": "Data links without slug", + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.6.0-pre", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "gdev-prometheus" + }, + "disableTextWrap": false, + "editorMode": "builder", + "expr": "counters_logins{geohash=\"$instance\"}", + "fullMetaSearch": false, + "includeNullMetadata": true, + "instant": false, + "legendFormat": "__auto", + "range": true, + "refId": "A", + "useBackend": false + } + ], + "title": "Panel Title", + "type": "timeseries" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 41, + "tags": [], + "templating": { + "list": [ + { + "current": { + "text": "9wvfgzurfzb", + "value": "9wvfgzurfzb" + }, + "name": "instance", + "options": [ + { + "selected": true, + "text": "9wvfgzurfzb", + "value": "9wvfgzurfzb" + }, + { + "selected": false, + "text": "9yy21uzzxypg", + "value": "9yy21uzzxypg" + }, + { + "selected": false, + "text": "dr199bpvpcru", + "value": "dr199bpvpcru" + }, + { + "selected": false, + "text": "dre33fzyxcrz", + "value": "dre33fzyxcrz" + }, + { + "selected": false, + "text": "gc6j7crvrcpf", + "value": "gc6j7crvrcpf" + }, + { + "selected": false, + "text": "u6g9zuxvxypv", + "value": "u6g9zuxvxypv" + } + ], + "query": "9wvfgzurfzb, 9yy21uzzxypg, dr199bpvpcru, dre33fzyxcrz, gc6j7crvrcpf, u6g9zuxvxypv", + "type": "custom" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "utc", + "title": "Data Link without slug test", + "uid": "data-link-no-slug", + "version": 3 +} diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx index 3303244cb22..9e883816200 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.tsx @@ -1,9 +1,9 @@ -// Libraries -import { useEffect } from 'react'; -import { useParams } from 'react-router-dom-v5-compat'; +import { useEffect, useRef } from 'react'; +import { Params, useParams } from 'react-router-dom-v5-compat'; import { usePrevious } from 'react-use'; import { PageLayoutType } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; import { UrlSyncContextProvider } from '@grafana/scenes'; import { Box } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; @@ -31,6 +31,7 @@ export function DashboardScenePage({ route, queryParams, location }: Props) { const { dashboard, isLoading, loadError } = stateManager.useState(); // After scene migration is complete and we get rid of old dashboard we should refactor dashboardWatcher so this route reload is not need const routeReloadCounter = (location.state as any)?.routeReloadCounter; + const prevParams = useRef>(params); useEffect(() => { if (route.routeName === DashboardRoutes.Normal && type === 'snapshot') { @@ -48,7 +49,31 @@ export function DashboardScenePage({ route, queryParams, location }: Props) { return () => { stateManager.clearState(); }; - }, [stateManager, uid, route.routeName, queryParams.folderUid, routeReloadCounter, slug, type, path]); + + // removing slug and path (which has slug in it) from dependencies to prevent unmount when data links reference + // the same dashboard with no slug in url + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [stateManager, uid, route.routeName, queryParams.folderUid, routeReloadCounter, type]); + + useEffect(() => { + // This use effect corrects URL without refresh when navigating to the same dashboard + // using data link that has no slug in url + if (route.routeName === DashboardRoutes.Normal) { + // correct URL only when there are no new slug + // if slug is defined and incorrect it will be corrected in stateManager + if (uid === prevParams.current.uid && prevParams.current.slug && !slug) { + const correctedUrl = `/d/${uid}/${prevParams.current.slug}`; + locationService.replace({ + ...locationService.getLocation(), + pathname: correctedUrl, + }); + } + } + + return () => { + prevParams.current = { uid, slug: !slug ? prevParams.current.slug : slug }; + }; + }, [route, slug, type, uid]); if (!dashboard) { let errorElement; From 54b8dde1e17efd7d6caf0e2bfbe0b3d935d3ff93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Fri, 11 Apr 2025 10:28:21 +0200 Subject: [PATCH 23/37] services/user: Fix userimpl tests when running on Spanner. (#103715) Fix userimpl tests when running on Spanner. --- pkg/services/sqlstore/migrator/dialect.go | 14 +++++++ .../sqlstore/migrator/postgres_dialect.go | 11 +++++ .../sqlstore/migrator/spanner_dialect.go | 13 ++++++ pkg/services/user/userimpl/store.go | 40 +++++-------------- 4 files changed, 47 insertions(+), 31 deletions(-) diff --git a/pkg/services/sqlstore/migrator/dialect.go b/pkg/services/sqlstore/migrator/dialect.go index c5a0128450d..f68a1b6672d 100644 --- a/pkg/services/sqlstore/migrator/dialect.go +++ b/pkg/services/sqlstore/migrator/dialect.go @@ -27,7 +27,10 @@ type Dialect interface { ShowCreateNull() bool SQLType(col *Column) string SupportEngine() bool + // Deprecated. This doesn't work correctly for all databases. LikeStr() string + // LikeOperator returns SQL snippet and query parameter for case-insensitive LIKE operation, with optional wildcards (%) before/after the pattern. + LikeOperator(column string, wildcardBefore bool, pattern string, wildcardAfter bool) (string, string) Default(col *Column) string // BooleanValue can be used as an argument in SELECT or INSERT statements. For constructing // raw SQL queries, please use BooleanStr instead. @@ -153,6 +156,17 @@ func (b *BaseDialect) LikeStr() string { return "LIKE" } +func (b *BaseDialect) LikeOperator(column string, wildcardBefore bool, pattern string, wildcardAfter bool) (string, string) { + param := pattern + if wildcardBefore { + param = "%" + param + } + if wildcardAfter { + param = param + "%" + } + return fmt.Sprintf("%s LIKE ?", column), param +} + func (b *BaseDialect) OrStr() string { return "OR" } diff --git a/pkg/services/sqlstore/migrator/postgres_dialect.go b/pkg/services/sqlstore/migrator/postgres_dialect.go index 9d343cc2f8f..47a0f2707b5 100644 --- a/pkg/services/sqlstore/migrator/postgres_dialect.go +++ b/pkg/services/sqlstore/migrator/postgres_dialect.go @@ -35,6 +35,17 @@ func (db *PostgresDialect) LikeStr() string { return "ILIKE" } +func (db *PostgresDialect) LikeOperator(column string, wildcardBefore bool, pattern string, wildcardAfter bool) (string, string) { + param := pattern + if wildcardBefore { + param = "%" + param + } + if wildcardAfter { + param = param + "%" + } + return fmt.Sprintf("%s ILIKE ?", column), param +} + func (db *PostgresDialect) AutoIncrStr() string { return "" } diff --git a/pkg/services/sqlstore/migrator/spanner_dialect.go b/pkg/services/sqlstore/migrator/spanner_dialect.go index b4c83bdc07a..db1a1b4bc9b 100644 --- a/pkg/services/sqlstore/migrator/spanner_dialect.go +++ b/pkg/services/sqlstore/migrator/spanner_dialect.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "time" "cloud.google.com/go/spanner" @@ -44,6 +45,18 @@ func NewSpannerDialect() Dialect { func (s *SpannerDialect) AutoIncrStr() string { return s.d.AutoIncrStr() } func (s *SpannerDialect) Quote(name string) string { return s.d.Quote(name) } func (s *SpannerDialect) SupportEngine() bool { return s.d.SupportEngine() } + +func (s *SpannerDialect) LikeOperator(column string, wildcardBefore bool, pattern string, wildcardAfter bool) (string, string) { + param := strings.ToLower(pattern) + if wildcardBefore { + param = "%" + param + } + if wildcardAfter { + param = param + "%" + } + return fmt.Sprintf("LOWER(%s) LIKE ?", column), param +} + func (s *SpannerDialect) IndexCheckSQL(tableName, indexName string) (string, []any) { return s.d.IndexCheckSql(tableName, indexName) } diff --git a/pkg/services/user/userimpl/store.go b/pkg/services/user/userimpl/store.go index a5216b04e7a..e8d65d5f820 100644 --- a/pkg/services/user/userimpl/store.go +++ b/pkg/services/user/userimpl/store.go @@ -2,15 +2,11 @@ package userimpl import ( "context" - "errors" "fmt" "strconv" "strings" "time" - "github.com/go-sql-driver/mysql" - "github.com/mattn/go-sqlite3" - "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" @@ -76,8 +72,9 @@ func (ss *sqlStore) Insert(ctx context.Context, cmd *user.User) (int64, error) { }) return nil }) + if err != nil { - return 0, handleSQLError(err) + return 0, handleSQLError(ss.dialect, err) } return cmd.ID, nil @@ -482,8 +479,6 @@ func (ss *sqlStore) Search(ctx context.Context, query *user.SearchUsersQuery) (* Users: make([]*user.UserSearchHitDTO, 0), } err := ss.db.WithDbSession(ctx, func(dbSess *db.Session) error { - queryWithWildcards := "%" + query.Query + "%" - whereConditions := make([]string, 0) whereParams := make([]any, 0) sess := dbSess.Table("user").Alias("u") @@ -512,8 +507,11 @@ func (ss *sqlStore) Search(ctx context.Context, query *user.SearchUsersQuery) (* whereParams = append(whereParams, acFilter.Args...) if query.Query != "" { - whereConditions = append(whereConditions, "(email "+ss.dialect.LikeStr()+" ? OR name "+ss.dialect.LikeStr()+" ? OR login "+ss.dialect.LikeStr()+" ?)") - whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards) + emailSql, emailArg := ss.dialect.LikeOperator("email", true, query.Query, true) + nameSql, nameArg := ss.dialect.LikeOperator("name", true, query.Query, true) + loginSql, loginArg := ss.dialect.LikeOperator("login", true, query.Query, true) + whereConditions = append(whereConditions, fmt.Sprintf("(%s OR %s OR %s)", emailSql, nameSql, loginSql)) + whereParams = append(whereParams, emailArg, nameArg, loginArg) } if query.IsDisabled != nil { @@ -606,29 +604,9 @@ func setOptional[T any](v *T, add func(v T)) { } } -func handleSQLError(err error) error { - if isUniqueConstraintError(err) { +func handleSQLError(dialect migrator.Dialect, err error) error { + if dialect.IsUniqueConstraintViolation(err) { return user.ErrUserAlreadyExists } return err } - -func isUniqueConstraintError(err error) bool { - // check mysql error code - var me *mysql.MySQLError - if errors.As(err, &me) && me.Number == 1062 { - return true - } - - // for postgres we check the error message - if strings.Contains(err.Error(), "duplicate key value") { - return true - } - - var se sqlite3.Error - if errors.As(err, &se) && se.ExtendedCode == sqlite3.ErrConstraintUnique { - return true - } - - return false -} From b0b4b31765d11fa993e0af3f1ba315fc745d287c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= Date: Fri, 11 Apr 2025 10:31:10 +0200 Subject: [PATCH 24/37] publicdashboards: Don't format timestamp before passing it to database. (#103463) Don't format timestamp before passing it to database. --- pkg/services/publicdashboards/database/database.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/publicdashboards/database/database.go b/pkg/services/publicdashboards/database/database.go index ce11d7f3778..10ef574abc7 100644 --- a/pkg/services/publicdashboards/database/database.go +++ b/pkg/services/publicdashboards/database/database.go @@ -242,7 +242,7 @@ func (d *PublicDashboardStoreImpl) Update(ctx context.Context, cmd SavePublicDas cmd.PublicDashboard.Share, string(timeSettingsJSON), cmd.PublicDashboard.UpdatedBy, - cmd.PublicDashboard.UpdatedAt.UTC().Format("2006-01-02 15:04:05"), + cmd.PublicDashboard.UpdatedAt.UTC(), cmd.PublicDashboard.Uid) if err != nil { From c303cd3a8bdc17e3c3a2e1bcd19134143bf50b32 Mon Sep 17 00:00:00 2001 From: "grafana-pr-automation[bot]" <140550294+grafana-pr-automation[bot]@users.noreply.github.com> Date: Fri, 11 Apr 2025 09:55:12 +0100 Subject: [PATCH 25/37] I18n: Download translations from Crowdin (#103840) New Crowdin translations by GitHub Action Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- public/locales/cs-CZ/grafana.json | 377 ++++++++++++++++++++-------- public/locales/de-DE/grafana.json | 377 ++++++++++++++++++++-------- public/locales/es-ES/grafana.json | 377 ++++++++++++++++++++-------- public/locales/fr-FR/grafana.json | 377 ++++++++++++++++++++-------- public/locales/hu-HU/grafana.json | 377 ++++++++++++++++++++-------- public/locales/id-ID/grafana.json | 377 ++++++++++++++++++++-------- public/locales/it-IT/grafana.json | 377 ++++++++++++++++++++-------- public/locales/ja-JP/grafana.json | 377 ++++++++++++++++++++-------- public/locales/ko-KR/grafana.json | 377 ++++++++++++++++++++-------- public/locales/nl-NL/grafana.json | 377 ++++++++++++++++++++-------- public/locales/pl-PL/grafana.json | 377 ++++++++++++++++++++-------- public/locales/pt-BR/grafana.json | 377 ++++++++++++++++++++-------- public/locales/pt-PT/grafana.json | 377 ++++++++++++++++++++-------- public/locales/ru-RU/grafana.json | 377 ++++++++++++++++++++-------- public/locales/sv-SE/grafana.json | 377 ++++++++++++++++++++-------- public/locales/tr-TR/grafana.json | 377 ++++++++++++++++++++-------- public/locales/zh-Hans/grafana.json | 377 ++++++++++++++++++++-------- public/locales/zh-Hant/grafana.json | 377 ++++++++++++++++++++-------- 18 files changed, 4842 insertions(+), 1944 deletions(-) diff --git a/public/locales/cs-CZ/grafana.json b/public/locales/cs-CZ/grafana.json index 79fef471cb1..7e6ce227ead 100644 --- a/public/locales/cs-CZ/grafana.json +++ b/public/locales/cs-CZ/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -807,7 +810,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -828,7 +832,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -900,7 +905,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -931,6 +937,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -962,6 +969,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -994,6 +1004,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1052,12 +1063,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1321,6 +1335,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1539,7 +1557,8 @@ "label-target-data-source": "Cílový zdroj dat" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1817,7 +1836,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1886,7 +1907,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1931,7 +1953,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1956,6 +1980,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -2000,7 +2025,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "DO" @@ -2021,6 +2047,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2644,6 +2673,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2719,7 +2749,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2808,7 +2839,8 @@ "rows": "Řádky", "tab": "Karta", "tabs": "Karty", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Otevřít podokno možností", "row": { @@ -2816,6 +2848,10 @@ "hide": "Skrýt", "title": "Záhlaví řádku" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2934,13 +2970,13 @@ "title-option": "Název" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(prázdné)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3052,7 +3088,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3524,7 +3566,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Cesta k souboru:", - "managed-by-version-control": "Tento {type} je spravován správcem verzí a nelze ho odstranit. Pro odebrání ho odstraňte z úložiště a proveďte synchronizaci pro použití změn.", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "Zajištěný zdroj nelze odstranit" }, @@ -3920,6 +3962,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3945,7 +3988,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4059,6 +4103,10 @@ "description": "Hledáte aplikace Grafana Explore? Teď se nazývají aplikace Grafana Drilldown a najdete je v <1>Nabídka > Drilldown", "title": "Explore Metrics, Logs, Traces a Profiles byly přesunuty!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4107,6 +4155,9 @@ "scan-for-older-logs": "Skenovat starší protokoly", "stop-scan": "Ukončit skenování" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4133,6 +4184,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4164,6 +4216,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Zavřít historii dotazů", "datasource-a-z": "Zdroj dat A–Z", @@ -4291,6 +4346,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4370,8 +4426,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4386,6 +4446,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5232,6 +5293,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Kopírovat odkaz na řádek protokolu", "copy-log": "Kopírovat řádek protokolu", @@ -5284,11 +5349,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5355,14 +5422,20 @@ "when-complete": "Po dokončení snímku ho budete moct nahrát do cloudové sady." }, "building-snapshot": { - "description": "Vytváříme momentální snímek aktuálního stavu této instalace. Po dokončení snímku ho budete moct nahrát na Grafana Cloud.", + "description": "", "description-eta": "Vytvoření snímku obvykle trvá méně než dvě minuty.", - "title": "Snímek vytváření instalace" + "title": "" }, "can-i-move": { - "body": "Po připojení této instalace ke cloudové sadě budete moct nahrávat zdroje dat a nástěnky.", - "link-title": "Zjistěte víc o migraci dalšího nastavení", - "title": "Můžu tuto instalaci přesunout na Grafana Cloud?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Budete taky potřebovat cloudovou sadu. Pokud jste se právě zaregistrovali, automaticky vám vytvoříme první sadu. Pokud máte účet, budete muset vybrat nebo vytvořit sadu.", @@ -5371,7 +5444,7 @@ "body-token": "Vaše samostatně spravovaná instalace Grafana potřebuje speciální přístup k bezpečné migraci obsahu. Vytvořte migrační token ve vybrané cloudové sadě.", "body-token-field": "Migrační token", "body-token-field-placeholder": "Vložte token tady", - "body-token-instructions": "Přihlaste se ke své cloudové sadě a přejděte do Administrace, Obecné, Migrovat na Grafana Cloud. Vytvořte migrační token na této obrazovce a vložte ho sem.", + "body-token-instructions": "", "body-view-stacks": "Zobrazit mé cloudové sady", "cancel": "Zrušit", "connect": "Připojit k této sadě", @@ -5415,11 +5488,12 @@ "body": "Proces migrace musí být spuštěn ze samostatně spravované instance Grafany.", "configure-pdc-link": "Konfigurovat PDC pro tuto sadu", "link-title": "Zjistěte víc o Private Data Source Connect", - "step-1": "Přihlaste se ke své samostatně spravované instanci a přejděte na Administrace, Obecné, Migrovat na Grafana Cloud.", + "step-1": "", "step-2": "Vyberte „Migrovat tuto instanci na cloud“.", - "step-3": "Zobrazí se výzva k zadání migračního tokenu. Vygenerujte si token na této obrazovce.", - "step-4": "Ve své samostatně spravované instanci vyberte „Nahrát vše“ a nahrajte zdroje dat a nástěnky do této cloudové sady.", - "step-5": "Pokud některý ze zdrojů dat nebude fungovat přes veřejný internet, budete muset nainstalovat Private Data Source Connect ve svém samostatně spravovaném prostředí.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Provádění migrace" }, "is-it-secure": { @@ -5428,7 +5502,7 @@ "title": "Je to bezpečné?" }, "migrate-to-this-stack": { - "body": "Některé zdroje můžete migrovat ze své samostatně spravované instalace Grafany do této cloudové sady. Chcete-li to provést bezpečně, budete muset vygenerovat migrační token. Vaše samostatně spravovaná instance použije token k ověření pomocí této cloudové sady.", + "body": "", "link-title": "Zobrazit úplného průvodce migrací", "title": "Pomůžeme vám přejít na tuto sadu" }, @@ -5446,7 +5520,7 @@ "modal-close": "Zavřít", "modal-copy-and-close": "Kopírovat do schránky a zavřít", "modal-copy-button": "Kopírovat do schránky", - "modal-field-description": "Zkopírujte token teď, protože ho znovu neuvidíte. Ztráta tokenu vyžaduje vytvoření nového tokenu.", + "modal-field-description": "", "modal-field-label": "Token", "modal-title": "Migrační token vytvořen", "status": "Aktuální stav: <2>" @@ -5463,7 +5537,7 @@ "get-session-error-title": "Chyba při načítání konfigurace migrace", "get-snapshot-error-title": "Chyba při načítání snímku", "migration-finished-with-caveat-title": "Migrace zdroje dokončena", - "migration-finished-with-errors-body": "Migrace byla dokončena, ale některé položky nebylo možné migrovat do cloudové sady. Další podrobnosti najdete v části Neúspěšné zdroje", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "Migrace byla dokončena s několika varováními. Další podrobnosti najdete v jednotlivých zdrojích", "snapshot-error-status-body": "Při vytváření snímku nebo spouštění procesu migrace došlo k chybě. Další podrobnosti najdete v serverových protokolech Grafany", "snapshot-error-status-title": "Chyba při migraci zdrojů", @@ -5475,7 +5549,7 @@ "pdc": { "body": "Zveřejnění zdrojů dat na internetu může vyvolat obavy o bezpečnost. Private Data Source Connect (PDC) umožňuje Grafana Cloud přístup k vašim stávajícím zdrojům dat přes zabezpečený síťový tunel.", "link-title": "Zjistěte víc o PDC", - "title": "Ne všechny mé zdroje dat jsou na veřejném internetu" + "title": "" }, "pricing": { "body": "Grafana Cloud má velkorysý bezplatný plán a 14denní neomezenou zkušební verzi. Po vypršení zkušebního období vám bude účtováno na základě využití nad rámec bezplatného tarifu.", @@ -5553,7 +5627,7 @@ "disconnect": "Odpojit", "errored-resource-count": "Chyby", "page-loading": "Načítání…", - "rebuild-snapshot": "Znovu vytvořit snímek", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Časové razítko snímku", "snapshot-not-created": "Dosud nevytvořeno", @@ -5810,7 +5884,7 @@ "subtitle": "Spravujte nástěnky složek a oprávnění" }, "migrate-to-cloud": { - "subtitle": "Zkopírujte konfiguraci ze své samostatně spravované instalace do cloudové sady", + "subtitle": "", "title": "Migrujte na Grafana Cloud" }, "monitoring": { @@ -6174,7 +6248,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Nejsou k dispozici žádné aktualizace", @@ -6358,6 +6433,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6372,21 +6448,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_few": "", "files-count_many": "", @@ -6400,13 +6469,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6417,25 +6484,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6453,12 +6516,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6469,14 +6533,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6514,7 +6574,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6532,19 +6591,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6602,16 +6661,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6641,8 +6701,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6721,30 +6780,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6951,6 +7016,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7234,6 +7300,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7242,8 +7309,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7253,7 +7322,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7262,7 +7333,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7288,6 +7360,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7570,16 +7643,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Zobrazení příliš mnoha sloupců v jedné tabulce může ovlivnit výkon a zhoršit čitelnost dat. Zvažte upřesnění dotazů.", @@ -7602,6 +7680,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7613,24 +7693,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7729,6 +7821,7 @@ "date-created": "Datum vytvoření: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7750,11 +7843,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7766,6 +7861,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7786,7 +7882,8 @@ "now": "Teď:", "previously": "Dříve:", "see-examples": "> Zobrazit příklady", - "sentence": "Nativní histogramy Prometheus nabízejí vysoké rozlišení, vysokou přesnost, jednoduché použití v instrumentaci a způsob, jak kombinovat a manipulovat s histogramy v dotazech a na Grafaně." + "sentence": "Nativní histogramy Prometheus nabízejí vysoké rozlišení, vysokou přesnost, jednoduché použití v instrumentaci a způsob, jak kombinovat a manipulovat s histogramy v dotazech a na Grafaně.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Nebo zobrazit nedávné prozkoumané" @@ -7794,11 +7891,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Vždy mějte vybraný graf s metrikami pod dohledem", "show-previews-of-metric-graphs": "Zobrazit náhledy grafů s metrikami" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7836,11 +7937,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7850,6 +7957,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7862,8 +7970,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7890,6 +8010,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7906,8 +8027,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7916,7 +8042,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7929,6 +8057,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7939,6 +8069,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7949,21 +8082,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7993,7 +8137,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -8013,11 +8158,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8085,6 +8234,7 @@ "message": "Nebyli nalezeni žádní uživatelé" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8099,6 +8249,11 @@ "shared-dashboard-users-tab-title": "Uživatelé sdílené nástěnky" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Vybrat hodnotu" @@ -8109,7 +8264,7 @@ "picker": { "link-all": "Vše", "option-all": "Vše", - "option-selected-values": "Vybrané", + "option-selected-values": "", "option-tooltip": "Vymazat výběr" }, "textbox": { @@ -8135,6 +8290,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8187,9 +8345,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/de-DE/grafana.json b/public/locales/de-DE/grafana.json index b40ddf17935..fc5c9b88351 100644 --- a/public/locales/de-DE/grafana.json +++ b/public/locales/de-DE/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -803,7 +806,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -824,7 +828,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -896,7 +901,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -927,6 +933,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -958,6 +965,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -990,6 +1000,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1048,12 +1059,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1315,6 +1329,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1529,7 +1547,8 @@ "label-target-data-source": "Zieldatenquelle" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1805,7 +1824,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1874,7 +1895,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1919,7 +1941,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1944,6 +1968,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1988,7 +2013,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "BIS" @@ -2009,6 +2035,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2622,6 +2651,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2697,7 +2727,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2786,7 +2817,8 @@ "rows": "Zeilen", "tab": "Tab", "tabs": "Tabs", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Optionsfenster öffnen", "row": { @@ -2794,6 +2826,10 @@ "hide": "Ausblenden", "title": "Kopfzeile" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2912,13 +2948,13 @@ "title-option": "" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(leer)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3030,7 +3066,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3502,7 +3544,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Dateipfad:", - "managed-by-version-control": "Diese/r/s {type} wird von der Versionskontrolle verwaltet und kann nicht gelöscht werden. Um dieses Element zu entfernen, muss es aus dem Repository gelöscht und dann synchronisiert werden, damit die Änderungen übernommen werden.", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "Bereitgestellte Ressource kann nicht gelöscht werden" }, @@ -3898,6 +3940,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3923,7 +3966,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4037,6 +4081,10 @@ "description": "Sie sind auf der Suche nach den Grafana-Explore-Apps? Sie heißen jetzt Grafana-Drilldown-Apps und sind unter <1>Menü > Drilldown zu finden", "title": "Explore-Metriken, Protokolle, Traces und Profile wurden verschoben!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4085,6 +4133,9 @@ "scan-for-older-logs": "Nach älteren Protokollen suchen", "stop-scan": "Durchsuchung anhalten" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4111,6 +4162,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4142,6 +4194,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Abfrageverlauf schließen", "datasource-a-z": "Datenquelle A-Z", @@ -4269,6 +4324,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4348,8 +4404,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4364,6 +4424,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5208,6 +5269,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Link zur Protokollzeile kopieren", "copy-log": "Protokollzeile kopieren", @@ -5260,11 +5325,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5331,14 +5398,20 @@ "when-complete": "Sobald der Snapshot erstellt wurde, können Sie ihn in Ihren Cloud-Stack hochladen." }, "building-snapshot": { - "description": "Wir erstellen einen Momentaufnahme-Snapshot des aktuellen Status dieser Installation. Sobald der Snapshot erstellt wurde, können Sie ihn in die Grafana-Cloud hochladen.", + "description": "", "description-eta": "Das Erstellen eines Snapshots dauert in der Regel weniger als zwei Minuten.", - "title": "Installations-Snapshot wird erstellt" + "title": "" }, "can-i-move": { - "body": "Sobald Sie diese Installation mit einem Cloud Stack verbinden, können Sie Datenquellen und Dashboards hochladen.", - "link-title": "Erfahren Sie, wie Sie andere Einstellungen migrieren", - "title": "Kann ich diese Installation in die Grafana Cloud verschieben?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Sie benötigen außerdem einen Cloud Stack. Wenn Sie sich gerade angemeldet haben, erstellen wir automatisch Ihren ersten Stack. Wenn Sie bereits ein Konto haben, müssen Sie einen Stack auswählen oder erstellen.", @@ -5347,7 +5420,7 @@ "body-token": "Ihre selbstverwaltete Grafana-Installation benötigt einen speziellen Zugang, um Inhalte sicher zu migrieren. Sie müssen ein Migrationstoken für den von Ihnen gewählten Cloud-Stack erstellen.", "body-token-field": "Migrationstoken", "body-token-field-placeholder": "Token hier einfügen", - "body-token-instructions": "Loggen Sie sich in Ihren Cloud Stack ein und navigieren Sie zu Administration, Allgemein, Zu Grafana Cloud migrieren. Erstellen Sie auf diesem Bildschirm ein Migrationstoken und fügen Sie das Token hier ein.", + "body-token-instructions": "", "body-view-stacks": "Meine Cloud Stacks anzeigen", "cancel": "Abbrechen", "connect": "Mit diesem Stack verbinden", @@ -5391,11 +5464,12 @@ "body": "Der Migrationsprozess muss von Ihrer selbstverwalteten Grafana-Instanz aus gestartet werden.", "configure-pdc-link": "PDC für diesen Stack konfigurieren", "link-title": "Erfahren Sie mehr über Private Data Source Connect", - "step-1": "Loggen Sie sich in Ihre selbstverwaltete Instanz ein und navigieren Sie zur Administration, Allgemein, Zu Grafana Cloud migrieren.", + "step-1": "", "step-2": "Wählen Sie „Diese Instanz in die Cloud migrieren“.", - "step-3": "Sie werden nach einem Migrationstoken gefragt. Generieren Sie eines von diesem Bildschirm.", - "step-4": "Wählen Sie in Ihrer selbstverwalteten Instanz „Alles hochladen“ aus, um Datenquellen und Dashboards auf diesen Cloud Stack hochzuladen.", - "step-5": "Wenn einige Ihrer Datenquellen nicht über das öffentliche Internet funktionieren, müssen Sie Private Data Source Connect in Ihrer selbstverwalteten Umgebung installieren.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Durchführung einer Migration" }, "is-it-secure": { @@ -5404,7 +5478,7 @@ "title": "Ist es sicher?" }, "migrate-to-this-stack": { - "body": "Sie können Ressourcen von Ihrer selbst verwalteten Grafana-Installation zu diesem Cloud-Stack migrieren. Um dies sicher ausführen zu können, müssen Sie ein Migrations-Token generieren. Ihre selbst verwaltete Instanz verwendet das Token, um sich bei diesem Cloud-Stack zu authentifizieren.", + "body": "", "link-title": "Gesamte Migrationsanleitung anzeigen", "title": "Wir helfen Ihnen bei der Migration zu diesem Stack" }, @@ -5422,7 +5496,7 @@ "modal-close": "Schließen", "modal-copy-and-close": "In Zwischenablage kopieren und schließen", "modal-copy-button": "In die Zwischenablage kopieren", - "modal-field-description": "Kopieren Sie den Token jetzt, denn Sie werden ihn später nicht mehr sehen können. Wenn Sie einen Token verlieren, müssen Sie einen neuen Token erstellen.", + "modal-field-description": "", "modal-field-label": "Token", "modal-title": "Migrationstoken erstellt", "status": "" @@ -5439,7 +5513,7 @@ "get-session-error-title": "Fehler beim Laden der Migrationskonfiguration", "get-snapshot-error-title": "Fehler beim Laden des Snapshots", "migration-finished-with-caveat-title": "Ressourcenmigration abgeschlossen", - "migration-finished-with-errors-body": "Die Migration ist abgeschlossen, aber einige Elemente konnten nicht in den Cloud-Stack migriert werden. Überprüfen Sie die fehlgeschlagenen Ressourcen, um weitere Details zu erhalten", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "Die Migration wurde mit einigen Warnungen abgeschlossen. Überprüfen Sie die einzelnen Ressourcen, um weitere Details zu erhalten", "snapshot-error-status-body": "Beim Erstellen des Snapshots oder beim Starten des Migrationsprozesses ist ein Fehler aufgetreten. Weitere Informationen finden Sie in den Grafana-Serverprotokollen", "snapshot-error-status-title": "Fehler beim Migrieren von Ressourcen", @@ -5451,7 +5525,7 @@ "pdc": { "body": "Wenn Sie Ihre Datenquellen dem Internet aussetzen, kann dies zu Sicherheitsrisiken führen. Private Data Source Connect (PDC) ermöglicht Grafana Cloud den Zugriff auf Ihre bestehenden Datenquellen über einen sicheren Netzwerktunnel.", "link-title": "Mehr über PDC erfahren", - "title": "Nicht alle meine Datenquellen sind im öffentlichen Internet" + "title": "" }, "pricing": { "body": "Grafana Cloud bietet einen großzügigen kostenlosen Tarif und eine 14-tägige Testphase mit unbegrenzter Nutzung. Nach Ablauf der Testphase wird Ihnen die Nutzung über die Grenzen des kostenlosen Tarifs hinaus in Rechnung gestellt.", @@ -5529,7 +5603,7 @@ "disconnect": "Verbindung trennen", "errored-resource-count": "Fehler", "page-loading": "", - "rebuild-snapshot": "Snapshot erneut erstellen", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Snapshot-Zeitstempel", "snapshot-not-created": "Noch nicht erstellt", @@ -5786,7 +5860,7 @@ "subtitle": "Ordner-Dashboards und Berechtigungen verwalten" }, "migrate-to-cloud": { - "subtitle": "Kopieren Sie die Konfiguration von Ihrer selbstverwalteten Installation in einen Cloud Stack", + "subtitle": "", "title": "In die Grafana Cloud migrieren" }, "monitoring": { @@ -6150,7 +6224,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Keine Updates vorhanden", @@ -6334,6 +6409,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6348,21 +6424,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_other": "" }, @@ -6374,13 +6443,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6391,25 +6458,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6427,12 +6490,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6443,14 +6507,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6488,7 +6548,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6506,19 +6565,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6576,16 +6635,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6615,8 +6675,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6695,30 +6754,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6923,6 +6988,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7200,6 +7266,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7208,8 +7275,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7219,7 +7288,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7228,7 +7299,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7254,6 +7326,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7536,16 +7609,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Wenn zu viele Spalten in einer Tabelle angezeigt werden, kann dies die Leistung beeinträchtigen und die Daten können schwieriger zu lesen sein. Präzisieren Sie, sofern möglich, Ihre Abfragen.", @@ -7568,6 +7646,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7579,24 +7659,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7695,6 +7787,7 @@ "date-created": "Erstellt am: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7716,11 +7809,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7732,6 +7827,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7752,7 +7848,8 @@ "now": "Jetzt:", "previously": "Zuvor:", "see-examples": "> Beispiele ansehen", - "sentence": "Native Prometheus-Histogramme überzeugen durch eine hohe Auflösung, eine hohe Präzision, eine einfache Verwendung bei der Instrumentierung sowie die Möglichkeit, Histogramme in Abfragen und in Grafana zu kombinieren und zu bearbeiten." + "sentence": "Native Prometheus-Histogramme überzeugen durch eine hohe Auflösung, eine hohe Präzision, eine einfache Verwendung bei der Instrumentierung sowie die Möglichkeit, Histogramme in Abfragen und in Grafana zu kombinieren und zu bearbeiten.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Oder eine aktuelle Erkundung anzeigen" @@ -7760,11 +7857,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Behalten Sie ausgewählte Metrik-Diagramme im Blick", "show-previews-of-metric-graphs": "Vorschau von Metrik-Diagrammen anzeigen" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7802,11 +7903,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7816,6 +7923,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7828,8 +7936,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7856,6 +7976,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7872,8 +7993,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7882,7 +8008,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7895,6 +8023,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7905,6 +8035,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7915,21 +8048,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7959,7 +8103,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7979,11 +8124,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8051,6 +8200,7 @@ "message": "Keine Benutzer gefunden" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8065,6 +8215,11 @@ "shared-dashboard-users-tab-title": "Freigegebene Dashboard-Benutzer" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Wert auswählen" @@ -8075,7 +8230,7 @@ "picker": { "link-all": "Alle", "option-all": "Alle", - "option-selected-values": "Ausgewählt", + "option-selected-values": "", "option-tooltip": "Auswahl löschen" }, "textbox": { @@ -8101,6 +8256,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8153,9 +8311,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/es-ES/grafana.json b/public/locales/es-ES/grafana.json index fa2e5610e93..833b693fc0e 100644 --- a/public/locales/es-ES/grafana.json +++ b/public/locales/es-ES/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -803,7 +806,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -824,7 +828,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -896,7 +901,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -927,6 +933,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -958,6 +965,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -990,6 +1000,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1048,12 +1059,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1315,6 +1329,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1529,7 +1547,8 @@ "label-target-data-source": "Fuente de datos de destino" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1805,7 +1824,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1874,7 +1895,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1919,7 +1941,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1944,6 +1968,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1988,7 +2013,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "HASTA" @@ -2009,6 +2035,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2622,6 +2651,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2697,7 +2727,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2786,7 +2817,8 @@ "rows": "Filas", "tab": "Pestaña", "tabs": "Pestañas", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Abrir panel de opciones", "row": { @@ -2794,6 +2826,10 @@ "hide": "Ocultar", "title": "Encabezado de fila" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2912,13 +2948,13 @@ "title-option": "" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(vacío)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3030,7 +3066,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3502,7 +3544,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Ruta del archivo:", - "managed-by-version-control": "Este {type} está gestionado por el control de versiones y no se puede eliminar. Para eliminarlo, bórralo del repositorio y sincroniza para aplicar los cambios.", + "managed-by-version-control": "", "ok": "Aceptar", "title-cannot-delete-provisioned-resource": "No se puede eliminar el recurso aprovisionado" }, @@ -3898,6 +3940,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3923,7 +3966,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4037,6 +4081,10 @@ "description": "¿Buscas las aplicaciones Grafana Explore? Ahora se llaman aplicaciones Grafana Drilldown y se pueden encontrar en <1>Menú > Drilldown", "title": "Las funciones de explorar métricas, registros, rastros y perfiles se han movido." }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4085,6 +4133,9 @@ "scan-for-older-logs": "Buscar registros más antiguos", "stop-scan": "Detener búsqueda" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4111,6 +4162,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4142,6 +4194,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Cerrar el historial de consultas", "datasource-a-z": "Fuente de datos A-Z", @@ -4269,6 +4324,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4348,8 +4404,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4364,6 +4424,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5208,6 +5269,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Copiar enlace a la línea de registro", "copy-log": "Copiar línea de registro", @@ -5260,11 +5325,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5331,14 +5398,20 @@ "when-complete": "Una vez que se complete la instantánea, podrás subirla en tu pila en la nube." }, "building-snapshot": { - "description": "Estamos creando una instantánea del estado actual de esta instalación en un momento determinado. Una vez que se complete la instantánea, podrás subirla a Grafana Cloud.", + "description": "", "description-eta": "La creación de una instantánea suele tardar menos de dos minutos.", - "title": "Creando instantánea de la instalación" + "title": "" }, "can-i-move": { - "body": "Una vez que conectes esta instalación a una pila en la nube, podrás cargar fuentes de datos y paneles.", - "link-title": "Infórmate sobre el proceso de migrar otros ajustes", - "title": "¿Puedo mover esta instalación a Grafana Cloud?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "También necesitarás una pila en la nube. Si te acabas de registrar, crearemos automáticamente tu primera pila. Si tienes una cuenta, deberás seleccionar o crear una.", @@ -5347,7 +5420,7 @@ "body-token": "Tu instalación autogestionada de Grafana necesita acceso especial para poder migrar el contenido de forma segura. Tendrás que crear un token de migración en la pila en la nube que hayas elegido.", "body-token-field": "Token de migración", "body-token-field-placeholder": "Pega el token aquí", - "body-token-instructions": "Inicia sesión en tu pila en la nube y ve a Administración > General > Migrar a Grafana Cloud. Crea un token de migración en esa pantalla y pégalo aquí.", + "body-token-instructions": "", "body-view-stacks": "Ver mis pilas en la nube", "cancel": "Cancelar", "connect": "Conectar con esta pila", @@ -5391,11 +5464,12 @@ "body": "El proceso de migración debe iniciarse desde tu instancia autogestionada de Grafana.", "configure-pdc-link": "Configurar PDC para esta pila", "link-title": "Más información sobre Private Data Source Connect", - "step-1": "Inicia sesión en tu instancia autogestionada y ve a Administración > General > Migrar a Grafana Cloud.", + "step-1": "", "step-2": "Selecciona «Migrar esta instancia a Cloud».", - "step-3": "Se te pedirá un token de migración. Genera uno desde esta pantalla.", - "step-4": "En tu instancia autogestionada, selecciona «Subir todo» para subir fuentes de datos y paneles a esta pila en la nube.", - "step-5": "Si algunas de tus fuentes de datos no funcionan a través de la red de Internet pública, deberás instalar Private Data Source Connect en tu entorno autogestionado.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Realizar una migración" }, "is-it-secure": { @@ -5404,7 +5478,7 @@ "title": "¿Es seguro?" }, "migrate-to-this-stack": { - "body": "Puedes migrar algunos recursos de tu instalación autogestionada de Grafana a esta pila en la nube. Para hacerlo de forma segura, deberás generar un token de migración. Tu instancia autogestionada utilizará el token para autenticarse con esta pila en la nube.", + "body": "", "link-title": "Ver la guía de migración completa", "title": "Permítenos ayudarte a migrar a esta pila" }, @@ -5422,7 +5496,7 @@ "modal-close": "Cerrar", "modal-copy-and-close": "Copiar al portapapeles y cerrar", "modal-copy-button": "Copiar al portapapeles", - "modal-field-description": "Copia el token ahora, ya que no lo volverás a ver. Si lo pierdes, deberás crear uno nuevo.", + "modal-field-description": "", "modal-field-label": "Token", "modal-title": "Se ha creado el token de migración", "status": "" @@ -5439,7 +5513,7 @@ "get-session-error-title": "Error al cargar la configuración de migración", "get-snapshot-error-title": "Error al cargar la instantánea", "migration-finished-with-caveat-title": "Migración de recursos completada", - "migration-finished-with-errors-body": "La migración se ha completado, pero algunos elementos no se han podido migrar a la pila en la nube. Comprueba los recursos fallidos para obtener más información", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "La migración se ha completado con algunas advertencias. Consulta los recursos individuales para obtener más información", "snapshot-error-status-body": "Se ha producido un error al crear la instantánea o al iniciar el proceso de migración. Consulta los registros del servidor de Grafana para obtener más información", "snapshot-error-status-title": "Error al migrar los recursos", @@ -5451,7 +5525,7 @@ "pdc": { "body": "Exponer tus fuentes de datos en Internet puede plantear problemas de seguridad. Private Data Source Connect (PDC) permite a Grafana Cloud acceder a tus fuentes de datos a través de un túnel de red seguro.", "link-title": "Infórmate sobre PDC", - "title": "No todas mis fuentes de datos están en la red de Internet pública" + "title": "" }, "pricing": { "body": "Grafana Cloud te ofrece un generoso plan gratuito y una prueba de 14 días de uso ilimitado. Una vez finalizado el período de prueba, se te facturará en función del uso que supere los límites del plan gratuito.", @@ -5529,7 +5603,7 @@ "disconnect": "", "errored-resource-count": "Errores", "page-loading": "", - "rebuild-snapshot": "Volver a crear instantánea", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Marca de tiempo de la instantánea", "snapshot-not-created": "Aún no se ha creado", @@ -5786,7 +5860,7 @@ "subtitle": "Gestionar paneles de control de carpetas y permisos" }, "migrate-to-cloud": { - "subtitle": "Copia la configuración de tu instalación autogestionada a una pila en la nube", + "subtitle": "", "title": "Migrar a Grafana Cloud" }, "monitoring": { @@ -6150,7 +6224,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "No hay actualizaciones disponibles", @@ -6334,6 +6409,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6348,21 +6424,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_other": "" }, @@ -6374,13 +6443,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6391,25 +6458,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6427,12 +6490,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6443,14 +6507,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6488,7 +6548,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6506,19 +6565,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6576,16 +6635,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6615,8 +6675,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6695,30 +6754,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6923,6 +6988,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7200,6 +7266,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7208,8 +7275,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7219,7 +7288,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7228,7 +7299,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7254,6 +7326,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7536,16 +7609,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Mostrar demasiadas columnas en una sola tabla puede afectar al rendimiento y dificultar la lectura de los datos. Considera la posibilidad de afinar tus consultas.", @@ -7568,6 +7646,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7579,24 +7659,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7695,6 +7787,7 @@ "date-created": "Fecha de creación: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7716,11 +7809,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7732,6 +7827,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7752,7 +7848,8 @@ "now": "Ahora:", "previously": "Previamente:", "see-examples": "> Ver ejemplos", - "sentence": "Los histogramas nativos de Prometheus ofrecen alta resolución, alta precisión, un uso sencillo en la instrumentación y una forma de combinar y manipular histogramas en las consultas y en Grafana." + "sentence": "Los histogramas nativos de Prometheus ofrecen alta resolución, alta precisión, un uso sencillo en la instrumentación y una forma de combinar y manipular histogramas en las consultas y en Grafana.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "O ver una exploración reciente" @@ -7760,11 +7857,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Mostrar siempre el gráfico métrico seleccionado", "show-previews-of-metric-graphs": "Mostrar vistas previas de gráficos de métricas" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7802,11 +7903,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7816,6 +7923,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7828,8 +7936,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7856,6 +7976,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7872,8 +7993,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7882,7 +8008,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7895,6 +8023,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7905,6 +8035,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7915,21 +8048,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7959,7 +8103,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7979,11 +8124,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8051,6 +8200,7 @@ "message": "No se han encontrado usuarios" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8065,6 +8215,11 @@ "shared-dashboard-users-tab-title": "Usuarios del panel de control compartido" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Seleccionar valor" @@ -8075,7 +8230,7 @@ "picker": { "link-all": "Todo", "option-all": "Todo", - "option-selected-values": "Seleccionados", + "option-selected-values": "", "option-tooltip": "Eliminar selecciones" }, "textbox": { @@ -8101,6 +8256,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8153,9 +8311,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/fr-FR/grafana.json b/public/locales/fr-FR/grafana.json index 1fb0323b96a..a402ebe7d9c 100644 --- a/public/locales/fr-FR/grafana.json +++ b/public/locales/fr-FR/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -803,7 +806,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -824,7 +828,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -896,7 +901,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -927,6 +933,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -958,6 +965,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -990,6 +1000,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1048,12 +1059,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1315,6 +1329,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1529,7 +1547,8 @@ "label-target-data-source": "Source de données cible" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1805,7 +1824,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1874,7 +1895,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1919,7 +1941,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1944,6 +1968,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1988,7 +2013,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "AU" @@ -2009,6 +2035,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2622,6 +2651,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2697,7 +2727,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2786,7 +2817,8 @@ "rows": "Lignes", "tab": "Onglet", "tabs": "Onglets", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Ouvrir le volet d'options", "row": { @@ -2794,6 +2826,10 @@ "hide": "Masquer", "title": "En-tête de ligne" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2912,13 +2948,13 @@ "title-option": "Titre" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(vide)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3030,7 +3066,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3502,7 +3544,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Chemin du fichier :", - "managed-by-version-control": "Ce {type} est géré par le contrôle de version et ne peut pas être supprimé. Pour le supprimer, supprimez-le du référentiel et synchronisez pour appliquer les modifications.", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "Impossible de supprimer la ressource mise en service" }, @@ -3898,6 +3940,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3923,7 +3966,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4037,6 +4081,10 @@ "description": "Vous cherchez les applications Grafana Explore ? Elles s'appellent désormais les applications Grafana Drilldown et se trouvent sous <1>Menu > Drilldown", "title": "Explore Metrics, Logs, Traces et Profiles ont été déplacés !" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4085,6 +4133,9 @@ "scan-for-older-logs": "Scanner des journaux plus anciens", "stop-scan": "Arrêter le scan" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4111,6 +4162,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4142,6 +4194,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Fermer l'historique des requêtes", "datasource-a-z": "Source de données A-Z", @@ -4269,6 +4324,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4348,8 +4404,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4364,6 +4424,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5208,6 +5269,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Copier le lien vers la ligne de journal", "copy-log": "Copier la ligne de journal", @@ -5260,11 +5325,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5331,14 +5398,20 @@ "when-complete": "Une fois l'instantané terminé, vous pourrez le télécharger sur votre pile cloud." }, "building-snapshot": { - "description": "Nous créons un instantané ponctuel de l'état actuel de cette installation. Une fois l'instantané terminé, vous pourrez le télécharger sur Grafana Cloud.", + "description": "", "description-eta": "La création d'un instantané prend généralement moins de deux minutes.", - "title": "Création d'un instantané de l'installation" + "title": "" }, "can-i-move": { - "body": "Une fois que vous aurez connecté cette installation à une pile cloud, vous pourrez importer les sources de données et les tableaux de bord.", - "link-title": "En savoir plus sur la migration des autres paramètres", - "title": "Puis-je déplacer cette installation vers Grafana Cloud ?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Vous aurez également besoin d'une pile cloud. Si vous venez de vous inscrire, nous créerons automatiquement votre première pile. Si vous avez déjà un compte, vous devez sélectionner ou créer une pile.", @@ -5347,7 +5420,7 @@ "body-token": "Votre installation Grafana autogérée a besoin d'un accès spécial pour migrer du contenu de manière sécurisée. Vous devrez créer un jeton de migration sur la pile cloud que vous avez choisie.", "body-token-field": "Jeton de migration", "body-token-field-placeholder": "Coller le jeton ici", - "body-token-instructions": "Connectez-vous à votre pile cloud et accédez à Administration > Général > Migrer vers Grafana Cloud. Créez un jeton de migration sur cet écran et collez-le ici.", + "body-token-instructions": "", "body-view-stacks": "Voir mes piles cloud", "cancel": "Annuler", "connect": "Se connecter à cette pile", @@ -5391,11 +5464,12 @@ "body": "Le processus de migration doit être démarré à partir de votre instance Grafana autogérée.", "configure-pdc-link": "Configurer la PDC pour cette pile", "link-title": "En savoir plus sur la connexion privée aux sources de données", - "step-1": "Connectez-vous à votre instance autogérée et accédez à Administration > Général > Migrer vers Grafana Cloud.", + "step-1": "", "step-2": "Sélectionnez Migrer cette instance vers le cloud.", - "step-3": "Le système vous demandera un jeton de migration. Générez-en un à partir de cet écran.", - "step-4": "Dans votre instance autogérée, sélectionnez Importer tout pour importer les sources de données et les tableaux de bord vers cette pile cloud.", - "step-5": "Si certaines de vos sources de données ne fonctionnent pas sur le réseau Internet public, vous devez installer une connexion privée aux sources de données dans votre environnement autogéré.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Effectuer une migration" }, "is-it-secure": { @@ -5404,7 +5478,7 @@ "title": "La solution est-elle sécurisée ?" }, "migrate-to-this-stack": { - "body": "Vous pouvez migrer certaines ressources de votre installation Grafana autogérée vers cette pile cloud. Pour le faire en toute sécurité, vous devrez générer un jeton de migration. Votre instance autogérée utilisera ce jeton pour s'authentifier auprès de cette pile cloud.", + "body": "", "link-title": "Voir le guide de migration complet", "title": "Laissez-nous vous aider à migrer vers cette pile" }, @@ -5422,7 +5496,7 @@ "modal-close": "Fermer", "modal-copy-and-close": "Copier dans le presse-papiers et fermer", "modal-copy-button": "Copier dans le presse-papiers", - "modal-field-description": "Copiez le jeton maintenant, car vous ne pourrez plus le revoir. Si vous perdez un jeton, vous devrez 'en créer un nouveau.", + "modal-field-description": "", "modal-field-label": "Jeton", "modal-title": "Jeton de migration créé", "status": "Statut actuel : <2>" @@ -5439,7 +5513,7 @@ "get-session-error-title": "Erreur lors du chargement de la configuration de la migration", "get-snapshot-error-title": "Erreur lors du chargement de l'instantané", "migration-finished-with-caveat-title": "Migration des ressources terminée", - "migration-finished-with-errors-body": "La migration est terminée, mais certains éléments n'ont pas pu être migrés vers la pile cloud. Vérifiez les ressources ayant échoué pour plus de détails", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "La migration est terminée avec quelques avertissements. Vérifiez les ressources individuelles pour plus de détails", "snapshot-error-status-body": "Une erreur s'est produite lors de la création de l'instantané ou du démarrage du processus de migration. Consultez les journaux du serveur Grafana pour plus de détails", "snapshot-error-status-title": "Erreur lors de la migration des ressources", @@ -5451,7 +5525,7 @@ "pdc": { "body": "L'exposition de vos sources de données sur Internet peut compromettre la sécurité. Private data source connect (PDC) permet à Grafana Cloud d'accéder à vos sources de données existantes en passant par un tunnel réseau sécurisé.", "link-title": "En savoir plus sur la PDC", - "title": "Toutes mes sources de données ne sont pas sur le réseau Internet public" + "title": "" }, "pricing": { "body": "Grafana Cloud propose une formule gratuite généreuse et un essai illimité de 14 jours. Après votre période d'essai, nous vous facturerons en fonction de votre utilisation dépassant les limites de la formule gratuite.", @@ -5529,7 +5603,7 @@ "disconnect": "Se déconnecter", "errored-resource-count": "Erreurs", "page-loading": "Chargement en cours...", - "rebuild-snapshot": "Reconstruire l'instantané", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Horodatage de l'instantané", "snapshot-not-created": "Pas encore créé", @@ -5786,7 +5860,7 @@ "subtitle": "Gérer les tableaux de bord et les autorisations des dossiers" }, "migrate-to-cloud": { - "subtitle": "Copier la configuration de votre installation autogérée vers une pile cloud", + "subtitle": "", "title": "Migrer vers Grafana Cloud" }, "monitoring": { @@ -6150,7 +6224,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Aucune mise à jour disponible", @@ -6334,6 +6409,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6348,21 +6424,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_other": "" }, @@ -6374,13 +6443,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6391,25 +6458,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6427,12 +6490,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6443,14 +6507,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6488,7 +6548,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6506,19 +6565,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6576,16 +6635,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6615,8 +6675,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6695,30 +6754,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6923,6 +6988,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7200,6 +7266,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7208,8 +7275,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7219,7 +7288,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7228,7 +7299,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7254,6 +7326,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7536,16 +7609,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Afficher trop de colonnes dans un seul tableau peut affecter les performances et rendre les données plus difficiles à lire. Envisagez d'affiner vos requêtes.", @@ -7568,6 +7646,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7579,24 +7659,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7695,6 +7787,7 @@ "date-created": "Date de création : " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7716,11 +7809,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7732,6 +7827,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7752,7 +7848,8 @@ "now": "Maintenant :", "previously": "Précédemment :", "see-examples": "> Voir des exemples", - "sentence": "Les histogrammes natifs de Prometheus offrent une haute résolution, une haute précision, une utilisation simple dans l'instrumentation et un moyen de combiner et de manipuler les histogrammes dans les requêtes et au sein de Grafana." + "sentence": "Les histogrammes natifs de Prometheus offrent une haute résolution, une haute précision, une utilisation simple dans l'instrumentation et un moyen de combiner et de manipuler les histogrammes dans les requêtes et au sein de Grafana.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Ou consulter une exploration récente" @@ -7760,11 +7857,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Toujours garder le graphique des indicateurs sélectionné en vue", "show-previews-of-metric-graphs": "Afficher les aperçus des graphiques des indicateurs" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7802,11 +7903,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7816,6 +7923,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7828,8 +7936,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7856,6 +7976,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7872,8 +7993,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7882,7 +8008,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7895,6 +8023,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7905,6 +8035,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7915,21 +8048,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7959,7 +8103,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7979,11 +8124,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8051,6 +8200,7 @@ "message": "Aucun utilisateur trouvé" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8065,6 +8215,11 @@ "shared-dashboard-users-tab-title": "Utilisateurs du tableau de bord partagé" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Sélectionner une valeur" @@ -8075,7 +8230,7 @@ "picker": { "link-all": "Tout", "option-all": "Tout", - "option-selected-values": "Sélection", + "option-selected-values": "", "option-tooltip": "Effacer les sélections" }, "textbox": { @@ -8101,6 +8256,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8153,9 +8311,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/hu-HU/grafana.json b/public/locales/hu-HU/grafana.json index d51e9affc6a..c3c7e9dbb94 100644 --- a/public/locales/hu-HU/grafana.json +++ b/public/locales/hu-HU/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -803,7 +806,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -824,7 +828,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -896,7 +901,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -927,6 +933,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -958,6 +965,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -990,6 +1000,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1048,12 +1059,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1315,6 +1329,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1529,7 +1547,8 @@ "label-target-data-source": "Céladatforrás" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1805,7 +1824,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1874,7 +1895,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1919,7 +1941,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1944,6 +1968,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1988,7 +2013,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "EDDIG" @@ -2009,6 +2035,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2622,6 +2651,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2697,7 +2727,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2786,7 +2817,8 @@ "rows": "Sorok", "tab": "Lap", "tabs": "Lapok", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Beállítások ablaktábla megnyitása", "row": { @@ -2794,6 +2826,10 @@ "hide": "Elrejtés", "title": "Sorfejléc" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2912,13 +2948,13 @@ "title-option": "Cím" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(üres)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3030,7 +3066,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3502,7 +3544,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Fájl útvonala:", - "managed-by-version-control": "Ez a(z) {type} a verziókezelés által felügyelt, és nem törölhető. Az eltávolításához törölje az adattárból, és szinkronizálja a változtatások alkalmazásához.", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "Nem lehet törölni a létrehozott erőforrást" }, @@ -3898,6 +3940,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3923,7 +3966,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4037,6 +4081,10 @@ "description": "Grafana Explore-alkalmazásokat keres? Mostantól Grafana Drilldown-alkalmazásoknak hívják őket, és a <1>Menü > Drilldown menüpontban találhatók", "title": "Az Explore Metrics, Logs, Traces és Profiles új helyre költözött!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4085,6 +4133,9 @@ "scan-for-older-logs": "Régebbi naplók keresése", "stop-scan": "Keresés leállítása" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4111,6 +4162,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4142,6 +4194,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Lekérdezési előzmények bezárása", "datasource-a-z": "Adatforrás A–Z", @@ -4269,6 +4324,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4348,8 +4404,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4364,6 +4424,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5208,6 +5269,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Hivatkozás másolása a naplósorba", "copy-log": "Naplósor másolása", @@ -5260,11 +5325,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5331,14 +5398,20 @@ "when-complete": "Miután elkészült a pillanatfelvétel, feltöltheti azt a felhőstackbe." }, "building-snapshot": { - "description": "Létrehozunk egy aktuális pillanatfelvételt a telepített példány aktuális állapotáról. Miután elkészült a pillanatfelvétel, feltöltheti azt a Grafana Cloudba.", + "description": "", "description-eta": "A pillanatfelvétel létrehozása általában kevesebb mint két percet vesz igénybe.", - "title": "Telepített példány pillanatképének létrehozása" + "title": "" }, "can-i-move": { - "body": "Miután csatlakoztatta ezt a telepítést egy felhőstackhez, feltölthet adatforrásokat és irányítópultokat.", - "link-title": "További információ más beállítások áttelepítéséről", - "title": "Áthelyezhetem ezt a telepítést a Grafana Cloudba?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Szüksége lesz egy felhőstackre is. Ha most regisztráltál, automatikusan létrehozzuk az első stackjét. Ha van fiókja, ki kell választania vagy létre kell hoznia egy stacket.", @@ -5347,7 +5420,7 @@ "body-token": "A saját kezelésű Grafana-telepítésnek speciális hozzáférésre van szüksége a tartalom biztonságos áttelepítéséhez. Létre kell hoznia egy áttelepítési tokent a kiválasztott felhőstackben.", "body-token-field": "Áttelepítési token", "body-token-field-placeholder": "Ide kell beilleszteni a tokent", - "body-token-instructions": "Jelentkezzen be a felhőstackbe, és navigáljon az Adminisztráció, Általános, Áttelepítés a Grafana Cloudba menüponthoz. Hozzon létre egy áttelepítési tokent azon a képernyőn, és illessze be ide a tokent.", + "body-token-instructions": "", "body-view-stacks": "Saját felhőstackek megtekintése", "cancel": "Mégse", "connect": "Csatlakozás ehhez a stackhez", @@ -5391,11 +5464,12 @@ "body": "Az áttelepítési folyamatot a saját kezelésű Grafana-példányból kell elindítani.", "configure-pdc-link": "PDC konfigurálása ehhez a stackhez", "link-title": "További információ a Private Data Source Connectről", - "step-1": "Jelentkezzen be az önállóan kezelt példányba, és navigáljon az Adminisztráció, Általános, Áttelepítés a Grafana Cloudba menüponthoz.", + "step-1": "", "step-2": "Válassza a „Példány áttelepítése a felhőbe” lehetőséget.", - "step-3": "Meg kell adnia egy áttelepítési tokent. Hozzon létre egyet erről a képernyőről.", - "step-4": "Az Ön által kezelt példányban válassza az „Összes feltöltése” lehetőséget, hogy adatforrásokat és irányítópultokat töltsön fel ebbe a felhőstackbe.", - "step-5": "Ha néhány adatforrása nem fog működni a nyilvános interneten keresztül, telepítenie kell a Private Data Source Connectet az Ön által kezelt környezetben.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Áttelepítés végrehajtása" }, "is-it-secure": { @@ -5404,7 +5478,7 @@ "title": "Biztonságos?" }, "migrate-to-this-stack": { - "body": "Áttelepíthet bizonyos erőforrásokat az Ön által kezelt Grafana-telepítésből ebbe a felhőstackbe. Ehhez létre kell hoznia egy áttelepítési tokent. Az Ön által kezelt példány a tokent fogja használni a hitelesítéshez ezzel a felhőstackkel.", + "body": "", "link-title": "Tekintse meg a teljes áttelepítési útmutatót", "title": "Segítsünk az erre a stackre történő áttelepítésben" }, @@ -5422,7 +5496,7 @@ "modal-close": "Bezárás", "modal-copy-and-close": "Másolás vágólapra és bezárás", "modal-copy-button": "Másolás vágólapra", - "modal-field-description": "Másolja át most a tokent, mert később nem lesz látható. A token elvesztése esetén új tokent kell létrehozni.", + "modal-field-description": "", "modal-field-label": "Token", "modal-title": "Áttelepítési token létrehozva", "status": "Jelenlegi állapot: <2>" @@ -5439,7 +5513,7 @@ "get-session-error-title": "Hiba történt az áttelepítési konfiguráció betöltésekor", "get-snapshot-error-title": "Hiba történt a pillanatfelvétel betöltésekor", "migration-finished-with-caveat-title": "Erőforrás áttelepítése befejeződött", - "migration-finished-with-errors-body": "Az áttelepítés befejeződött, de néhány elemet nem sikerült áttelepíteni a felhőstackbe. További részletekért tekintse meg a sikertelen erőforrásokat", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "Az áttelepítés néhány figyelmeztetéssel befejeződött. További részletekért tekintse meg az egyes erőforrásokat", "snapshot-error-status-body": "Hiba történt a pillanatfelvétel létrehozása vagy az áttelepítési folyamat indítása során. További részletekért tekintse meg a Grafana-kiszolgáló naplóit", "snapshot-error-status-title": "Hiba történt az erőforrások áttelepítésekor", @@ -5451,7 +5525,7 @@ "pdc": { "body": "Az adatforrások interneten való közzététele biztonsági problémákat vethet fel. A Private Data Source Connect (PDC) lehetővé teszi a Grafana Cloud számára, hogy biztonságos hálózati alagúton keresztül hozzáférjen az Ön meglévő adatforrásaihoz.", "link-title": "További információ a PDC-ről", - "title": "Nem minden adatforrásom található meg a nyilvános interneten" + "title": "" }, "pricing": { "body": "A Grafana Cloudhoz elérhető egy funkciókban bővelkedő ingyenes csomag és egy 14 napos, korlátlanul használható próbaverzió. A próbaidőszak lejárta után az ingyenes csomag korlátain túli használat alapján számlázunk.", @@ -5529,7 +5603,7 @@ "disconnect": "Kapcsolat bontása", "errored-resource-count": "Hibák", "page-loading": "Betöltés...", - "rebuild-snapshot": "Pillanatkép újbóli létrehozása", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Pillanatkép időbélyege", "snapshot-not-created": "Még nincs létrehozva", @@ -5786,7 +5860,7 @@ "subtitle": "Mappa-irányítópultok és engedélyek kezelése" }, "migrate-to-cloud": { - "subtitle": "Konfiguráció másolása a saját kezelésű telepített példányból egy felhőstackbe", + "subtitle": "", "title": "Áttelepítés a Grafana Cloudba" }, "monitoring": { @@ -6150,7 +6224,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Nem érhetők el frissítések", @@ -6334,6 +6409,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6348,21 +6424,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_other": "" }, @@ -6374,13 +6443,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6391,25 +6458,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6427,12 +6490,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6443,14 +6507,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6488,7 +6548,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6506,19 +6565,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6576,16 +6635,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6615,8 +6675,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6695,30 +6754,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6923,6 +6988,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7200,6 +7266,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7208,8 +7275,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7219,7 +7288,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7228,7 +7299,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7254,6 +7326,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7536,16 +7609,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Ha túl sok oszlopot jelenít meg egyetlen táblázatban, az hatással lehet a teljesítményre, és megnehezítheti az adatok olvasását. Mérlegelje a lekérdezések pontosítását.", @@ -7568,6 +7646,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7579,24 +7659,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7695,6 +7787,7 @@ "date-created": "Létrehozás dátuma: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7716,11 +7809,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7732,6 +7827,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7752,7 +7848,8 @@ "now": "Most:", "previously": "Korábban:", "see-examples": "> Példák megtekintése", - "sentence": "A Prometheus-natív hisztogramok nagy felbontást, nagy pontosságot, egyszerű használatot kínálnak az adatok megjelenítésénél, valamint lehetővé teszik a hisztogramok kombinálását és manipulálását a lekérdezésekben és a Grafanában." + "sentence": "A Prometheus-natív hisztogramok nagy felbontást, nagy pontosságot, egyszerű használatot kínálnak az adatok megjelenítésénél, valamint lehetővé teszik a hisztogramok kombinálását és manipulálását a lekérdezésekben és a Grafanában.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Vagy tekintsen meg egy közelmúltbeli felfedezést" @@ -7760,11 +7857,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "A kijelölt metrika grafikonja mindig legyen látható", "show-previews-of-metric-graphs": "Metrikagrafikonok előnézetének megjelenítése" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7802,11 +7903,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7816,6 +7923,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7828,8 +7936,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7856,6 +7976,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7872,8 +7993,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7882,7 +8008,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7895,6 +8023,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7905,6 +8035,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7915,21 +8048,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7959,7 +8103,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7979,11 +8124,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8051,6 +8200,7 @@ "message": "Nincsenek felhasználók" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8065,6 +8215,11 @@ "shared-dashboard-users-tab-title": "Megosztott irányítópult felhasználói" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Érték kijelölése" @@ -8075,7 +8230,7 @@ "picker": { "link-all": "Összes", "option-all": "Összes", - "option-selected-values": "Kijelölve", + "option-selected-values": "", "option-tooltip": "Kijelölés törlése" }, "textbox": { @@ -8101,6 +8256,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8153,9 +8311,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/id-ID/grafana.json b/public/locales/id-ID/grafana.json index 7758f417db6..5b608b53b37 100644 --- a/public/locales/id-ID/grafana.json +++ b/public/locales/id-ID/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -801,7 +804,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -822,7 +826,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -894,7 +899,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -925,6 +931,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -956,6 +963,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -988,6 +998,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1046,12 +1057,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1312,6 +1326,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1524,7 +1542,8 @@ "label-target-data-source": "Sumber data target" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1799,7 +1818,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1868,7 +1889,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1913,7 +1935,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1938,6 +1962,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1982,7 +2007,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "KE" @@ -2003,6 +2029,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2611,6 +2640,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2686,7 +2716,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2775,7 +2806,8 @@ "rows": "Baris", "tab": "Tab", "tabs": "Tab", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Buka panel opsi", "row": { @@ -2783,6 +2815,10 @@ "hide": "Sembunyikan", "title": "Header baris" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2901,13 +2937,13 @@ "title-option": "Judul" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(kosong)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3019,7 +3055,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3491,7 +3533,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Jalur file:", - "managed-by-version-control": "{type} ini dikelola oleh kontrol versi dan tidak dapat dihapus. Untuk menghapusnya, hapus dari repositori dan sinkronkan untuk menerapkan perubahan.", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "Tidak dapat menghapus sumber daya yang disediakan" }, @@ -3887,6 +3929,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3912,7 +3955,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4026,6 +4070,10 @@ "description": "Mencari aplikasi Grafana Explore? Aplikasi ini sekarang disebut sebagai aplikasi Grafana Drilldown dan dapat ditemukan dalam <1>Menu > Drilldown", "title": "Explore Metrics, Logs, Traces, dan Profiles telah dipindahkan!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4074,6 +4122,9 @@ "scan-for-older-logs": "Pindai log yang lebih lama", "stop-scan": "Hentikan pemindaian" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4100,6 +4151,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4131,6 +4183,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Tutup riwayat kueri", "datasource-a-z": "Sumber data A-Z", @@ -4258,6 +4313,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4337,8 +4393,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4353,6 +4413,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5196,6 +5257,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Salin tautan ke baris log", "copy-log": "Salin baris log", @@ -5248,11 +5313,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5319,14 +5386,20 @@ "when-complete": "Setelah snapshot selesai, Anda akan dapat mengunggahnya ke tumpukan cloud Anda." }, "building-snapshot": { - "description": "Kami sedang membuat snapshot titik waktu dari keadaan instalasi saat ini. Setelah snapshot selesai, Anda akan dapat mengunggahnya ke Grafana Cloud.", + "description": "", "description-eta": "Membuat snapshot biasanya membutuhkan waktu kurang dari dua menit. ", - "title": "Menyusun snapshot instalasi" + "title": "" }, "can-i-move": { - "body": "Setelah menghubungkan instalasi ini ke tumpukan cloud, Anda akan dapat mengunggah sumber data dan dasbor.", - "link-title": "Pelajari tentang memigrasikan pengaturan lain", - "title": "Bisakah saya memindahkan instalasi ini ke Grafana Cloud?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Anda juga akan membutuhkan tumpukan cloud. Jika Anda baru saja mendaftar, kami akan secara otomatis membuat tumpukan pertama Anda. Jika sudah punya akun, Anda harus memilih atau membuat tumpukan.", @@ -5335,7 +5408,7 @@ "body-token": "Instalasi Grafana yang dikelola sendiri memerlukan akses khusus untuk memigrasikan konten dengan aman. Anda harus membuat token migrasi di tumpukan cloud yang Anda pilih.", "body-token-field": "Token migrasi", "body-token-field-placeholder": "Tempel token di sini", - "body-token-instructions": "Login ke tumpukan cloud Anda dan buka Administrasi, Umum, Bermigrasi ke Grafana Cloud. Buat token migrasi di layar tersebut dan tempelkan token di sini.", + "body-token-instructions": "", "body-view-stacks": "Lihat tumpukan cloud saya", "cancel": "Batalkan", "connect": "Hubungkan ke tumpukan ini", @@ -5379,11 +5452,12 @@ "body": "Proses migrasi harus dimulai dari instans Grafana yang dikelola sendiri.", "configure-pdc-link": "Konfigurasikan PDC untuk tumpukan ini", "link-title": "Pelajari selengkapnya tentang Private Data Source Connect", - "step-1": "Login ke instans yang dikelola sendiri dan buka Administrasi, Umum, Bermigrasi ke Grafana Cloud.", + "step-1": "", "step-2": "Pilih \"Migrasikan instans ini ke Cloud\".", - "step-3": "Anda akan dimintai token migrasi. Buat dari layar ini.", - "step-4": "Dalam instans yang dikelola sendiri, pilih \"Unggah semuanya\" untuk mengunggah sumber data dan dasbor ke tumpukan cloud ini.", - "step-5": "Jika beberapa sumber data Anda tidak akan berfungsi melalui internet publik, Anda harus menginstal Private Data Source Connect di lingkungan yang dikelola sendiri.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Melakukan migrasi" }, "is-it-secure": { @@ -5392,7 +5466,7 @@ "title": "Apakah aman?" }, "migrate-to-this-stack": { - "body": "Anda dapat memigrasikan beberapa sumber daya dari instalasi Grafana yang dikelola sendiri ke tumpukan cloud ini. Untuk melakukan ini dengan aman, Anda harus membuat token migrasi. Instans yang dikelola sendiri akan menggunakan token untuk mengautentikasi dengan tumpukan cloud ini.", + "body": "", "link-title": "Lihat panduan migrasi lengkap", "title": "Biarkan kami membantu Anda bermigrasi ke tumpukan ini" }, @@ -5410,7 +5484,7 @@ "modal-close": "Tutup", "modal-copy-and-close": "Salin ke papan klip dan tutup", "modal-copy-button": "Salin ke papan klip", - "modal-field-description": "Salin token sekarang karena Anda tidak akan dapat melihatnya lagi. Kehilangan token membutuhkan pembuatan token baru.", + "modal-field-description": "", "modal-field-label": "Token", "modal-title": "Token migrasi dibuat", "status": "Status saat ini: <2>" @@ -5427,7 +5501,7 @@ "get-session-error-title": "Kesalahan saat memuat konfigurasi migrasi", "get-snapshot-error-title": "Kesalahan saat memuat snapshot", "migration-finished-with-caveat-title": "Migrasi sumber daya selesai", - "migration-finished-with-errors-body": "Migrasi telah selesai, tetapi beberapa item tidak dapat dimigrasikan ke tumpukan cloud. Periksa sumber daya yang gagal untuk detail selengkapnya", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "Migrasi telah selesai dengan beberapa peringatan. Periksa sumber daya individu untuk detail selengkapnya", "snapshot-error-status-body": "Terjadi kesalahan saat membuat snapshot atau memulai proses migrasi. Lihat log server Grafana untuk detail selengkapnya", "snapshot-error-status-title": "Kesalahan saat memigrasikan sumber daya", @@ -5439,7 +5513,7 @@ "pdc": { "body": "Memaparkan sumber data Anda ke internet dapat menimbulkan masalah keamanan. Private data source connect (PDC) memungkinkan Grafana Cloud mengakses sumber data yang ada melalui terowongan jaringan yang aman.", "link-title": "Pelajari tentang PDC", - "title": "Tidak semua sumber data saya berada di internet publik" + "title": "" }, "pricing": { "body": "Grafana Cloud memiliki paket gratis dan uji coba penggunaan tanpa batas selama 14 hari. Setelah uji coba berakhir, Anda akan ditagih berdasarkan penggunaan yang melebihi batas paket gratis.", @@ -5517,7 +5591,7 @@ "disconnect": "Putuskan koneksi", "errored-resource-count": "Kesalahan", "page-loading": "Memuat...", - "rebuild-snapshot": "Menyusun kembali snapshot", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Stempel waktu Snapshot", "snapshot-not-created": "Belum dibuat", @@ -5774,7 +5848,7 @@ "subtitle": "Kelola dasbor dan izin folder" }, "migrate-to-cloud": { - "subtitle": "Salin konfigurasi dari instalasi yang dikelola sendiri ke tumpukan cloud", + "subtitle": "", "title": "Bermigrasi ke Grafana Cloud" }, "monitoring": { @@ -6138,7 +6212,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Tidak ada pembaruan yang tersedia", @@ -6322,6 +6397,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6336,21 +6412,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_other": "" }, "check-repository": { @@ -6361,13 +6430,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6378,25 +6445,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6414,12 +6477,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6430,14 +6494,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6475,7 +6535,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6493,19 +6552,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6563,16 +6622,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6602,8 +6662,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6682,30 +6741,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6909,6 +6974,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7183,6 +7249,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7191,8 +7258,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7202,7 +7271,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7211,7 +7282,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7237,6 +7309,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7519,16 +7592,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Menampilkan terlalu banyak kolom dalam satu tabel dapat memengaruhi kinerja dan membuat data lebih sulit dibaca. Pertimbangkan untuk menyempurnakan kueri Anda.", @@ -7551,6 +7629,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7562,24 +7642,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7678,6 +7770,7 @@ "date-created": "Tanggal Diciptakan: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7699,11 +7792,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7715,6 +7810,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7735,7 +7831,8 @@ "now": "Sekarang:", "previously": "Sebelumnya:", "see-examples": "> Lihat contoh", - "sentence": "Histogram asli Prometheus menawarkan resolusi tinggi, presisi tinggi, penggunaan sederhana dalam instrumentasi, serta cara untuk menggabungkan dan memanipulasi histogram dalam kueri dan di Grafana." + "sentence": "Histogram asli Prometheus menawarkan resolusi tinggi, presisi tinggi, penggunaan sederhana dalam instrumentasi, serta cara untuk menggabungkan dan memanipulasi histogram dalam kueri dan di Grafana.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Atau lihat eksplorasi terbaru" @@ -7743,11 +7840,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Selalu pertahankan tampilan grafik metrik yang dipilih", "show-previews-of-metric-graphs": "Tampilkan pratinjau grafik metrik" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7785,11 +7886,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7799,6 +7906,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7811,8 +7919,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7839,6 +7959,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7855,8 +7976,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7865,7 +7991,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7878,6 +8006,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7888,6 +8018,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7898,21 +8031,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7942,7 +8086,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7962,11 +8107,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8034,6 +8183,7 @@ "message": "Pengguna tidak ditemukan" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8048,6 +8198,11 @@ "shared-dashboard-users-tab-title": "Pengguna dasbor bersama" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Pilih nilai" @@ -8058,7 +8213,7 @@ "picker": { "link-all": "Semua", "option-all": "Semua", - "option-selected-values": "Dipilih", + "option-selected-values": "", "option-tooltip": "Hapus pilihan" }, "textbox": { @@ -8084,6 +8239,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8136,9 +8294,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/it-IT/grafana.json b/public/locales/it-IT/grafana.json index 860e12a7e54..80c5815d9bf 100644 --- a/public/locales/it-IT/grafana.json +++ b/public/locales/it-IT/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -803,7 +806,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -824,7 +828,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -896,7 +901,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -927,6 +933,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -958,6 +965,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -990,6 +1000,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1048,12 +1059,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1315,6 +1329,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1529,7 +1547,8 @@ "label-target-data-source": "Origine dati di destinazione" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1805,7 +1824,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1874,7 +1895,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1919,7 +1941,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1944,6 +1968,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1988,7 +2013,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "A" @@ -2009,6 +2035,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2622,6 +2651,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2697,7 +2727,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2786,7 +2817,8 @@ "rows": "Righe", "tab": "Scheda", "tabs": "Schede", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Apri il riquadro delle opzioni", "row": { @@ -2794,6 +2826,10 @@ "hide": "Nascondi", "title": "Intestazione di riga" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2912,13 +2948,13 @@ "title-option": "Titolo" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(vuoto)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3030,7 +3066,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3502,7 +3544,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Percorso file:", - "managed-by-version-control": "Questo {type} è gestito dal controllo della versione e non può essere eliminato. Per rimuoverlo, eliminalo dal repository e sincronizza per applicare le modifiche.", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "Impossibile eliminare la risorsa con provisioning" }, @@ -3898,6 +3940,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3923,7 +3966,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4037,6 +4081,10 @@ "description": "Cerchi le app Grafana Explore? Ora si chiamano app Grafana Drilldown e sono disponibili in <1>Menu > Drilldown", "title": "Le metriche, i registri, le tracce e i profili di esplorazione sono stati spostati!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4085,6 +4133,9 @@ "scan-for-older-logs": "Cerca registri precedenti", "stop-scan": "Interrompi ricerca" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4111,6 +4162,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4142,6 +4194,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Chiudi la cronologia delle query", "datasource-a-z": "Origine dei dati A-Z", @@ -4269,6 +4324,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4348,8 +4404,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4364,6 +4424,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5208,6 +5269,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Copia il link alla riga del registro", "copy-log": "Copia la riga del registro", @@ -5260,11 +5325,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5331,14 +5398,20 @@ "when-complete": "Una volta completata l'istantanea, potrai caricarla sul tuo stack cloud." }, "building-snapshot": { - "description": "Stiamo creando un'istantanea temporizzata dello stato corrente di questa installazione. Una volta completata l'istantanea, potrai caricarla su Grafana Cloud.", + "description": "", "description-eta": "La creazione di un'istantanea richiede in genere meno di due minuti. ", - "title": "Creazione di un'istantanea dell'installazione" + "title": "" }, "can-i-move": { - "body": "Quando questa installazione sarà connessa a uno stack cloud, potrai caricare origini dati e dashboard.", - "link-title": "Ulteriori informazioni sulla migrazione di altre impostazioni", - "title": "Posso spostare questa installazione su Grafana Cloud?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Avrai anche bisogno di uno stack cloud. Se ti sei appena registrato, creeremo automaticamente il tuo primo stack. Se hai un account, dovrai selezionare o creare uno stack.", @@ -5347,7 +5420,7 @@ "body-token": "L'installazione di Grafana autogestita richiede un accesso speciale per eseguire la migrazione dei contenuti in modo sicuro. Dovrai creare un token di migrazione per lo stack cloud scelto.", "body-token-field": "Token di migrazione", "body-token-field-placeholder": "Incolla qui il token", - "body-token-instructions": "Accedi al tuo stack cloud e vai ad Amministrazione, Generale, Migra su Grafana Cloud. Crea un token di migrazione in quella schermata e incolla qui il token.", + "body-token-instructions": "", "body-view-stacks": "Visualizza i miei stack cloud", "cancel": "Annulla", "connect": "Connetti a questo stack", @@ -5391,11 +5464,12 @@ "body": "Il processo di migrazione deve essere avviato dall'istanza di Grafana autogestita.", "configure-pdc-link": "Configura PDC per questo stack", "link-title": "Scopri di più su Private Data Source Connect", - "step-1": "Accedi alla tua istanza autogestita e vai ad Amministrazione, Generale, Migra su Grafana Cloud.", + "step-1": "", "step-2": "Seleziona \"Esegui la migrazione di questa istanza su Cloud\".", - "step-3": "Ti verrà richiesto un token di migrazione. Generane uno da questa schermata.", - "step-4": "Nell'istanza autogestita, seleziona \"Carica tutto\" per caricare origini dati e dashboard in questo stack cloud.", - "step-5": "Se alcune delle tue origini dati non funzionano su Internet pubblico, dovrai installare Private Data Source Connect nel tuo ambiente autogestito.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Esecuzione di una migrazione" }, "is-it-secure": { @@ -5404,7 +5478,7 @@ "title": "È sicuro?" }, "migrate-to-this-stack": { - "body": "Puoi eseguire la migrazione di alcune risorse dalla tua installazione Grafana autogestita a questo stack cloud. Per farlo in modo sicuro, dovrai generare un token di migrazione. La tua istanza autogestita utilizzerà il token per autenticarsi con questo stack cloud.", + "body": "", "link-title": "Visualizza la Guida alla migrazione completa", "title": "Lascia che ti aiutiamo a eseguire la migrazione su questo stack" }, @@ -5422,7 +5496,7 @@ "modal-close": "Chiudi", "modal-copy-and-close": "Copia negli appunti e chiudi", "modal-copy-button": "Copia negli appunti", - "modal-field-description": "Copia il token ora perché non potrai più visualizzarlo. La perdita di un token richiede la creazione di uno nuovo.", + "modal-field-description": "", "modal-field-label": "Token", "modal-title": "Token di migrazione creato", "status": "Stato attuale: <2>" @@ -5439,7 +5513,7 @@ "get-session-error-title": "Errore durante il caricamento della configurazione della migrazione", "get-snapshot-error-title": "Errore durante il caricamento dell'istantanea", "migration-finished-with-caveat-title": "Migrazione delle risorse completata", - "migration-finished-with-errors-body": "La migrazione è stata completata, ma non è stato possibile eseguire la migrazione nello stack cloud di alcuni elementi. Controlla le risorse non riuscite per maggiori dettagli", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "La migrazione è stata completata con alcuni avvisi. Controlla le singole risorse per maggiori dettagli", "snapshot-error-status-body": "Si è verificato un errore durante la creazione dell'istantanea o l'avvio del processo di migrazione. Visualizza i registri del server di Grafana per maggiori dettagli", "snapshot-error-status-title": "Errore durante la migrazione delle risorse", @@ -5451,7 +5525,7 @@ "pdc": { "body": "L'esposizione delle origini dati a Internet può sollevare problemi di sicurezza. Private data source connect (PDC) consente a Grafana Cloud di accedere alle origini dati esistenti mediante un tunnel di rete sicuro.", "link-title": "Ulteriori informazioni su PDC", - "title": "Non tutte le mie origini dati sono su Internet pubblico" + "title": "" }, "pricing": { "body": "Grafana Cloud ha un generoso piano gratuito e una prova di utilizzo illimitato di 14 giorni. Dopo la scadenza del periodo di prova, ti verrà addebitato un costo in base all'utilizzo oltre i limiti del piano gratuito.", @@ -5529,7 +5603,7 @@ "disconnect": "Disconnetti", "errored-resource-count": "Errori", "page-loading": "Caricamento in corso...", - "rebuild-snapshot": "Ricostruisci istantanea", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Timestamp istantanea", "snapshot-not-created": "Creazione non ancora effettuata", @@ -5786,7 +5860,7 @@ "subtitle": "Gestisci dashboard e autorizzazioni delle cartelle" }, "migrate-to-cloud": { - "subtitle": "Copia la configurazione dall'installazione autogestita a uno stack cloud", + "subtitle": "", "title": "Esegui migrazione a Grafana Cloud" }, "monitoring": { @@ -6150,7 +6224,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Non sono disponibili aggiornamenti", @@ -6334,6 +6409,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6348,21 +6424,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_other": "" }, @@ -6374,13 +6443,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6391,25 +6458,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6427,12 +6490,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6443,14 +6507,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6488,7 +6548,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6506,19 +6565,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6576,16 +6635,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6615,8 +6675,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6695,30 +6754,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6923,6 +6988,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7200,6 +7266,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7208,8 +7275,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7219,7 +7288,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7228,7 +7299,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7254,6 +7326,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7536,16 +7609,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "La visualizzazione di troppe colonne in una singola tabella può influire sulle prestazioni e rendere più difficile la lettura dei dati. Valuta di perfezionare le query.", @@ -7568,6 +7646,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7579,24 +7659,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7695,6 +7787,7 @@ "date-created": "Data creazione: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7716,11 +7809,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7732,6 +7827,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7752,7 +7848,8 @@ "now": "Ora:", "previously": "In precedenza:", "see-examples": "> Vedi esempi", - "sentence": "Gli istogrammi nativi di Prometheus offrono alta risoluzione, alta precisione, semplice utilizzo nella strumentazione e un modo per combinare e manipolare gli istogrammi nelle query e in Grafana." + "sentence": "Gli istogrammi nativi di Prometheus offrono alta risoluzione, alta precisione, semplice utilizzo nella strumentazione e un modo per combinare e manipolare gli istogrammi nelle query e in Grafana.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Oppure visualizza un'esplorazione recente" @@ -7760,11 +7857,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Mantieni sempre in vista il grafico della metrica selezionata", "show-previews-of-metric-graphs": "Mostra anteprime dei grafici delle metriche" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7802,11 +7903,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7816,6 +7923,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7828,8 +7936,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7856,6 +7976,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7872,8 +7993,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7882,7 +8008,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7895,6 +8023,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7905,6 +8035,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7915,21 +8048,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7959,7 +8103,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7979,11 +8124,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8051,6 +8200,7 @@ "message": "Nessun utente trovato" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8065,6 +8215,11 @@ "shared-dashboard-users-tab-title": "Utenti con dashboard condiviso" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Seleziona valore" @@ -8075,7 +8230,7 @@ "picker": { "link-all": "Tutti", "option-all": "Tutti", - "option-selected-values": "Selezionato", + "option-selected-values": "", "option-tooltip": "Cancella elementi selezionati" }, "textbox": { @@ -8101,6 +8256,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8153,9 +8311,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/ja-JP/grafana.json b/public/locales/ja-JP/grafana.json index ba85db4ce5f..d7985372085 100644 --- a/public/locales/ja-JP/grafana.json +++ b/public/locales/ja-JP/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -801,7 +804,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -822,7 +826,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -894,7 +899,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -925,6 +931,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -956,6 +963,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -988,6 +998,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1046,12 +1057,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1312,6 +1326,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1524,7 +1542,8 @@ "label-target-data-source": "ターゲットデータソース" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1799,7 +1818,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1868,7 +1889,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1913,7 +1935,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1938,6 +1962,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1982,7 +2007,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "へ" @@ -2003,6 +2029,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2611,6 +2640,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2686,7 +2716,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2775,7 +2806,8 @@ "rows": "行", "tab": "タブ", "tabs": "タブ", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "オプションペインを開く", "row": { @@ -2783,6 +2815,10 @@ "hide": "非表示", "title": "行ヘッダー" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2901,13 +2937,13 @@ "title-option": "タイトル" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(空白)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3019,7 +3055,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3491,7 +3533,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "ファイルパス:", - "managed-by-version-control": "この{type}はバージョン管理によって管理されており、削除できません。削除するには、リポジトリから削除して同期し、変更を適用します。", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "プロビジョニングされたリソースを削除できません" }, @@ -3887,6 +3929,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3912,7 +3955,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4026,6 +4070,10 @@ "description": "Grafana Exploreアプリをお探しですか?これらはGrafanaドリルダウンアプリと呼ばれるようになり、<1>メニュー > ドリルダウンにあります", "title": "メトリック、ログ、トレース、プロフィールの検索が移動しました!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4074,6 +4122,9 @@ "scan-for-older-logs": "古いログをスキャンする", "stop-scan": "スキャンの停止" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4100,6 +4151,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4131,6 +4183,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "クエリ履歴を閉じる", "datasource-a-z": "データソースA-Z", @@ -4258,6 +4313,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4337,8 +4393,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4353,6 +4413,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5196,6 +5257,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "ログ行へのリンクをコピー", "copy-log": "ログ行をコピー", @@ -5248,11 +5313,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5319,14 +5386,20 @@ "when-complete": "スナップショットが完了すると、クラウドスタックにアップロードできるようになります。" }, "building-snapshot": { - "description": "このインストールの現在のステートのポイントインタイムスナップショットを作成しています。スナップショットが完了すると、Grafana クラウドにアップロードできるようになります。", + "description": "", "description-eta": "スナップショットの作成には通常2分未満かかります。", - "title": "インストールスナップショットの構築" + "title": "" }, "can-i-move": { - "body": "このインストールをクラウドスタックに接続すると、データソースとダッシュボードをアップロードできるようになります。", - "link-title": "他の設定の移行について学ぶ", - "title": "このインストールをGrafana クラウドに移行できますか?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "クラウドスタックも必要です。サインアップしたばかりの場合は、最初のスタックが自動的に作成されます。アカウントをお持ちの場合は、スタックを選択するか作成する必要があります。", @@ -5335,7 +5408,7 @@ "body-token": "自己管理のGrafanaインストールには、コンテンツを安全に移行するための特別なアクセスが必要です。選択したクラウドスタックで移行トークンを作成する必要があります。", "body-token-field": "移行トークン", "body-token-field-placeholder": "ここにトークンを貼り付けてください", - "body-token-instructions": "クラウドスタックにログインし、[管理]、[一般]、[Grafana クラウドに移行]に移動します。その画面で移行トークンを作成し、そのトークンをここに貼り付けます。", + "body-token-instructions": "", "body-view-stacks": "クラウドスタックを表示", "cancel": "キャンセル", "connect": "このスタックに接続する", @@ -5379,11 +5452,12 @@ "body": "移行プロセスは、自己管理のGrafanaインスタンスから開始する必要があります。", "configure-pdc-link": "このスタックのPDCを構成する", "link-title": "プライベートデータソース接続の詳細", - "step-1": "自己管理インスタンスにログインし、[管理]、[一般]、[Grafana クラウドに移行]に移動します。", + "step-1": "", "step-2": "[このインスタンスをクラウドに移行する]を選択します。", - "step-3": "移行トークンの入力を求められます。この画面から1つ生成します。", - "step-4": "自己管理インスタンスで、[すべてアップロード]を選択して、データソースとダッシュボードをこのクラウドスタックにアップロードします。", - "step-5": "データソースの一部がパブリックインターネット上で機能しない場合は、自己管理環境にプライベートデータソース接続をインストールする必要があります。", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "移行を実行する" }, "is-it-secure": { @@ -5392,7 +5466,7 @@ "title": "安全ですか?" }, "migrate-to-this-stack": { - "body": "自己管理のGrafanaインストールからこのクラウドスタックに一部のリソースを移行できます。これを安全に行うには、移行トークンを生成する必要があります。自己管理インスタンスは、このクラウドスタックでの認証にトークンを使用します。", + "body": "", "link-title": "完全な移行ガイドを表示", "title": "このスタックへの移行をお手伝いします" }, @@ -5410,7 +5484,7 @@ "modal-close": "閉じる", "modal-copy-and-close": "クリップボードにコピーして閉じる", "modal-copy-button": "クリップボードにコピー", - "modal-field-description": "トークンはもう表示できなくなるため、今すぐコピーしてください。トークンを失うと、新しいトークンを作成する必要があります。", + "modal-field-description": "", "modal-field-label": "トークン", "modal-title": "移行トークンが作成されました", "status": "現在のステータス:<2>" @@ -5427,7 +5501,7 @@ "get-session-error-title": "移行構成の読み込み中にエラーが発生しました", "get-snapshot-error-title": "スナップショットの読み込み中にエラーが発生しました", "migration-finished-with-caveat-title": "リソースの移行が完了しました", - "migration-finished-with-errors-body": "移行は完了しましたが、一部のアイテムをクラウドスタックに移行できませんでした。詳細については、失敗したリソースを確認してください", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "移行はいくつかの警告で完了しました。詳細については、個々のリソースを確認してください", "snapshot-error-status-body": "スナップショットの作成または移行プロセスの開始中にエラーが発生しました。詳細については、Grafanaサーバーログを参照してください。", "snapshot-error-status-title": "リソースの移行中にエラーが発生しました", @@ -5439,7 +5513,7 @@ "pdc": { "body": "データソースをインターネットに公開すると、セキュリティ上の懸念が生じる可能性があります。プライベートデータソース接続(PDC)を使用すると、Grafana クラウドは安全なネットワークトンネルを介して既存のデータソースにアクセスできます。", "link-title": "PDCについて学ぶ", - "title": "すべてのデータソースがパブリックインターネット上にあるわけではありません" + "title": "" }, "pricing": { "body": "Grafana クラウドには、寛大な無料プランと14日間の無制限使用トライアルがあります。トライアルの有効期限が切れると、無料プランの制限を超えた使用量に基づいて請求されます。", @@ -5517,7 +5591,7 @@ "disconnect": "切断する", "errored-resource-count": "エラー", "page-loading": "読み込み中...", - "rebuild-snapshot": "スナップショットを再構築", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "スナップショットのタイムスタンプ", "snapshot-not-created": "まだ作成されていません", @@ -5774,7 +5848,7 @@ "subtitle": "フォルダのダッシュボードと権限を管理する" }, "migrate-to-cloud": { - "subtitle": "自己管理インストールからクラウドスタックに構成をコピーする", + "subtitle": "", "title": "Grafana クラウドに移行する" }, "monitoring": { @@ -6138,7 +6212,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "更新が見つかりませんでした", @@ -6322,6 +6397,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6336,21 +6412,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_other": "" }, "check-repository": { @@ -6361,13 +6430,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6378,25 +6445,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6414,12 +6477,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6430,14 +6494,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6475,7 +6535,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6493,19 +6552,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6563,16 +6622,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6602,8 +6662,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6682,30 +6741,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6909,6 +6974,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7183,6 +7249,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7191,8 +7258,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7202,7 +7271,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7211,7 +7282,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7237,6 +7309,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7519,16 +7592,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "1つのテーブルで多すぎる列を表示すると、パフォーマンスに影響を与え、データの読み取りが困難になる可能性があります。クエリを絞り込むことを検討してください。", @@ -7551,6 +7629,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7562,24 +7642,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7678,6 +7770,7 @@ "date-created": "作成日:" }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7699,11 +7792,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7715,6 +7810,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7735,7 +7831,8 @@ "now": "今:", "previously": "以前:", "see-examples": "> 例を見る", - "sentence": "Prometheusのネイティブヒストグラムは、高分解能、高精度、計測におけるシンプルな使い方、クエリとGrafanaでのヒストグラムの組み合わせと操作の方法を提供します。" + "sentence": "Prometheusのネイティブヒストグラムは、高分解能、高精度、計測におけるシンプルな使い方、クエリとGrafanaでのヒストグラムの組み合わせと操作の方法を提供します。", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "または最近の探索を表示" @@ -7743,11 +7840,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "選択したメトリックグラフを常にビューに表示する", "show-previews-of-metric-graphs": "メトリックグラフのプレビューを表示" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7785,11 +7886,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7799,6 +7906,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7811,8 +7919,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7839,6 +7959,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7855,8 +7976,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7865,7 +7991,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7878,6 +8006,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7888,6 +8018,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7898,21 +8031,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7942,7 +8086,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7962,11 +8107,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8034,6 +8183,7 @@ "message": "ユーザーが見つかりません" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8048,6 +8198,11 @@ "shared-dashboard-users-tab-title": "共有ダッシュボードのユーザー" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "値を選択" @@ -8058,7 +8213,7 @@ "picker": { "link-all": "すべて", "option-all": "すべて", - "option-selected-values": "選択中", + "option-selected-values": "", "option-tooltip": "選択のクリア" }, "textbox": { @@ -8084,6 +8239,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8136,9 +8294,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/ko-KR/grafana.json b/public/locales/ko-KR/grafana.json index 732c1be2b84..e94bd6afb2c 100644 --- a/public/locales/ko-KR/grafana.json +++ b/public/locales/ko-KR/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -801,7 +804,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -822,7 +826,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -894,7 +899,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -925,6 +931,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -956,6 +963,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -988,6 +998,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1046,12 +1057,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1312,6 +1326,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1524,7 +1542,8 @@ "label-target-data-source": "대상 데이터 소스" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1799,7 +1818,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1868,7 +1889,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1913,7 +1935,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1938,6 +1962,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1982,7 +2007,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "다음으로 설정" @@ -2003,6 +2029,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2611,6 +2640,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2686,7 +2716,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2775,7 +2806,8 @@ "rows": "행", "tab": "탭", "tabs": "탭", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "옵션 창 열기", "row": { @@ -2783,6 +2815,10 @@ "hide": "숨기기", "title": "행 헤더" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2901,13 +2937,13 @@ "title-option": "제목" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(비어 있음)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3019,7 +3055,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3491,7 +3533,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "파일 경로:", - "managed-by-version-control": "이 {type}은(는) 버전 제어에 의해 관리되며 삭제할 수 없습니다. 제거하려면 리포지토리에서 삭제하고 동기화하여 변경 사항을 적용하세요.", + "managed-by-version-control": "", "ok": "확인", "title-cannot-delete-provisioned-resource": "프로비저닝된 리소스는 삭제할 수 없습니다." }, @@ -3887,6 +3929,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3912,7 +3955,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4026,6 +4070,10 @@ "description": "Grafana 탐색 앱을 찾고 계신가요? 이제 Grafana 드릴다운 앱이라고 불리며, <1>메뉴 > 드릴다운에서 찾을 수 있습니다.", "title": "Explore Metrics, Logs, Traces 및 Profiles 위치가 변경되었습니다!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4074,6 +4122,9 @@ "scan-for-older-logs": "이전 로그 검색", "stop-scan": "스캔 중지" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4100,6 +4151,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4131,6 +4183,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "쿼리 이력을 닫습니다.", "datasource-a-z": "데이터 소스 A-Z", @@ -4258,6 +4313,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4337,8 +4393,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4353,6 +4413,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5196,6 +5257,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "링크를 로그 라인에 복사", "copy-log": "로그 라인 복사", @@ -5248,11 +5313,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5319,14 +5386,20 @@ "when-complete": "스냅샷이 완료되면 클라우드 스택에 업로드할 수 있습니다." }, "building-snapshot": { - "description": "이 설치 인스턴스의 현재 상태에 대해 지정된 시점의 스냅샷을 생성하고 있습니다. 스냅샷이 완료되면 Grafana Cloud에 업로드할 수 있습니다.", + "description": "", "description-eta": "스냅샷을 생성하는 데는 일반적으로 2분이 채 걸리지 않습니다.", - "title": "설치 인스턴스 스냅샷 구축" + "title": "" }, "can-i-move": { - "body": "이 설치를 클라우드 스택에 연결하면 데이터 소스와 대시보드를 업로드할 수 있습니다.", - "link-title": "다른 설정 마이그레이션하는 방법 자세히 알아보기", - "title": "이 설치를 Grafana Cloud로 이동할 수 있나요?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "이를 위해서는 클라우드 스택도 필요합니다. 방금 가입하셨다면 첫 번째 스택이 자동으로 생성됩니다. 계정이 있는 경우 스택을 선택하거나 생성해야 합니다.", @@ -5335,7 +5408,7 @@ "body-token": "자체 관리 Grafana 설치는 안전한 콘텐츠 마이그레이션을 위한 특별한 액세스 권한이 필요합니다. 선택한 클라우드 스택에서 마이그레이션 토큰을 생성해야 합니다.", "body-token-field": "마이그레이션 토큰", "body-token-field-placeholder": "여기에 토큰 붙여넣기", - "body-token-instructions": "클라우드 스택에 로그인하고 '관리', '일반', 'Grafana Cloud로 마이그레이션'으로 이동하세요. 해당 화면에서 마이그레이션 토큰을 생성하고 여기에 토큰을 붙여넣으세요.", + "body-token-instructions": "", "body-view-stacks": "내 클라우드 스택 보기", "cancel": "취소", "connect": "이 스택에 연결", @@ -5379,11 +5452,12 @@ "body": "마이그레이션 프로세스는 자체 관리 Grafana 인스턴스에서 시작해야 합니다.", "configure-pdc-link": "이 스택에 대해 PDC 구성하기", "link-title": "Private Data Source Connect(PDC)에 대해 자세히 알아보기", - "step-1": "자체 관리 인스턴스에 로그인하고 '관리', '일반', 'Grafana Cloud로 마이그레이션'으로 이동하세요.", + "step-1": "", "step-2": "'이 인스턴스를 클라우드로 마이그레이션'을 선택하세요.", - "step-3": "그러면 마이그레이션 토큰을 입력하라는 메시지가 표시됩니다. 이 화면에서 토큰을 생성하세요.", - "step-4": "자체 관리 인스턴스에서 '모두 업로드'를 선택하여 데이터 소스와 대시보드를 이 클라우드 스택에 업로드하세요.", - "step-5": "일부 데이터 소스가 공개 인터넷에서 작동하지 않는 경우 자체 관리 환경에서 비공개 데이터 소스 연결(PDC)을 설치해야 합니다.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "마이그레이션 수행하기" }, "is-it-secure": { @@ -5392,7 +5466,7 @@ "title": "안전한가요?" }, "migrate-to-this-stack": { - "body": "자체 관리 Grafana 설치 인스턴스에서 이 클라우드 스택으로 일부 리소스를 마이그레이션할 수 있습니다. 이를 안전하게 수행하려면 마이그레이션 토큰을 생성해야 합니다. 자체 관리 인스턴스는 이 클라우드 스택의 인증을 위해 토큰을 사용합니다.", + "body": "", "link-title": "전체 마이그레이션 가이드 보기", "title": "저희가 이 스택으로 마이그레이션할 수 있도록 도와드리겠습니다." }, @@ -5410,7 +5484,7 @@ "modal-close": "닫기", "modal-copy-and-close": "클립보드로 복사하고 닫기", "modal-copy-button": "클립보드로 복사", - "modal-field-description": "토큰이 다시 표시되지 않으므로 지금 복사해 두세요. 토큰을 분실하면 새 토큰을 생성해야 합니다.", + "modal-field-description": "", "modal-field-label": "토큰", "modal-title": "마이그레이션 토큰 생성됨", "status": "현재 상태: <2>" @@ -5427,7 +5501,7 @@ "get-session-error-title": "마이그레이션 구성 로딩 중 오류 발생", "get-snapshot-error-title": "스냅샷 로딩 중 오류 발생", "migration-finished-with-caveat-title": "리소스 마이그레이션 완료", - "migration-finished-with-errors-body": "마이그레이션이 완료되었지만 일부 항목을 클라우드 스택으로 마이그레이션하지 못했습니다. 자세한 내용은 실패한 리소스를 확인하세요.", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "경고가 몇 건 발생한 상태에서 마이그레이션이 완료되었습니다. 자세한 내용은 개별 리소스를 확인하세요.", "snapshot-error-status-body": "스냅샷을 생성하거나 마이그레이션 프로세스를 시작하는 중에 오류가 발생했습니다. 자세한 내용은 Grafana 서버 로그를 참고하세요.", "snapshot-error-status-title": "리소스 마이그레이션 중 오류 발생", @@ -5439,7 +5513,7 @@ "pdc": { "body": "데이터 소스를 인터넷에 노출하면 보안 문제가 발생할 수 있습니다. 비공개 데이터 소스 연결(PDC)을 사용하면 Grafana Cloud가 보안 네트워크 터널을 통해 기존 데이터 소스에 액세스할 수 있습니다.", "link-title": "PDC에 대해 알아보기", - "title": "모든 데이터 소스가 공개 인터넷에 있지 않은 경우" + "title": "" }, "pricing": { "body": "Grafana Cloud에는 넉넉한 무료 플랜과 14일 무제한 사용 체험판이 있습니다. 체험판이 만료된 후에는 무료 플랜 한도를 기준으로 초과한 사용량만큼 요금이 청구됩니다.", @@ -5517,7 +5591,7 @@ "disconnect": "연결 해제", "errored-resource-count": "오류", "page-loading": "로딩 중...", - "rebuild-snapshot": "스냅샷 재구축", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "스냅샷 타임스탬프", "snapshot-not-created": "아직 생성되지 않음", @@ -5774,7 +5848,7 @@ "subtitle": "폴더 대시보드 및 권한 관리" }, "migrate-to-cloud": { - "subtitle": "자체 관리 설치 인스턴스에서 클라우드 스택으로 구성 복사", + "subtitle": "", "title": "Grafana Cloud로 마이그레이션" }, "monitoring": { @@ -6138,7 +6212,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "사용 가능 업데이트 없음", @@ -6322,6 +6397,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6336,21 +6412,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_other": "" }, "check-repository": { @@ -6361,13 +6430,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6378,25 +6445,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6414,12 +6477,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6430,14 +6494,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6475,7 +6535,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6493,19 +6552,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6563,16 +6622,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6602,8 +6662,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6682,30 +6741,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6909,6 +6974,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7183,6 +7249,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7191,8 +7258,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7202,7 +7271,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7211,7 +7282,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7237,6 +7309,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7519,16 +7592,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "하나의 표에 열을 너무 많이 표시하면 성능에 영향을 주고 데이터를 읽기 힘들어질 수 있습니다. 쿼리를 좁혀 보세요.", @@ -7551,6 +7629,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7562,24 +7642,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7678,6 +7770,7 @@ "date-created": "생성 날짜: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7699,11 +7792,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7715,6 +7810,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7735,7 +7831,8 @@ "now": "현재:", "previously": "이전: ", "see-examples": "> 예시 보기", - "sentence": "Prometheus 기본 히스토그램은 고해상도, 고정밀도, 간단한 계측 사용법을 제공하며, 쿼리 및 Grafana에서 히스토그램을 결합하고 조작하는 방법을 제공합니다." + "sentence": "Prometheus 기본 히스토그램은 고해상도, 고정밀도, 간단한 계측 사용법을 제공하며, 쿼리 및 Grafana에서 히스토그램을 결합하고 조작하는 방법을 제공합니다.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "또는 최근 탐색 보기" @@ -7743,11 +7840,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "선택한 메트릭 그래프가 항상 화면에 보이도록 표시", "show-previews-of-metric-graphs": "메트릭 그래프 미리 보기 표시" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7785,11 +7886,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7799,6 +7906,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7811,8 +7919,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7839,6 +7959,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7855,8 +7976,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7865,7 +7991,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7878,6 +8006,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7888,6 +8018,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7898,21 +8031,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7942,7 +8086,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7962,11 +8107,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8034,6 +8183,7 @@ "message": "찾은 사용자 없음" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8048,6 +8198,11 @@ "shared-dashboard-users-tab-title": "공유 대시보드 사용자" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "값 선택" @@ -8058,7 +8213,7 @@ "picker": { "link-all": "전체", "option-all": "전체", - "option-selected-values": "선택됨", + "option-selected-values": "", "option-tooltip": "선택 항목 지우기" }, "textbox": { @@ -8084,6 +8239,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8136,9 +8294,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/nl-NL/grafana.json b/public/locales/nl-NL/grafana.json index 948d3d22a77..238105e693f 100644 --- a/public/locales/nl-NL/grafana.json +++ b/public/locales/nl-NL/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -803,7 +806,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -824,7 +828,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -896,7 +901,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -927,6 +933,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -958,6 +965,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -990,6 +1000,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1048,12 +1059,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1315,6 +1329,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1529,7 +1547,8 @@ "label-target-data-source": "Doelgegevensbron" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1805,7 +1824,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1874,7 +1895,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1919,7 +1941,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1944,6 +1968,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1988,7 +2013,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "TO" @@ -2009,6 +2035,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2622,6 +2651,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2697,7 +2727,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2786,7 +2817,8 @@ "rows": "Rijen", "tab": "Tabblad", "tabs": "Tabbladen", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Deelvenster Opties openen", "row": { @@ -2794,6 +2826,10 @@ "hide": "Verbergen", "title": "Rijkoptekst" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2912,13 +2948,13 @@ "title-option": "Titel" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(leeg)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3030,7 +3066,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3502,7 +3544,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Bestandspad:", - "managed-by-version-control": "Dit {type} wordt beheerd door versiebeheer en kan niet worden verwijderd. Om het te verwijderen, verwijder je het uit de repository en synchroniseer je het om de wijzigingen toe te passen.", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "Kan provisioned bron niet verwijderen" }, @@ -3898,6 +3940,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3923,7 +3966,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4037,6 +4081,10 @@ "description": "Op zoek naar de Grafana Explore-apps? Ze worden nu de Grafana Drilldown-apps genoemd en zijn te vinden onder <1>Menu> Drilldown", "title": "Explore Metrics, Logs, Traces en Profiles zijn verplaatst!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4085,6 +4133,9 @@ "scan-for-older-logs": "Scannen op oudere logboeken", "stop-scan": "Scan stoppen" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4111,6 +4162,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4142,6 +4194,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Querygeschiedenis sluiten", "datasource-a-z": "Databron A-Z", @@ -4269,6 +4324,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4348,8 +4404,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4364,6 +4424,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5208,6 +5269,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Link naar logboekregel kopiëren", "copy-log": "Logboekregel kopiëren", @@ -5260,11 +5325,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5331,14 +5398,20 @@ "when-complete": "Zodra de snapshot is voltooid, kun je deze uploaden naar je cloudstack." }, "building-snapshot": { - "description": "We maken een snapshot van de huidige staat van deze installatie. Zodra de snapshot is voltooid, kun je deze uploaden naar Grafana Cloud.", + "description": "", "description-eta": "Het maken van een snapshot duurt meestal minder dan twee minuten.", - "title": "Snapshot van de installatie wordt gemaakt" + "title": "" }, "can-i-move": { - "body": "Zodra je deze installatie aansluit op een cloudstack, kun je gegevensbronnen en dashboards uploaden.", - "link-title": "Meer informatie over het migreren van andere instellingen", - "title": "Kan ik deze installatie verplaatsen naar Grafana Cloud?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Je hebt ook een cloudstack nodig. Als je je net hebt geregistreerd, maken we automatisch je eerste stack aan. Als je een account hebt, moet je een stack selecteren of maken.", @@ -5347,7 +5420,7 @@ "body-token": "Je zelfbeheerde Grafana-installatie heeft speciale toegang nodig om inhoud veilig te migreren. Je moet een migratietoken maken op de door jou gekozen cloudstack.", "body-token-field": "Migratietoken", "body-token-field-placeholder": "Token hier plakken", - "body-token-instructions": "Log in op je cloudstack en navigeer naar Administratie, Algemeen, Migreren naar Grafana Cloud. Maak een migratietoken op dat scherm en plak het token hier.", + "body-token-instructions": "", "body-view-stacks": "Mijn cloudstacks bekijken", "cancel": "Annuleren", "connect": "Verbinden met deze stack", @@ -5391,11 +5464,12 @@ "body": "Het migratieproces moet worden gestart vanuit je zelfbeheerde Grafana-instantie.", "configure-pdc-link": "PDC configureren voor deze stack", "link-title": "Meer informatie over Private Data Source Connect", - "step-1": "Log in op je zelfbeheerde instantie en navigeer naar Administratie, Algemeen, Migreren naar Grafana Cloud.", + "step-1": "", "step-2": "Selecteer 'Deze instantie naar de cloud migreren'.", - "step-3": "Je wordt gevraagd om een migratietoken. Genereer er een vanuit dit scherm.", - "step-4": "Selecteer in je zelfbeheerde instantie 'Alles uploaden' om gegevensbronnen en dashboards naar deze cloudstack te uploaden.", - "step-5": "Als sommige van je gegevensbronnen niet via het openbare internet werken, moet je Private Data Source Connect installeren in je zelfbeheerde omgeving.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Een migratie uitvoeren" }, "is-it-secure": { @@ -5404,7 +5478,7 @@ "title": "Is het veilig?" }, "migrate-to-this-stack": { - "body": "Je kunt sommige bronnen van je zelfbeheerde Grafana-installatie naar deze cloudstack migreren. Om dit veilig te doen, moet je een migratietoken genereren. Je zelfbeheerde instantie gebruikt het token om te verifiëren met deze cloudstack.", + "body": "", "link-title": "Bekijk de volledige migratiehandleiding", "title": "Laat ons je helpen migreren naar deze stack" }, @@ -5422,7 +5496,7 @@ "modal-close": "Sluiten", "modal-copy-and-close": "Kopiëren naar klembord en sluiten", "modal-copy-button": "Kopiëren naar klembord", - "modal-field-description": "Kopieer het token nu, want hierna kun je het niet meer zien. Als je een token verliest, moet je een nieuw token maken.", + "modal-field-description": "", "modal-field-label": "Token", "modal-title": "Migratietoken aangemaakt", "status": "Huidige status: <2>" @@ -5439,7 +5513,7 @@ "get-session-error-title": "Fout bij laden van migratieconfiguratie", "get-snapshot-error-title": "Fout bij laden snapshot", "migration-finished-with-caveat-title": "Bronmigratie voltooid", - "migration-finished-with-errors-body": "De migratie is voltooid, maar sommige items konden niet naar de cloudstack worden gemigreerd. Controleer de mislukte bronnen voor meer informatie", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "De migratie is voltooid met enkele waarschuwingen. Controleer individuele bronnen voor meer informatie", "snapshot-error-status-body": "Er is een fout opgetreden bij het maken van het snapshot of het starten van het migratieproces. Zie de Grafana-serverlogboeken voor meer informatie", "snapshot-error-status-title": "Fout bij migreren van bronnen", @@ -5451,7 +5525,7 @@ "pdc": { "body": "Het blootstellen van je gegevensbronnen aan het internet kan beveiligingsproblemen veroorzaken. Met Private Data Source Connect (PDC) heeft Grafana Cloud toegang tot je bestaande gegevensbronnen via een beveiligde netwerktunnel.", "link-title": "Meer informatie over PDC", - "title": "Niet al mijn gegevensbronnen staan op het openbare internet" + "title": "" }, "pricing": { "body": "Grafana Cloud heeft een royaal gratis abonnement en een proefperiode van 14 dagen voor onbeperkt gebruik. Zodra je proefperiode verloopt, betaal je voor gebruik dat de grenzen van het gratis plan overschrijdt.", @@ -5529,7 +5603,7 @@ "disconnect": "Verbinding verbreken", "errored-resource-count": "Fouten", "page-loading": "Bezig met laden ...", - "rebuild-snapshot": "Snapshot opnieuw maken", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Snapshottijdstempel", "snapshot-not-created": "Nog niet gemaakt", @@ -5786,7 +5860,7 @@ "subtitle": "Mapdashboards en toestemmingen beheren" }, "migrate-to-cloud": { - "subtitle": "Kopieer de configuratie van je zelfbeheerde installatie naar een cloudstack", + "subtitle": "", "title": "Migreren naar Grafana Cloud" }, "monitoring": { @@ -6150,7 +6224,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Geen updates beschikbaar", @@ -6334,6 +6409,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6348,21 +6424,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_other": "" }, @@ -6374,13 +6443,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6391,25 +6458,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6427,12 +6490,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6443,14 +6507,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6488,7 +6548,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6506,19 +6565,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6576,16 +6635,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6615,8 +6675,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6695,30 +6754,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6923,6 +6988,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7200,6 +7266,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7208,8 +7275,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7219,7 +7288,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7228,7 +7299,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7254,6 +7326,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7536,16 +7609,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Het weergeven van te veel kolommen in een enkele tabel kan de prestaties beïnvloeden en gegevens moeilijker leesbaar maken. Overweeg om je query's te verfijnen.", @@ -7568,6 +7646,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7579,24 +7659,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7695,6 +7787,7 @@ "date-created": "Aangemaakt op: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7716,11 +7809,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7732,6 +7827,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7752,7 +7848,8 @@ "now": "Nu:", "previously": "Voordien:", "see-examples": "> Zie voorbeelden", - "sentence": "Prometheus native histogrammen bieden een hoge resolutie, hoge precisie, eenvoudig gebruik in instrumentatie en een manier om histogrammen in query's en in Grafana te combineren en te manipuleren." + "sentence": "Prometheus native histogrammen bieden een hoge resolutie, hoge precisie, eenvoudig gebruik in instrumentatie en een manier om histogrammen in query's en in Grafana te combineren en te manipuleren.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Of bekijk een recente verkenning" @@ -7760,11 +7857,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Houd de geselecteerde metrische grafiek altijd in beeld", "show-previews-of-metric-graphs": "Toon voorbeelden van metrische grafieken" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7802,11 +7903,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7816,6 +7923,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7828,8 +7936,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7856,6 +7976,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7872,8 +7993,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7882,7 +8008,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7895,6 +8023,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7905,6 +8035,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7915,21 +8048,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7959,7 +8103,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7979,11 +8124,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8051,6 +8200,7 @@ "message": "Geen gebruikers gevonden" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8065,6 +8215,11 @@ "shared-dashboard-users-tab-title": "Gebruikers van gedeeld dashboard" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Waarde selecteren" @@ -8075,7 +8230,7 @@ "picker": { "link-all": "Alle", "option-all": "Alle", - "option-selected-values": "Geselecteerd", + "option-selected-values": "", "option-tooltip": "Selecties wissen" }, "textbox": { @@ -8101,6 +8256,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8153,9 +8311,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/pl-PL/grafana.json b/public/locales/pl-PL/grafana.json index 2963e1a628c..85594b8bef3 100644 --- a/public/locales/pl-PL/grafana.json +++ b/public/locales/pl-PL/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -807,7 +810,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -828,7 +832,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -900,7 +905,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -931,6 +937,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -962,6 +969,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -994,6 +1004,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1052,12 +1063,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1321,6 +1335,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1539,7 +1557,8 @@ "label-target-data-source": "Docelowe źródło danych" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1817,7 +1836,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1886,7 +1907,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1931,7 +1953,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1956,6 +1980,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -2000,7 +2025,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "DO" @@ -2021,6 +2047,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2644,6 +2673,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2719,7 +2749,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2808,7 +2839,8 @@ "rows": "Wiersze", "tab": "Karta", "tabs": "Karty", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Otwórz okno opcji", "row": { @@ -2816,6 +2848,10 @@ "hide": "Ukryj", "title": "Nagłówek wiersza" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2934,13 +2970,13 @@ "title-option": "Tytuł" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(puste)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3052,7 +3088,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3524,7 +3566,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Ścieżka pliku:", - "managed-by-version-control": "Ten {type} jest zarządzany z poziomu kontroli wersji i nie można go usunąć. Aby go usunąć, usuń go z repozytorium i zsynchronizuj, aby zastosować zmiany.", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "Nie można usunąć skonfigurowanego zasobu" }, @@ -3920,6 +3962,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3945,7 +3988,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4059,6 +4103,10 @@ "description": "Szukasz aplikacji Grafana Explore? Teraz nazywają się Grafana Drilldown i można je znaleźć w <1>Menu > Drilldown", "title": "Funkcje „Eksploruj metryki”, „Logi”, „Śledzenie” i „Profile” zostały przeniesione." }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4107,6 +4155,9 @@ "scan-for-older-logs": "Skanuj w poszukiwaniu starszych logów", "stop-scan": "Zatrzymaj skanowanie" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4133,6 +4184,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4164,6 +4216,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Zamknij historię zapytań", "datasource-a-z": "Źródło danych A–Z", @@ -4291,6 +4346,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4370,8 +4426,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4386,6 +4446,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5232,6 +5293,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Kopiuj link do wiersza logu", "copy-log": "Kopiuj wiersz logu", @@ -5284,11 +5349,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5355,14 +5422,20 @@ "when-complete": "Po zrobieniu migawki będzie można ją przesłać do stosu w chmurze." }, "building-snapshot": { - "description": "Tworzymy migawkę aktualnego stanu tej instalacji. Po zrobieniu migawki będzie można ją przesłać do Grafana Cloud.", + "description": "", "description-eta": "Utworzenie migawki zajmuje zwykle mniej niż dwie minuty.", - "title": "Tworzenie migawki instalacji" + "title": "" }, "can-i-move": { - "body": "Po podłączeniu tej instalacji do stosu w chmurze będziesz mieć możliwość przesyłania źródeł danych i pulpitów.", - "link-title": "Dowiedz się więcej o migracji innych ustawień", - "title": "Czy mogę przenieść tę instalację do chmury Grafana?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Będziesz też potrzebować stosu w chmurze. Jeśli jesteś tuż po rejestracji, automatycznie utworzymy Twój pierwszy stos. Jeśli masz już konto, musisz wybrać stos lub go utworzyć.", @@ -5371,7 +5444,7 @@ "body-token": "Aby bezpiecznie migrować treści, musisz mieć dostęp specjalny do samodzielnie zarządzanej instalacji usługi Grafana. Musisz utworzyć token migracji na wybranym stosie w chmurze.", "body-token-field": "Token migracji", "body-token-field-placeholder": "Wklej token tutaj", - "body-token-instructions": "Zaloguj się do stosu w chmurze i przejdź do ekranu Administracja, Ogólne, Migracja do chmury Grafana. Utwórz tam token migracji i wklej go tutaj.", + "body-token-instructions": "", "body-view-stacks": "Wyświetl moje stosy w chmurze", "cancel": "Anuluj", "connect": "Połącz z tym stosem", @@ -5415,11 +5488,12 @@ "body": "Proces migracji należy rozpocząć z samodzielnie zarządzanej instancji Grafana.", "configure-pdc-link": "Skonfiguruj PDC dla tego stosu", "link-title": "Dowiedz się więcej o Private Data Source Connect", - "step-1": "Zaloguj się do samodzielnie zarządzanej instancji i przejdź do ekranu Administracja, Ogólne, Migracja do chmury Grafana.", + "step-1": "", "step-2": "Wybierz „Migruj tę instancję do chmury”.", - "step-3": "Poprosimy Cię o podanie o tokenu migracji. Wygeneruj go na tym ekranie.", - "step-4": "W samodzielnie zarządzanej instancji wybierz „Prześlij wszystko”, aby przesłać do tego stosu w chmurze źródła danych i pulpity.", - "step-5": "Jeśli nie wszystkie źródła danych będą działać w publicznym internecie, w samodzielnie zarządzanym środowisku musisz zainstalować Private Data Source Connect.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Wykonywanie migracji" }, "is-it-secure": { @@ -5428,7 +5502,7 @@ "title": "Czy to jest bezpieczne?" }, "migrate-to-this-stack": { - "body": "Do tego stosu w chmurze możesz migrować niektóre zasoby z samodzielnie zarządzanej instalacji Grafana. Aby to zrobić bezpiecznie, musisz wygenerować token migracji. Twoja samodzielnie zarządzana instancja wykorzysta token do uwierzytelnienia w stosie w chmurze.", + "body": "", "link-title": "Zobacz pełny przewodnik po migracji", "title": "Pomożemy Ci przeprowadzić migrację do tego stosu" }, @@ -5446,7 +5520,7 @@ "modal-close": "Zamknij", "modal-copy-and-close": "Kopiuj do schowka i zamknij", "modal-copy-button": "Kopiuj do schowka", - "modal-field-description": "Skopiuj token teraz, ponieważ później nie będzie go można ponownie wyświetlić. Utrata tokena wymaga utworzenia nowego.", + "modal-field-description": "", "modal-field-label": "Token", "modal-title": "Utworzono token migracji", "status": "Bieżący status: <2>" @@ -5463,7 +5537,7 @@ "get-session-error-title": "Błąd podczas ładowania konfiguracji migracji", "get-snapshot-error-title": "Błąd podczas ładowania migawki", "migration-finished-with-caveat-title": "Migracja zasobów zakończona", - "migration-finished-with-errors-body": "Migracja została zakończona, ale niektórych elementów nie można było przenieść do stosu w chmurze. Więcej informacji uzyskasz na liście wadliwych zasobów", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "Migracja została zakończona z pewnymi ostrzeżeniami. Więcej informacji uzyskasz na liście poszczególnych zasobów", "snapshot-error-status-body": "Wystąpił błąd podczas tworzenia migawki lub rozpoczynania procesu migracji. Więcej informacji uzyskasz w logach serwera Grafana", "snapshot-error-status-title": "Błąd migracji zasobów", @@ -5475,7 +5549,7 @@ "pdc": { "body": "Ujawnienie źródeł danych w internecie może budzić obawy dotyczące bezpieczeństwa. Połączenie z prywatnym źródłem danych (PDC) umożliwia chmurze Grafana dostęp do istniejących źródeł danych za pośrednictwem bezpiecznego tunelu sieciowego.", "link-title": "Dowiedz się więcej o PDC", - "title": "Nie wszystkie moje źródła danych znajdują się w publicznym internecie" + "title": "" }, "pricing": { "body": "Chmura Grafana ma obszerny plan bezpłatny i 14-dniowy nieograniczony okres próbny. Po wygaśnięciu okresu próbnego zapłacisz za wykorzystanie usług przekraczające limity planu bezpłatnego.", @@ -5553,7 +5627,7 @@ "disconnect": "Rozłącz", "errored-resource-count": "Błędy", "page-loading": "Ładowanie…", - "rebuild-snapshot": "Utwórz ponownie migawkę", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Sygnatura czasowa migawki", "snapshot-not-created": "Jeszcze nie utworzono", @@ -5810,7 +5884,7 @@ "subtitle": "Zarządzaj pulpitami folderów i uprawnieniami" }, "migrate-to-cloud": { - "subtitle": "Skopiuj konfigurację z samodzielnie zarządzanej instalacji do stosu w chmurze", + "subtitle": "", "title": "Migruj do chmury Grafana" }, "monitoring": { @@ -6174,7 +6248,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Brak dostępnych aktualizacji", @@ -6358,6 +6433,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6372,21 +6448,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_few": "", "files-count_many": "", @@ -6400,13 +6469,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6417,25 +6484,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6453,12 +6516,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6469,14 +6533,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6514,7 +6574,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6532,19 +6591,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6602,16 +6661,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6641,8 +6701,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6721,30 +6780,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6951,6 +7016,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7234,6 +7300,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7242,8 +7309,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7253,7 +7322,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7262,7 +7333,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7288,6 +7360,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7570,16 +7643,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Pokazanie zbyt wielu kolumn w jednej tabeli może wpłynąć na wydajność i utrudnić odczyt danych. Rozważ doprecyzowanie zapytań.", @@ -7602,6 +7680,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7613,24 +7693,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7729,6 +7821,7 @@ "date-created": "Data utworzenia: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7750,11 +7843,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7766,6 +7861,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7786,7 +7882,8 @@ "now": "Teraz:", "previously": "Poprzednio:", "see-examples": "> Zobacz przykłady", - "sentence": "Histogramy natywne Prometheus oferują wysoką rozdzielczość, wysoką precyzję, proste użycie w rozwiązaniach technicznych oraz sposób łączenia z histogramami i modyfikowania ich w zapytaniach i w usłudze Grafana." + "sentence": "Histogramy natywne Prometheus oferują wysoką rozdzielczość, wysoką precyzję, proste użycie w rozwiązaniach technicznych oraz sposób łączenia z histogramami i modyfikowania ich w zapytaniach i w usłudze Grafana.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Lub wyświetl ostatnie wyszukiwanie" @@ -7794,11 +7891,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Zawsze trzymaj w widoku wykres wybranej metryki", "show-previews-of-metric-graphs": "Pokaż podglądy wykresów metryk" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7836,11 +7937,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7850,6 +7957,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7862,8 +7970,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7890,6 +8010,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7906,8 +8027,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7916,7 +8042,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7929,6 +8057,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7939,6 +8069,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7949,21 +8082,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7993,7 +8137,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -8013,11 +8158,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8085,6 +8234,7 @@ "message": "Nie znaleziono użytkowników" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8099,6 +8249,11 @@ "shared-dashboard-users-tab-title": "Użytkownicy pulpitu udostępnionego" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Wybierz wartość" @@ -8109,7 +8264,7 @@ "picker": { "link-all": "Wszystkie", "option-all": "Wszystkie", - "option-selected-values": "Wybrano", + "option-selected-values": "", "option-tooltip": "Wyczyść zaznaczenia" }, "textbox": { @@ -8135,6 +8290,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8187,9 +8345,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/pt-BR/grafana.json b/public/locales/pt-BR/grafana.json index a4c725a3039..6ace2c92726 100644 --- a/public/locales/pt-BR/grafana.json +++ b/public/locales/pt-BR/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -803,7 +806,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -824,7 +828,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -896,7 +901,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -927,6 +933,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -958,6 +965,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -990,6 +1000,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1048,12 +1059,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1315,6 +1329,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1529,7 +1547,8 @@ "label-target-data-source": "Fonte de dados de destino" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1805,7 +1824,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1874,7 +1895,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1919,7 +1941,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1944,6 +1968,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1988,7 +2013,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "ATÉ" @@ -2009,6 +2035,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2622,6 +2651,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2697,7 +2727,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2786,7 +2817,8 @@ "rows": "Linhas", "tab": "Aba", "tabs": "Abas", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Abrir painel de opções", "row": { @@ -2794,6 +2826,10 @@ "hide": "Ocultar", "title": "Cabeçalho de linha" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2912,13 +2948,13 @@ "title-option": "" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(vazio)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3030,7 +3066,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3502,7 +3544,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Caminho do arquivo:", - "managed-by-version-control": "Este {type} é gerenciado pelo controle de versão e não pode ser excluído. Para removê-lo, exclua-o do repositório e sincronize para aplicar as alterações.", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "Não é possível excluir o recurso provisionado" }, @@ -3898,6 +3940,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3923,7 +3966,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4037,6 +4081,10 @@ "description": "Você está procurando os aplicativos Grafana Explore? Agora eles são chamados de aplicativos Grafana Drilldown e podem ser encontrados em <1>Menu > Drilldown", "title": "Explore Metrics, Logs, Traces e Profiles foram movidos!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4085,6 +4133,9 @@ "scan-for-older-logs": "Pesquisar logs mais antigos", "stop-scan": "Interromper pesquisa" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4111,6 +4162,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4142,6 +4194,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Fechar histórico de consultas", "datasource-a-z": "Fonte de dados A-Z", @@ -4269,6 +4324,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4348,8 +4404,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4364,6 +4424,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5208,6 +5269,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Copiar link para a linha do log", "copy-log": "Copiar linha do log", @@ -5260,11 +5325,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5331,14 +5398,20 @@ "when-complete": "Assim que a captura estiver concluída, você poderá enviá-la para a sua pilha da nuvem." }, "building-snapshot": { - "description": "Estamos criando uma captura da situação atual desta instalação. Assim que a captura estiver concluída, você poderá enviá-la para a Grafana Cloud.", + "description": "", "description-eta": "A criação de uma captura normalmente leva menos de dois minutos.", - "title": "Criando captura da instalação" + "title": "" }, "can-i-move": { - "body": "Depois de conectar esta instalação a uma pilha na nuvem, você será capaz de enviar fontes de dados e painéis de controle.", - "link-title": "Aprenda sobre como migrar outras configurações", - "title": "Posso mover esta instalação para o Grafana Cloud?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Você também precisará de uma pilha na nuvem. Se você se inscreveu, criaremos automaticamente sua primeira pilha. Se você tem uma conta, precisará selecionar ou criar uma pilha.", @@ -5347,7 +5420,7 @@ "body-token": "Sua instalação autogerenciada do Grafana precisa de acesso especial para migrar conteúdo com segurança. Você precisará criar um token de migração para a pilha na nuvem escolhida.", "body-token-field": "Token de migração", "body-token-field-placeholder": "Cole o token aqui", - "body-token-instructions": "Entre na sua pilha de nuvem e navegue para Administração, Geral, Migrar para o Grafana Cloud. Crie um token de migração nesta tela e cole o token aqui.", + "body-token-instructions": "", "body-view-stacks": "Visualizar minhas pilhas na nuvem", "cancel": "Cancelar", "connect": "Conectar-se a esta pilha", @@ -5391,11 +5464,12 @@ "body": "O processo de migração deve ser iniciado a partir da sua instância autogerenciada do Grafana.", "configure-pdc-link": "Configurar PDC para esta pilha", "link-title": "Saiba mais sobre conexão privada de fonte de dados", - "step-1": "Faça login na sua instância autogerenciada e navegue para Administração, Geral, Migrar para o Grafana Cloud.", + "step-1": "", "step-2": "Selecione \"Migrar esta instância para a nuvem\".", - "step-3": "Você receberá uma solicitação por um token de migração. Gere um nessa tela.", - "step-4": "Na sua instância autogerenciada, selecione \"Enviar tudo\" para enviar fontes de dados e painéis de controle para esta pilha na nuvem.", - "step-5": "Se algumas das suas fontes de dados não funcionarem em Internet pública, você precisará instalar o Private Source Connect no seu ambiente autogerenciado.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Realizando uma migração" }, "is-it-secure": { @@ -5404,7 +5478,7 @@ "title": "É seguro?" }, "migrate-to-this-stack": { - "body": "Você pode migrar alguns recursos da sua instalação autogerenciada da Grafana para esta pilha na nuvem. Para fazer isso com segurança, você precisará gerar um token de migração. Sua instância autogerenciada usará o token para autenticação nesta pilha na nuvem.", + "body": "", "link-title": "Visualizar o guia completo de migração", "title": "Vamos auxiliar na sua migração para esta pilha" }, @@ -5422,7 +5496,7 @@ "modal-close": "Fechar", "modal-copy-and-close": "Copiar para a área de transferência e fechar", "modal-copy-button": "Copiar para a área de transferência", - "modal-field-description": "Copie o token agora, já que você não será capaz de vê-lo novamente. Perder um token requer criar um novo.", + "modal-field-description": "", "modal-field-label": "Token", "modal-title": "Token de migração criado", "status": "" @@ -5439,7 +5513,7 @@ "get-session-error-title": "Erro ao carregar a configuração de migração", "get-snapshot-error-title": "Erro ao carregar a captura", "migration-finished-with-caveat-title": "Migração de recursos concluída", - "migration-finished-with-errors-body": "A migração foi concluída, mas não foi possível migrar alguns itens para a pilha na nuvem. Confira os recursos que apresentaram falha para saber mais", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "A migração foi concluída com alguns avisos. Confira os recursos individuais para saber mais", "snapshot-error-status-body": "Ocorreu um erro ao criar a captura ou iniciar o processo de migração. Consulte os logs do servidor da Grafana para saber mais", "snapshot-error-status-title": "Erro ao migrar recursos", @@ -5451,7 +5525,7 @@ "pdc": { "body": "Expor suas fontes de dados à Internet pode suscitar questões de segurança. A fonte de dados privada de conexão (PDC) permite ao Grafana Cloud acessar seus recursos de dados existentes por meio de um túnel de rede seguro.", "link-title": "Saiba mais sobre o PDC", - "title": "Nem todas as minhas fontes de dados estão na Internet pública" + "title": "" }, "pricing": { "body": "O Grafana Cloud tem um plano grátis generoso e um teste de uso ilimitado de 14 dias. Depois que o seu período de teste expirar, você receberá uma cobrança com base no uso acima dos limites do plano grátis.", @@ -5529,7 +5603,7 @@ "disconnect": "", "errored-resource-count": "Erros", "page-loading": "", - "rebuild-snapshot": "Reconstruir captura", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Registro de data/hora da captura", "snapshot-not-created": "Ainda não criado", @@ -5786,7 +5860,7 @@ "subtitle": "Gerencie painéis de controle de pastas e permissões" }, "migrate-to-cloud": { - "subtitle": "Copie a configuração da sua instalação autogerenciada para uma pilha na nuvem", + "subtitle": "", "title": "Migrar para o Grafana Cloud" }, "monitoring": { @@ -6150,7 +6224,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Nenhuma atualização disponível", @@ -6334,6 +6409,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6348,21 +6424,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_other": "" }, @@ -6374,13 +6443,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6391,25 +6458,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6427,12 +6490,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6443,14 +6507,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6488,7 +6548,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6506,19 +6565,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6576,16 +6635,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6615,8 +6675,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6695,30 +6754,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6923,6 +6988,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7200,6 +7266,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7208,8 +7275,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7219,7 +7288,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7228,7 +7299,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7254,6 +7326,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7536,16 +7609,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Mostrar muitas colunas em uma única tabela pode afetar o desempenho e dificultar a leitura dos dados. Considere refinar suas consultas.", @@ -7568,6 +7646,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7579,24 +7659,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7695,6 +7787,7 @@ "date-created": "Data de criação: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7716,11 +7809,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7732,6 +7827,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7752,7 +7848,8 @@ "now": "Agora:", "previously": "Antes:", "see-examples": "> Veja exemplos", - "sentence": "Os histogramas nativos do Prometheus oferecem alta resolução, alta precisão, acessibilidade na instrumentação e uma forma de combinar e manipular histogramas em consultas e na Grafana." + "sentence": "Os histogramas nativos do Prometheus oferecem alta resolução, alta precisão, acessibilidade na instrumentação e uma forma de combinar e manipular histogramas em consultas e na Grafana.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Ou confira uma análise recente" @@ -7760,11 +7857,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Sempre manter o gráfico de métrica selecionado em exibição", "show-previews-of-metric-graphs": "Mostrar visualizações de gráficos de métricas" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7802,11 +7903,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7816,6 +7923,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7828,8 +7936,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7856,6 +7976,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7872,8 +7993,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7882,7 +8008,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7895,6 +8023,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7905,6 +8035,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7915,21 +8048,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7959,7 +8103,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7979,11 +8124,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8051,6 +8200,7 @@ "message": "Nenhum usuário encontrado" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8065,6 +8215,11 @@ "shared-dashboard-users-tab-title": "Usuários do painel de controle compartilhado" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Selecionar valor" @@ -8075,7 +8230,7 @@ "picker": { "link-all": "Tudo", "option-all": "Tudo", - "option-selected-values": "Selecionado", + "option-selected-values": "", "option-tooltip": "Limpar seleções" }, "textbox": { @@ -8101,6 +8256,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8153,9 +8311,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/pt-PT/grafana.json b/public/locales/pt-PT/grafana.json index abbd29f435d..7b60f8923ee 100644 --- a/public/locales/pt-PT/grafana.json +++ b/public/locales/pt-PT/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -803,7 +806,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -824,7 +828,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -896,7 +901,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -927,6 +933,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -958,6 +965,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -990,6 +1000,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1048,12 +1059,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1315,6 +1329,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1529,7 +1547,8 @@ "label-target-data-source": "Origem de dados de destino" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1805,7 +1824,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1874,7 +1895,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1919,7 +1941,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1944,6 +1968,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1988,7 +2013,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "PARA" @@ -2009,6 +2035,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2622,6 +2651,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2697,7 +2727,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2786,7 +2817,8 @@ "rows": "Linhas", "tab": "Separador", "tabs": "Separadores", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Abrir painel de opções", "row": { @@ -2794,6 +2826,10 @@ "hide": "Ocultar", "title": "Cabeçalho da linha" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2912,13 +2948,13 @@ "title-option": "Título" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(vazio)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3030,7 +3066,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3502,7 +3544,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Caminho do ficheiro:", - "managed-by-version-control": "Este {type} é gerido pelo controlo de versão e não pode ser eliminado. Para removê-lo, elimine-o do repositório e sincronize para aplicar as alterações.", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "Não é possível eliminar o recurso disponibilizado" }, @@ -3898,6 +3940,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3923,7 +3966,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4037,6 +4081,10 @@ "description": "Procura as aplicações de Grafana Explore? Agora são chamadas aplicações Grafana Drilldown e podem ser encontradas em <1>Menu > Drilldown", "title": "Os produtos Explore Metrics, Explore Logs, Explore Traces and Explore Profiles foram movidos!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4085,6 +4133,9 @@ "scan-for-older-logs": "Procurar registos mais antigos", "stop-scan": "Parar a pesquisa" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4111,6 +4162,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4142,6 +4194,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Fechar histórico de consultas", "datasource-a-z": "Origem dos dados A-Z", @@ -4269,6 +4324,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4348,8 +4404,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4364,6 +4424,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5208,6 +5269,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Copiar link para a linha de registo", "copy-log": "Copiar linha de registo", @@ -5260,11 +5325,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5331,14 +5398,20 @@ "when-complete": "Assim que o instantâneo estiver concluído, poderá carregá-lo para a sua pilha de nuvem." }, "building-snapshot": { - "description": "Estamos a criar um instantâneo pontual do estado atual desta instalação. Assim que o instantâneo estiver concluído, poderá carregá-lo para a nuvem da Grafana.", + "description": "", "description-eta": "A criação de um instantâneo geralmente demora menos de dois minutos.", - "title": "A criar instantâneo da instalação" + "title": "" }, "can-i-move": { - "body": "Após ligar esta instalação a uma pilha de nuvem, poderá carregar origens de dados e painéis de controlo.", - "link-title": "Saiba mais sobre a migração de outras definições", - "title": "Posso mover esta instalação para a Grafana Cloud?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Também precisará de uma pilha de nuvem. Se acabou de se inscrever, criaremos automaticamente a sua primeira pilha. Se tiver uma conta, terá de selecionar ou criar uma pilha.", @@ -5347,7 +5420,7 @@ "body-token": "A sua instalação de Grafana gerida automaticamente precisa de acesso especial para migrar conteúdo com segurança. Terá de criar um token de migração na pilha de nuvem escolhida.", "body-token-field": "Token de migração", "body-token-field-placeholder": "Colar o token aqui", - "body-token-instructions": "Inicie sessão na sua pilha de nuvem e navegue até Administração, Geral, Migrar para a Grafana Cloud. Crie um token de migração nesse ecrã e cole aí o token.", + "body-token-instructions": "", "body-view-stacks": "Ver as minhas pilhas de nuvem", "cancel": "Cancelar", "connect": "Ligar a esta pilha", @@ -5391,11 +5464,12 @@ "body": "O processo de migração deve ser iniciado a partir da sua instância Grafana gerida automaticamente.", "configure-pdc-link": "Configurar PDC para esta pilha", "link-title": "Saiba mais sobre a Private Data Source Connect", - "step-1": "Inicie sessão na sua instância gerida automaticamente e navegue até Administração, Geral, Migrar para a Grafana Cloud.", + "step-1": "", "step-2": "Selecione \"Migrar esta instância para a Cloud\".", - "step-3": "Ser-lhe-á solicitado um token de migração. Gere um a partir deste ecrã.", - "step-4": "Na sua instância gerida automaticamente, selecione \"Carregar tudo\" para carregar origens de dados e painéis de controlo para esta pilha de nuvem.", - "step-5": "Se algumas das suas origens de dados não funcionarem na Internet pública, terá de instalar a Private Data Source Connect no seu ambiente autogerido.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "A realizar uma migração" }, "is-it-secure": { @@ -5404,7 +5478,7 @@ "title": "É seguro?" }, "migrate-to-this-stack": { - "body": "Pode migrar alguns recursos da sua instalação da Grafana gerida automaticamente para esta pilha de nuvem. Para o fazer com segurança, terá de gerar um token de migração. A sua instância gerida automaticamente utilizará o token para autenticar com esta pilha de nuvem.", + "body": "", "link-title": "Ver o guia de migração completo", "title": "Deixe-nos ajudá-lo a migrar para esta pilha" }, @@ -5422,7 +5496,7 @@ "modal-close": "Fechar", "modal-copy-and-close": "Copiar para a área de transferência e fechar", "modal-copy-button": "Copiar para a área de transferência", - "modal-field-description": "Copie o token agora, pois não poderá vê-lo novamente. Perder um token requer a criação de um novo.", + "modal-field-description": "", "modal-field-label": "Token", "modal-title": "Token de migração criado", "status": "Estado atual: <2>" @@ -5439,7 +5513,7 @@ "get-session-error-title": "Erro ao carregar a configuração de migração", "get-snapshot-error-title": "Erro ao carregar instantâneo", "migration-finished-with-caveat-title": "Migração de recursos concluída", - "migration-finished-with-errors-body": "A migração foi concluída, mas alguns elementos não puderam ser migrados para a pilha de nuvem. Verifique os recursos com falha para obter mais detalhes", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "A migração foi concluída com alguns avisos. Verifique os recursos individuais para obter mais detalhes", "snapshot-error-status-body": "Ocorreu um erro ao criar o instantâneo ou ao iniciar o processo de migração. Consulte os registos do servidor Grafana para obter mais detalhes", "snapshot-error-status-title": "Erro ao migrar recursos", @@ -5451,7 +5525,7 @@ "pdc": { "body": "Expor as origens dos seus dados à Internet pode suscitar preocupações de segurança. A Private Data Source Connect (PDC) permite que a Grafana Cloud aceda às suas origens de dados existentes através de um túnel de rede seguro.", "link-title": "Saiba mais sobre PDC", - "title": "Nem todas as origens dos meus dados estão na Internet pública" + "title": "" }, "pricing": { "body": "A Grafana Cloud tem um plano gratuito generoso e um teste de utilização ilimitada de 14 dias. Após a expiração do período de avaliação será faturado com base na utilização acima dos limites do plano gratuito.", @@ -5529,7 +5603,7 @@ "disconnect": "Desligar", "errored-resource-count": "Erros", "page-loading": "A carregar...", - "rebuild-snapshot": "Reconstruir instantâneo", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Registo de data e hora do instantâneo", "snapshot-not-created": "Ainda não criado", @@ -5786,7 +5860,7 @@ "subtitle": "Gerir painéis de controlo e permissões de pastas" }, "migrate-to-cloud": { - "subtitle": "Copiar a configuração da sua instalação gerida automaticamente para uma pilha de nuvem", + "subtitle": "", "title": "Migrar para a Grafana Cloud" }, "monitoring": { @@ -6150,7 +6224,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Nenhuma atualização disponível", @@ -6334,6 +6409,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6348,21 +6424,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_other": "" }, @@ -6374,13 +6443,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6391,25 +6458,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6427,12 +6490,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6443,14 +6507,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6488,7 +6548,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6506,19 +6565,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6576,16 +6635,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6615,8 +6675,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6695,30 +6754,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6923,6 +6988,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7200,6 +7266,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7208,8 +7275,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7219,7 +7288,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7228,7 +7299,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7254,6 +7326,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7536,16 +7609,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Mostrar demasiadas colunas numa única tabela pode afetar o desempenho e dificultar a leitura dos dados. Considere refinar as suas consultas.", @@ -7568,6 +7646,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7579,24 +7659,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7695,6 +7787,7 @@ "date-created": "Data de criação: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7716,11 +7809,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7732,6 +7827,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7752,7 +7848,8 @@ "now": "Agora:", "previously": "Anteriormente:", "see-examples": "> Ver exemplos", - "sentence": "Os histogramas nativos do Prometheus oferecem alta resolução, alta precisão, utilização simples na instrumentação e uma forma de combinar e manipular histogramas em consultas e na Grafana." + "sentence": "Os histogramas nativos do Prometheus oferecem alta resolução, alta precisão, utilização simples na instrumentação e uma forma de combinar e manipular histogramas em consultas e na Grafana.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Ou ver uma exploração recente" @@ -7760,11 +7857,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Manter sempre o gráfico de métrica selecionado na vista", "show-previews-of-metric-graphs": "Mostrar pré-visualizações de gráficos métricos" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7802,11 +7903,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7816,6 +7923,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7828,8 +7936,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7856,6 +7976,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7872,8 +7993,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7882,7 +8008,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7895,6 +8023,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7905,6 +8035,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7915,21 +8048,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7959,7 +8103,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7979,11 +8124,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8051,6 +8200,7 @@ "message": "Nenhum utilizador encontrado" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8065,6 +8215,11 @@ "shared-dashboard-users-tab-title": "Utilizadores do painel de controlo partilhado" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Selecionar valor" @@ -8075,7 +8230,7 @@ "picker": { "link-all": "Todos", "option-all": "Todos", - "option-selected-values": "Selecionado", + "option-selected-values": "", "option-tooltip": "Limpar seleções" }, "textbox": { @@ -8101,6 +8256,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8153,9 +8311,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/ru-RU/grafana.json b/public/locales/ru-RU/grafana.json index 0139d760fe5..896c2ed4701 100644 --- a/public/locales/ru-RU/grafana.json +++ b/public/locales/ru-RU/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -807,7 +810,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -828,7 +832,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -900,7 +905,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -931,6 +937,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -962,6 +969,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -994,6 +1004,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1052,12 +1063,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1321,6 +1335,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1539,7 +1557,8 @@ "label-target-data-source": "Целевой источник данных" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1817,7 +1836,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1886,7 +1907,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1931,7 +1953,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1956,6 +1980,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -2000,7 +2025,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "К" @@ -2021,6 +2047,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2644,6 +2673,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2719,7 +2749,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2808,7 +2839,8 @@ "rows": "Строки", "tab": "Вкладка", "tabs": "Вкладки", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Открыть область параметров", "row": { @@ -2816,6 +2848,10 @@ "hide": "Скрыть", "title": "Заголовок строки" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2934,13 +2970,13 @@ "title-option": "Название" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(пусто)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3052,7 +3088,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3524,7 +3566,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Путь к файлу:", - "managed-by-version-control": "Этот {type} управляется системой контроля версий и не может быть удален. Чтобы удалить его, сначала удалите его из репозитория и выполните синхронизацию, чтобы применить изменения.", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "Невозможно удалить подготовленный ресурс" }, @@ -3920,6 +3962,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3945,7 +3988,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4059,6 +4103,10 @@ "description": "Ищете приложения Grafana Explore? Теперь они называются приложениями Grafana Drilldown и находятся в разделе <1>Меню > Drilldown", "title": "Explore Metrics, Logs, Traces и Profiles переехали!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4107,6 +4155,9 @@ "scan-for-older-logs": "Поиск более старых журналов", "stop-scan": "Остановить поиск" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4133,6 +4184,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4164,6 +4216,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Закрыть историю запросов", "datasource-a-z": "Источник данных A-Z", @@ -4291,6 +4346,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4370,8 +4426,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4386,6 +4446,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5232,6 +5293,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Копировать ссылку в строку журнала", "copy-log": "Копировать строку журнала", @@ -5284,11 +5349,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5355,14 +5422,20 @@ "when-complete": "После создания снимка вы сможете загрузить его в облачный стек." }, "building-snapshot": { - "description": "Мы создаем моментальный снимок текущего состояния этого экземпляра. После создания снимка вы сможете загрузить его в Grafana Cloud.", + "description": "", "description-eta": "Создание снимка обычно занимает менее двух минут.", - "title": "Создание снимка экземпляра" + "title": "" }, "can-i-move": { - "body": "После подключения к облачному стеку вы сможете загружать источники данных и дашборды.", - "link-title": "Подробнее о переносе других параметров", - "title": "Можно ли перенести этот экземпляр в Grafana Cloud?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Вам также понадобится облачный стек. Если вы только что зарегистрировались, мы автоматически создадим ваш первый стек. Если у вас есть учетная запись, вам нужно выбрать или создать стек.", @@ -5371,7 +5444,7 @@ "body-token": "Для безопасного переноса контента вашему автономному экземпляру Grafana требуется специальный доступ. Вам нужно создать токен переноса в выбранном облачном стеке.", "body-token-field": "Токен переноса", "body-token-field-placeholder": "Вставьте токен здесь", - "body-token-instructions": "Войдите в облачный стек и перейдите в раздел «Администрирование > Общие > Перенос в Grafana Cloud». Создайте токен переноса на этом экране и вставьте его здесь.", + "body-token-instructions": "", "body-view-stacks": "Просмотр облачных стеков", "cancel": "Отмена", "connect": "Подключиться к стеку", @@ -5415,11 +5488,12 @@ "body": "Процесс переноса необходимо запустить с вашего автономного экземпляра Grafana.", "configure-pdc-link": "Настроить PDC для стека", "link-title": "Подробнее о функции подключения частных источников данных (PDC)", - "step-1": "Войдите в автономный экземпляр и перейдите в раздел «Администрирование > Общие > Перенос в Grafana Cloud».", + "step-1": "", "step-2": "Нажмите «Перенести экземпляр в Cloud».", - "step-3": "Вам предложат ввести токен переноса. Сгенерируйте его на этом экране.", - "step-4": "В своем автономном экземпляре выберите «Загрузить всё», чтобы загрузить источники данных и дашборды в этот облачный стек.", - "step-5": "Если какие-либо из ваших источников данных не будут работать через интернет общего пользования, вам нужно будет установить функцию подключения частных источников данных в своей автономной среде.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Выполнение переноса" }, "is-it-secure": { @@ -5428,7 +5502,7 @@ "title": "Безопасен ли перенос?" }, "migrate-to-this-stack": { - "body": "Вы можете перенести некоторые ресурсы из вашего автономного экземпляра Grafana в облачный стек. Для безопасного выполнения этого действия необходимо создать токен переноса. Ваш автономный экземпляр использует токен для аутентификации в облачном стеке.", + "body": "", "link-title": "Ознакомьтесь с полным руководством по переносу", "title": "Помощь в выполнении переноса в стек" }, @@ -5446,7 +5520,7 @@ "modal-close": "Закрыть", "modal-copy-and-close": "Копировать в буфер обмена и закрыть", "modal-copy-button": "Копировать в буфер обмена", - "modal-field-description": "Скопируйте токен сейчас, так как вы не сможете увидеть его снова. При потере токена требуется создать новый.", + "modal-field-description": "", "modal-field-label": "Токен", "modal-title": "Токен переноса создан", "status": "Текущий статус: <2>" @@ -5463,7 +5537,7 @@ "get-session-error-title": "Ошибка при загрузке конфигурации переноса", "get-snapshot-error-title": "Ошибка при загрузке снимка", "migration-finished-with-caveat-title": "Перенос ресурсов завершен", - "migration-finished-with-errors-body": "Перенос завершен, но некоторые элементы не были перенесены в облачный стек. Для получения более подробной информации проверьте ресурсы, которые не удалось перенести", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "Перенос завершен с предупреждениями. Для получения более подробной информации проверьте отдельные ресурсы", "snapshot-error-status-body": "Ошибка при создании снимка или запуске процесса переноса. Для получения более подробной информации см. журналы сервера Grafana", "snapshot-error-status-title": "Ошибка при переносе ресурсов", @@ -5475,7 +5549,7 @@ "pdc": { "body": "Предоставление доступа к источникам данных через интернет может вызвать проблемы безопасности. Private data source connect (PDC) позволяет Grafana Cloud получать доступ к существующим источникам данных через безопасный сетевой туннель.", "link-title": "Подробнее о функции PDC", - "title": "Не все мои источники данных находятся в интернете общего пользования" + "title": "" }, "pricing": { "body": "Grafana Cloud предлагает отличный бесплатный план и 14-дневную пробную версию с неограниченным использованием. После истечения пробного периода плата будет взиматься исходя из использования сверх лимитов бесплатного плана.", @@ -5553,7 +5627,7 @@ "disconnect": "Отключить", "errored-resource-count": "Ошибки", "page-loading": "Загрузка…", - "rebuild-snapshot": "Повторно создать снимок", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Метка времени снимка", "snapshot-not-created": "Еще не создан", @@ -5810,7 +5884,7 @@ "subtitle": "Управление дашбордами папок и разрешениями" }, "migrate-to-cloud": { - "subtitle": "Копирование конфигурации из автономного экземпляра в облачный стек", + "subtitle": "", "title": "Перенос в Grafana Cloud" }, "monitoring": { @@ -6174,7 +6248,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Обновления отсутствуют", @@ -6358,6 +6433,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6372,21 +6448,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_few": "", "files-count_many": "", @@ -6400,13 +6469,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6417,25 +6484,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6453,12 +6516,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6469,14 +6533,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6514,7 +6574,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6532,19 +6591,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6602,16 +6661,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6641,8 +6701,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6721,30 +6780,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6951,6 +7016,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7234,6 +7300,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7242,8 +7309,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7253,7 +7322,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7262,7 +7333,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7288,6 +7360,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7570,16 +7643,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Отображение слишком большого количества столбцов в одной таблице может повлиять на производительность и затруднить чтение данных. Попробуйте уточнить запросы.", @@ -7602,6 +7680,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7613,24 +7693,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7729,6 +7821,7 @@ "date-created": "Дата создания: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7750,11 +7843,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7766,6 +7861,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7786,7 +7882,8 @@ "now": "Сейчас:", "previously": "Ранее:", "see-examples": "> См. примеры", - "sentence": "Встроенные гистограммы Prometheus обеспечивают высокое разрешение и точность, простоту использования в инструментах, а также возможность комбинировать гистограммы и управлять ими в запросах и в Grafana." + "sentence": "Встроенные гистограммы Prometheus обеспечивают высокое разрешение и точность, простоту использования в инструментах, а также возможность комбинировать гистограммы и управлять ими в запросах и в Grafana.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Или посмотрите недавние данные" @@ -7794,11 +7891,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Всегда держать выбранный граф метрик в поле зрения", "show-previews-of-metric-graphs": "Показать предварительный просмотр графов метрик" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7836,11 +7937,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7850,6 +7957,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7862,8 +7970,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7890,6 +8010,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7906,8 +8027,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7916,7 +8042,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7929,6 +8057,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7939,6 +8069,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7949,21 +8082,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7993,7 +8137,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -8013,11 +8158,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8085,6 +8234,7 @@ "message": "Пользователи не найдены" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8099,6 +8249,11 @@ "shared-dashboard-users-tab-title": "Пользователи совместно используемого дашборда" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Выбрать значение" @@ -8109,7 +8264,7 @@ "picker": { "link-all": "Все", "option-all": "Все", - "option-selected-values": "Выбрано", + "option-selected-values": "", "option-tooltip": "Очистить выбор" }, "textbox": { @@ -8135,6 +8290,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8187,9 +8345,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/sv-SE/grafana.json b/public/locales/sv-SE/grafana.json index 3d5e5e14689..182c5e4e176 100644 --- a/public/locales/sv-SE/grafana.json +++ b/public/locales/sv-SE/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -803,7 +806,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -824,7 +828,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -896,7 +901,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -927,6 +933,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -958,6 +965,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -990,6 +1000,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1048,12 +1059,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1315,6 +1329,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1529,7 +1547,8 @@ "label-target-data-source": "Måldatakälla" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1805,7 +1824,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1874,7 +1895,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1919,7 +1941,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1944,6 +1968,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1988,7 +2013,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "TILL" @@ -2009,6 +2035,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2622,6 +2651,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2697,7 +2727,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2786,7 +2817,8 @@ "rows": "Rader", "tab": "Flik", "tabs": "Flikar", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Öppna alternativfönstret", "row": { @@ -2794,6 +2826,10 @@ "hide": "Dölj", "title": "Radrubrik" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2912,13 +2948,13 @@ "title-option": "Titel" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(tomt)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3030,7 +3066,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3502,7 +3544,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Filsökväg:", - "managed-by-version-control": "Den här {type} hanteras av versionskontroll och kan inte raderas. Om du vill ta bort den tar du bort den från databasen och synkroniserar för att tillämpa ändringarna.", + "managed-by-version-control": "", "ok": "OK", "title-cannot-delete-provisioned-resource": "Det går inte att ta bort den tillhandahållna resursen" }, @@ -3898,6 +3940,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3923,7 +3966,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4037,6 +4081,10 @@ "description": "Letar du efter Grafana Explore-apparna? De kallas nu Grafana Drilldown och finns under <1>Meny > Drilldown", "title": "Utforska statistik, loggar, spår och profiler har flyttats!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4085,6 +4133,9 @@ "scan-for-older-logs": "Skanna efter äldre loggar", "stop-scan": "Stoppa skanning" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4111,6 +4162,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4142,6 +4194,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Stäng frågehistoriken", "datasource-a-z": "Datakälla A–Ö", @@ -4269,6 +4324,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4348,8 +4404,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4364,6 +4424,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5208,6 +5269,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Kopiera länk till loggrad", "copy-log": "Kopiera loggrad", @@ -5260,11 +5325,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5331,14 +5398,20 @@ "when-complete": "När ögonblicksbilden är klar kan du ladda upp den till din molnstack." }, "building-snapshot": { - "description": "Vi skapar en ögonblicksbild av det aktuella tillståndet för den här installationen. När ögonblicksbilden är klar kan du ladda upp den till Grafana Cloud.", + "description": "", "description-eta": "Att skapa en ögonblicksbild tar vanligtvis mindre än två minuter. ", - "title": "Ögonblicksbild av byggd installation" + "title": "" }, "can-i-move": { - "body": "När du väl har anslutit den här installationen till en molnstack kan du ladda upp datakällor och instrumentpaneler.", - "link-title": "Läs mer om att migrera andra inställningar", - "title": "Kan jag flytta den här installationen till Grafana Cloud?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Du behöver även en molnstack. Om du precis har registrerat dig skapar vi automatiskt din första stack. Om du har ett konto måste du välja en stack eller skapa en.", @@ -5347,7 +5420,7 @@ "body-token": "Din självhanterade Grafana-installation behöver särskild åtkomst för att migrera innehåll säkert. Du måste skapa en migreringstoken på din valda molnstack.", "body-token-field": "Migreringstoken", "body-token-field-placeholder": "Klistra in token här", - "body-token-instructions": "Logga in på din molnstack och navigera till Administration, Allmänt, Migrera till Grafana Cloud. Skapa en migreringstoken på den skärmen och klistra in token här.", + "body-token-instructions": "", "body-view-stacks": "Visa mina molnstackar", "cancel": "Avbryt", "connect": "Anslut till den här stacken", @@ -5391,11 +5464,12 @@ "body": "Migreringsprocessen måste startas från din självhanterade Grafana-instans.", "configure-pdc-link": "Konfigurera PDC för den här stacken", "link-title": "Läs mer om Private Data Source Connect", - "step-1": "Logga in på din självhanterade instans och navigera till Administration, Allmänt, Migrera till Grafana Cloud.", + "step-1": "", "step-2": "Välj Migrera den här instansen till molnet.", - "step-3": "Du kommer att bli ombedd att ange en migreringstoken. Generera en sådan token från den här skärmen.", - "step-4": "I din självhanterade instans väljer du ”Ladda upp allt” för att ladda upp datakällor och instrumentpaneler till den här molnstacken.", - "step-5": "Om några av dina datakällor inte fungerar över det offentliga internet måste du installera Private Data Source Connect i din självhanterade miljö.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Utföra en migrering" }, "is-it-secure": { @@ -5404,7 +5478,7 @@ "title": "Är det säkert?" }, "migrate-to-this-stack": { - "body": "Du kan migrera vissa resurser från din självhanterade Grafana-installation till den här molnstacken. För att göra det på ett säkert sätt måste du generera en migreringstoken. Din självhanterade instans kommer att använda token för att autentisera med den här molnstacken.", + "body": "", "link-title": "Visa hela migreringsguiden", "title": "Vi ska hjälpa dig att migrera till den här stacken" }, @@ -5422,7 +5496,7 @@ "modal-close": "Stäng", "modal-copy-and-close": "Kopiera till urklippet och stäng", "modal-copy-button": "Kopiera till urklippet", - "modal-field-description": "Kopiera token nu eftersom du inte kommer att kunna visa den igen. Om du förlorar denna token behöver du skapa en ny.", + "modal-field-description": "", "modal-field-label": "Token", "modal-title": "Migreringstoken skapades", "status": "Aktuell status: <2>" @@ -5439,7 +5513,7 @@ "get-session-error-title": "Fel vid laddning av migreringskonfiguration", "get-snapshot-error-title": "Fel vid laddning av ögonblicksbild", "migration-finished-with-caveat-title": "Resursmigrering slutförd", - "migration-finished-with-errors-body": "Migreringen har slutförts, men vissa objekt kunde inte migreras till molnstacken. Kontrollera de misslyckade resurserna för mer information", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "Migreringen har slutförts med några varningar. Kontrollera de enskilda resurserna för mer information", "snapshot-error-status-body": "Det gick inte att skapa ögonblicksbilden eller starta migreringsprocessen. Se Grafanas serverloggar för mer information", "snapshot-error-status-title": "Fel vid migrering av resurser", @@ -5451,7 +5525,7 @@ "pdc": { "body": "Exponering av dina datakällor för internet kan ge upphov till säkerhetsrisker. Private Data Source Connect (PDC) gör det möjligt för Grafana Cloud att komma åt dina befintliga datakällor via en säker nätverkstunnel.", "link-title": "Läs mer om PDC", - "title": "Inte alla mina datakällor finns på det offentliga internet" + "title": "" }, "pricing": { "body": "Grafana Cloud har en generös kostnadsfri plan och en 14-dagars obegränsad provperiod. När din provperiod har löpt ut faktureras du baserat på den användning som överskrider gränserna för den kostnadsfria planen.", @@ -5529,7 +5603,7 @@ "disconnect": "Koppla från", "errored-resource-count": "Fel", "page-loading": "Laddar …", - "rebuild-snapshot": "Återskapa ögonblicksbild", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Tidsstämpel för ögonblicksbild", "snapshot-not-created": "Ännu inte skapat", @@ -5786,7 +5860,7 @@ "subtitle": "Hantera instrumentpaneler och behörigheter för mappar" }, "migrate-to-cloud": { - "subtitle": "Kopiera konfiguration från din självhanterade installation till en molnstack", + "subtitle": "", "title": "Migrera till Grafana Cloud" }, "monitoring": { @@ -6150,7 +6224,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Inga uppdateringar tillgängliga", @@ -6334,6 +6409,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6348,21 +6424,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_other": "" }, @@ -6374,13 +6443,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6391,25 +6458,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6427,12 +6490,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6443,14 +6507,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6488,7 +6548,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6506,19 +6565,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6576,16 +6635,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6615,8 +6675,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6695,30 +6754,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6923,6 +6988,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7200,6 +7266,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7208,8 +7275,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7219,7 +7288,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7228,7 +7299,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7254,6 +7326,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7536,16 +7609,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Om för många kolumner visas i en enda tabell kan prestandan påverkas och data kan bli svårare att läsa. Överväg att förfina dina frågor.", @@ -7568,6 +7646,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7579,24 +7659,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7695,6 +7787,7 @@ "date-created": "Datum skapat: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7716,11 +7809,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7732,6 +7827,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7752,7 +7848,8 @@ "now": "Nu:", "previously": "Tidigare:", "see-examples": "> Se exempel", - "sentence": "Prometheus inbyggda histogram erbjuder hög upplösning, hög precision, enkel användning i instrumentering och ett sätt att kombinera och manipulera histogram i frågor och i Grafana." + "sentence": "Prometheus inbyggda histogram erbjuder hög upplösning, hög precision, enkel användning i instrumentering och ett sätt att kombinera och manipulera histogram i frågor och i Grafana.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Eller visa en nyligen genomförd utforskning" @@ -7760,11 +7857,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Behåll alltid det valda diagrammet synligt", "show-previews-of-metric-graphs": "Visa förhandsgranskningar av statistiska grafer" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7802,11 +7903,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7816,6 +7923,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7828,8 +7936,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7856,6 +7976,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7872,8 +7993,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7882,7 +8008,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7895,6 +8023,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7905,6 +8035,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7915,21 +8048,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7959,7 +8103,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7979,11 +8124,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8051,6 +8200,7 @@ "message": "Inga användare hittades" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8065,6 +8215,11 @@ "shared-dashboard-users-tab-title": "Användare av delade instrumentpaneler" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Välj värde" @@ -8075,7 +8230,7 @@ "picker": { "link-all": "Alla", "option-all": "Alla", - "option-selected-values": "Vald", + "option-selected-values": "", "option-tooltip": "Rensa val" }, "textbox": { @@ -8101,6 +8256,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8153,9 +8311,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/tr-TR/grafana.json b/public/locales/tr-TR/grafana.json index 9fc9094ccd3..15ced4d0a16 100644 --- a/public/locales/tr-TR/grafana.json +++ b/public/locales/tr-TR/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -803,7 +806,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -824,7 +828,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -896,7 +901,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -927,6 +933,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -958,6 +965,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -990,6 +1000,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1048,12 +1059,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1315,6 +1329,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1529,7 +1547,8 @@ "label-target-data-source": "Hedef veri kaynağı" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1805,7 +1824,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1874,7 +1895,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1919,7 +1941,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1944,6 +1968,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1988,7 +2013,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "-" @@ -2009,6 +2035,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2622,6 +2651,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2697,7 +2727,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2786,7 +2817,8 @@ "rows": "Satırlar", "tab": "Sekme", "tabs": "Sekmeler", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "Seçenekler bölmesini aç", "row": { @@ -2794,6 +2826,10 @@ "hide": "Gizle", "title": "Satır başlığı" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2912,13 +2948,13 @@ "title-option": "Başlık" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(boş)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3030,7 +3066,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3502,7 +3544,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "Dosya yolu:", - "managed-by-version-control": "Bu {type}, sürüm kontrolü tarafından yönetilmektedir ve silinemez. Kaldırmak için depodan silin ve değişiklikleri uygulamak için eşitleyin.", + "managed-by-version-control": "", "ok": "Tamam", "title-cannot-delete-provisioned-resource": "Sağlanan kaynak silinemiyor" }, @@ -3898,6 +3940,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3923,7 +3966,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4037,6 +4081,10 @@ "description": "Grafana Explore uygulamalarını mı arıyorsunuz? Artık Grafana Drilldown uygulamaları olarak adlandırılıyor ve <1>Menü > Drilldown altında bulunabilir.", "title": "Explore Metrics, Logs, Traces ve Profiles taşındı!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4085,6 +4133,9 @@ "scan-for-older-logs": "Daha eski günlükleri tara", "stop-scan": "Taramayı durdur" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4111,6 +4162,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4142,6 +4194,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "Sorgu geçmişini kapat", "datasource-a-z": "Veri kaynağı (A-Z)", @@ -4269,6 +4324,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4348,8 +4404,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4364,6 +4424,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5208,6 +5269,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "Bağlantıyı günlük satırına kopyala", "copy-log": "Günlük satırını kopyala", @@ -5260,11 +5325,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5331,14 +5398,20 @@ "when-complete": "Anlık görüntü tamamlandığında bunu bulut altyapınıza yükleyebileceksiniz." }, "building-snapshot": { - "description": "Bu kurulumun mevcut durumunun belirli bir anlık görüntüsünü oluşturuyoruz. Anlık görüntü tamamlandıktan sonra bunu Grafana Cloud'a yükleyebileceksiniz.", + "description": "", "description-eta": "Anlık görüntü oluşturma işlemi genellikle iki dakikadan kısa sürer. ", - "title": "Kurulum anlık görüntüsü oluşturuluyor" + "title": "" }, "can-i-move": { - "body": "Bu kurulumu bir bulut yığınına bağladıktan sonra veri kaynaklarını ve panoları yükleyebileceksiniz.", - "link-title": "Diğer ayarların nasıl taşınacağını öğrenin", - "title": "Bu kurulumu Grafana Cloud'a taşıyabilir miyim?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "Ayrıca bir bulut yığınına ihtiyacınız olacak. Yeni kaydolduysanız ilk yığınınızı otomatik olarak oluşturacağız. Hesabınız varsa bir yığın seçmeniz veya oluşturmanız gerekecektir.", @@ -5347,7 +5420,7 @@ "body-token": "Kendi kendine yönetilen Grafana kurulumunuzun, içerikleri güvenli bir şekilde taşıyabilmesi için özel erişime ihtiyacı vardır. Seçtiğiniz bulut yığınında bir taşıma belirteci oluşturmanız gerekecektir.", "body-token-field": "Taşıma belirteci", "body-token-field-placeholder": "Belirteci buraya yapıştırın", - "body-token-instructions": "Bulut yığınınıza giriş yapın ve Yönetim, Genel, Grafana Cloud'a Taşıma bölümüne gidin. Bu ekranda bir taşıma belirteci oluşturun ve belirteci buraya yapıştırın.", + "body-token-instructions": "", "body-view-stacks": "Bulut yığınlarımı görüntüle", "cancel": "İptal", "connect": "Bu yığına bağlan", @@ -5391,11 +5464,12 @@ "body": "Taşıma işlemi, kendi kendine yönetilen Grafana örneğinizden başlatılmalıdır.", "configure-pdc-link": "Bu yığın için PDC'yi yapılandırın", "link-title": "Private Data Source Connect hakkında daha fazla bilgi edinin", - "step-1": "Kendi kendine yönetilen örneğinize giriş yapın ve Yönetim, Genel, Grafana Cloud'a Taşıma bölümüne gidin.", + "step-1": "", "step-2": "\"Bu örneği Cloud'a taşı\"yı seçin.", - "step-3": "Bir taşıma jetonu girmeniz istenecektir. Bu ekrandan bir tane oluşturun.", - "step-4": "Kendi kendine yönetilen örneğinizde, veri kaynaklarını ve panoları bu bulut yığınına yüklemek için \"Her şeyi yükle\"yi seçin.", - "step-5": "Herkese açık internet üzerinden çalışmayan veri kaynaklarınız varsa kendi kendine yönetilen ortamınıza Private Data Source Connect'i yüklemeniz gerekecektir.", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "Taşıma gerçekleştiriliyor" }, "is-it-secure": { @@ -5404,7 +5478,7 @@ "title": "Güvenli mi?" }, "migrate-to-this-stack": { - "body": "Kendi kendine yönetilen Grafana kurulumunuzdan bu bulut altyapısına bazı kaynakları taşıyabilirsiniz. Bunu güvenli bir şekilde yapmak için bir taşıma belirteci oluşturmanız gerekecek. Kendi kendine yönetilen örneğiniz, bu bulut altyapısına kimlik doğrulamak için belirteci kullanacaktır.", + "body": "", "link-title": "Taşıma kılavuzunun tamamını görüntüleyin", "title": "Bu altyapıya geçiş yapmanıza yardımcı olalım" }, @@ -5422,7 +5496,7 @@ "modal-close": "Kapat", "modal-copy-and-close": "Panoya kopyala ve kapat", "modal-copy-button": "Panoya kopyala", - "modal-field-description": "Bu noktada belirteci kopyalayın çünkü tekrar göremeyeceksiniz. Belirtecin kaybedilmesi yeni bir belirteç oluşturulmasını gerektirir.", + "modal-field-description": "", "modal-field-label": "Belirteç", "modal-title": "Taşıma belirteci oluşturuldu", "status": "Mevcut durum: <2>" @@ -5439,7 +5513,7 @@ "get-session-error-title": "Taşıma yapılandırması yüklenirken hata oluştu", "get-snapshot-error-title": "Anlık görüntü yüklenirken hata oluştu", "migration-finished-with-caveat-title": "Kaynak taşıma tamamlandı", - "migration-finished-with-errors-body": "Taşıma tamamlandı ancak bazı ögeler bulut yığınına taşınamadı. Daha fazla ayrıntı için başarısız olan kaynakları kontrol edin.", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "Taşıma işlemi bazı uyarılarla tamamlandı. Daha fazla ayrıntı için bireysel kaynakları kontrol edin.", "snapshot-error-status-body": "Anlık görüntü oluşturulurken veya taşıma işlemi başlatılırken bir hata oluştu. Daha fazla ayrıntı için Grafana sunucu günlüklerine bakın", "snapshot-error-status-title": "Kaynaklar taşınırken hata oluştu", @@ -5451,7 +5525,7 @@ "pdc": { "body": "Veri kaynaklarınızı internete açmak güvenlik endişelerine yol açabilir. Private Data Source Connect (PDC), Grafana Cloud'un mevcut veri kaynaklarınıza güvenli bir ağ tüneli üzerinden erişmesini sağlar.", "link-title": "PDC hakkında bilgi edinin", - "title": "Veri kaynaklarım tamamı herkese açık internette bulunmuyor" + "title": "" }, "pricing": { "body": "Grafana Cloud, cömert bir ücretsiz plan ve 14 günlük, tüm özelliklerin kullanılabildiği bir deneme sürümü sunar. Deneme süreniz sona erdikten sonra yalnızca ücretsiz plan sınırlarının üzerindeki kullanımınız için faturalandırılırsınız.", @@ -5529,7 +5603,7 @@ "disconnect": "Bağlantıyı kes", "errored-resource-count": "Hata", "page-loading": "Yükleniyor...", - "rebuild-snapshot": "Anlık görüntüyü yeniden oluştur", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "Anlık görüntü zaman damgası", "snapshot-not-created": "Henüz oluşturulmadı", @@ -5786,7 +5860,7 @@ "subtitle": "Klasör panolarını ve izinlerini yönetin" }, "migrate-to-cloud": { - "subtitle": "Yapılandırmayı kendi kendini yöneten kurulumunuzdan bir bulut yığınına kopyalayın", + "subtitle": "", "title": "Grafana Cloud'a taşıma" }, "monitoring": { @@ -6150,7 +6224,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "Güncelleme yok", @@ -6334,6 +6409,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6348,21 +6424,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_one": "", "files-count_other": "" }, @@ -6374,13 +6443,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6391,25 +6458,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6427,12 +6490,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6443,14 +6507,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6488,7 +6548,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6506,19 +6565,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6576,16 +6635,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6615,8 +6675,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6695,30 +6754,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6923,6 +6988,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7200,6 +7266,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7208,8 +7275,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7219,7 +7288,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7228,7 +7299,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7254,6 +7326,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7536,16 +7609,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "Tek bir tabloda çok fazla sütun göstermek performansı etkileyebilir ve verilerin okunmasını zorlaştırabilir. Sorgularınızı daraltmayı düşünün.", @@ -7568,6 +7646,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7579,24 +7659,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7695,6 +7787,7 @@ "date-created": "Oluşturulma tarihi: " }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7716,11 +7809,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7732,6 +7827,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7752,7 +7848,8 @@ "now": "Şimdi:", "previously": "Önceden:", "see-examples": "> Örneklere bakın", - "sentence": "Prometheus yerel histogramları yüksek çözünürlük, yüksek hassasiyet, kolay izleme kullanımı ve Grafana'da histogramları sorgularla birleştirme ve işleme olanağı sunar." + "sentence": "Prometheus yerel histogramları yüksek çözünürlük, yüksek hassasiyet, kolay izleme kullanımı ve Grafana'da histogramları sorgularla birleştirme ve işleme olanağı sunar.", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "Veya son keşiflere göz atın" @@ -7760,11 +7857,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "Seçili metrik grafiğini her zaman görünürde tut", "show-previews-of-metric-graphs": "Metrik grafiklerinin ön izlemelerini göster" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7802,11 +7903,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7816,6 +7923,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7828,8 +7936,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7856,6 +7976,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7872,8 +7993,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7882,7 +8008,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7895,6 +8023,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7905,6 +8035,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7915,21 +8048,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7959,7 +8103,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7979,11 +8124,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8051,6 +8200,7 @@ "message": "Kullanıcı bulunamadı" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8065,6 +8215,11 @@ "shared-dashboard-users-tab-title": "Paylaşılan pano kullanıcıları" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "Değer seçin" @@ -8075,7 +8230,7 @@ "picker": { "link-all": "Tümü", "option-all": "Tümü", - "option-selected-values": "Seçilen", + "option-selected-values": "", "option-tooltip": "Seçimleri temizle" }, "textbox": { @@ -8101,6 +8256,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8153,9 +8311,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/zh-Hans/grafana.json b/public/locales/zh-Hans/grafana.json index 25797d46241..415369508bd 100644 --- a/public/locales/zh-Hans/grafana.json +++ b/public/locales/zh-Hans/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -801,7 +804,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -822,7 +826,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -894,7 +899,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -925,6 +931,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -956,6 +963,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -988,6 +998,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1046,12 +1057,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1312,6 +1326,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1524,7 +1542,8 @@ "label-target-data-source": "目标数据源" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1799,7 +1818,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1868,7 +1889,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1913,7 +1935,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1938,6 +1962,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1982,7 +2007,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "至" @@ -2003,6 +2029,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2611,6 +2640,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2686,7 +2716,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2775,7 +2806,8 @@ "rows": "行", "tab": "标签页", "tabs": "标签页", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "打开选项窗格", "row": { @@ -2783,6 +2815,10 @@ "hide": "隐藏", "title": "行标题" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2901,13 +2937,13 @@ "title-option": "" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(空缺)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3019,7 +3055,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3491,7 +3533,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "文件路径:", - "managed-by-version-control": "此 {type} 由版本控制管理,无法删除。要移除它,请从存储库中删除它,然后同步以应用更改。", + "managed-by-version-control": "", "ok": "好", "title-cannot-delete-provisioned-resource": "无法删除已配置的资源" }, @@ -3887,6 +3929,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3912,7 +3955,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4026,6 +4070,10 @@ "description": "在寻找 Grafana Explore 应用?它们现在称为 Grafana Drilldown 应用,可以在 <1>菜单 > Drilldown 下找到", "title": "Explore Metrics、Logs、Traces 和 Profiles 已移动!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4074,6 +4122,9 @@ "scan-for-older-logs": "扫描以查找旧日志", "stop-scan": "停止扫描" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4100,6 +4151,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4131,6 +4183,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "关闭查询历史记录", "datasource-a-z": "数据源 A-Z", @@ -4258,6 +4313,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4337,8 +4393,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4353,6 +4413,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5196,6 +5257,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "将链接复制到日志行", "copy-log": "复制日志行", @@ -5248,11 +5313,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5319,14 +5386,20 @@ "when-complete": "快照完成后,您将能够将其上传到您的云堆栈。" }, "building-snapshot": { - "description": "我们正在创建此安装当前状态的时间点快照。快照完成后,您将能够将其上传到 Grafana Cloud。", + "description": "", "description-eta": "创建快照通常需要不到两分钟的时间。", - "title": "正在构建安装快照" + "title": "" }, "can-i-move": { - "body": "将此安装连接到云堆栈后,就可以上传数据源和仪表盘。", - "link-title": "了解如何迁移其他设置", - "title": "我可以将此安装移动到Grafana Cloud吗?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "您还需要一个云堆栈。如果您刚刚注册,我们会自动创建您的第一个堆栈。 如果您已有帐户,您需要选择或创建一个堆栈。", @@ -5335,7 +5408,7 @@ "body-token": "自我管理的 Grafana 安装需要特殊访问权限才能安全迁移内容。您需要在所选云栈上创建一个迁移令牌。", "body-token-field": "迁移令牌", "body-token-field-placeholder": "在此粘贴令牌", - "body-token-instructions": "登录云堆栈,并导航至“管理”、“通用”、“迁移到 Grafana Cloud”。在该屏幕上创建迁移令牌,并将令牌粘贴到此处。", + "body-token-instructions": "", "body-view-stacks": "查看我的云堆栈。", "cancel": "取消", "connect": "连接到这个堆栈", @@ -5379,11 +5452,12 @@ "body": "迁移过程必须从您自己管理的 Grafana 实例开始。", "configure-pdc-link": "配置此堆栈的 PDC", "link-title": "了解更多关于私人数据源连接的信息", - "step-1": "登录到您自己管理的实例,并导航到“管理”、“通用”、“迁移到 Grafana Cloud”。", + "step-1": "", "step-2": "选择“将此实例迁移到云端”。", - "step-3": "系统将提示您输入迁移令牌。您可以从该屏幕生成一个。", - "step-4": "在您自己管理的实例中,选择“上传所有内容”,以将数据源和仪表盘上传到此云堆栈。", - "step-5": "如果某些数据源无法通过公共互联网运行,则需要在您自己管理的环境中安装专用数据源连接。", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "正在执行迁移" }, "is-it-secure": { @@ -5392,7 +5466,7 @@ "title": "它是否安全?" }, "migrate-to-this-stack": { - "body": "您可以将某些资源从自行管理的 Grafana 安装迁移到此云堆栈。为了安全地执行此操作,您需要生成一个迁移令牌。您的自行管理实例将使用该令牌对此云堆栈进行身份验证。", + "body": "", "link-title": "查看完整的迁移指南", "title": "让我们帮助您迁移到此堆栈" }, @@ -5410,7 +5484,7 @@ "modal-close": "关闭", "modal-copy-and-close": "复制到剪贴板并关闭", "modal-copy-button": "复制到剪贴板", - "modal-field-description": "现在就复制令牌,因为您将无法再次看到它。丢失令牌后需要创建一个新令牌。", + "modal-field-description": "", "modal-field-label": "令牌", "modal-title": "已创建迁移令牌", "status": "" @@ -5427,7 +5501,7 @@ "get-session-error-title": "加载迁移配置时出错", "get-snapshot-error-title": "加载快照时出错", "migration-finished-with-caveat-title": "资源迁移完成", - "migration-finished-with-errors-body": "迁移已完成,但某些项目无法迁移到云堆栈。请检查失败的资源,以获取更多详细信息", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "迁移已完成,但有一些警告。请检查个别资源,以了解更多详细信息", "snapshot-error-status-body": "创建快照时或开始迁移流程时出错。有关更多详细信息,请参阅 Grafana 服务器日志", "snapshot-error-status-title": "迁移资源时出错", @@ -5439,7 +5513,7 @@ "pdc": { "body": "将您的数据源暴露在互联网上可能会引发安全问题。专用数据源连接 (PDC) 允许 Grafana Cloud 通过安全的网络隧道访问您的现有数据源。", "link-title": "了解 PDC", - "title": "并非我所有的数据源都在公共互联网上" + "title": "" }, "pricing": { "body": "Grafana Cloud 提供宽松的免费计划和 14 天的无限使用试用期。试用到期后,您将根据超出免费计划限制的使用量付费。", @@ -5517,7 +5591,7 @@ "disconnect": "", "errored-resource-count": "错误", "page-loading": "", - "rebuild-snapshot": "重建快照", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "快照时间戳", "snapshot-not-created": "尚未创建", @@ -5774,7 +5848,7 @@ "subtitle": "管理文件夹仪表板和权限" }, "migrate-to-cloud": { - "subtitle": "将配置从您自己管理的安装复制到云堆栈", + "subtitle": "", "title": "迁移到Grafana Cloud" }, "monitoring": { @@ -6138,7 +6212,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "没有可用更新", @@ -6322,6 +6397,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6336,21 +6412,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_other": "" }, "check-repository": { @@ -6361,13 +6430,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6378,25 +6445,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6414,12 +6477,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6430,14 +6494,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6475,7 +6535,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6493,19 +6552,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6563,16 +6622,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6602,8 +6662,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6682,30 +6741,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6909,6 +6974,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7183,6 +7249,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7191,8 +7258,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7202,7 +7271,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7211,7 +7282,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7237,6 +7309,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7519,16 +7592,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "在单个表中显示太多列可能会影响性能,并使数据难以阅读。请考虑优化您的查询。", @@ -7551,6 +7629,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7562,24 +7642,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7678,6 +7770,7 @@ "date-created": "创建日期:" }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7699,11 +7792,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7715,6 +7810,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7735,7 +7831,8 @@ "now": "现在:", "previously": "以前:", "see-examples": "> 查看示例", - "sentence": "Prometheus 原生直方图具有高分辨率、高精度、在仪表中使用简单等特点,并能在查询和 Grafana 中对直方图进行组合与操作。" + "sentence": "Prometheus 原生直方图具有高分辨率、高精度、在仪表中使用简单等特点,并能在查询和 Grafana 中对直方图进行组合与操作。", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "或查看最近的探索" @@ -7743,11 +7840,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "始终将所选指标图表保持在视图中", "show-previews-of-metric-graphs": "显示指标图表的预览" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7785,11 +7886,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7799,6 +7906,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7811,8 +7919,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7839,6 +7959,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7855,8 +7976,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7865,7 +7991,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7878,6 +8006,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7888,6 +8018,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7898,21 +8031,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7942,7 +8086,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7962,11 +8107,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8034,6 +8183,7 @@ "message": "找不到用户" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8048,6 +8198,11 @@ "shared-dashboard-users-tab-title": "共享数据面板的用户" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "选择值" @@ -8058,7 +8213,7 @@ "picker": { "link-all": "全部", "option-all": "全部", - "option-selected-values": "已选择", + "option-selected-values": "", "option-tooltip": "清除选择" }, "textbox": { @@ -8084,6 +8239,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8136,9 +8294,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, diff --git a/public/locales/zh-Hant/grafana.json b/public/locales/zh-Hant/grafana.json index 807a85d39d4..88383172a08 100644 --- a/public/locales/zh-Hant/grafana.json +++ b/public/locales/zh-Hant/grafana.json @@ -329,6 +329,7 @@ "alert-groups": { "no-results": "", "text-loading-notifications": "", + "title-error-loading-notifications": "", "title-grafana-alerts-delivered-alertmanager": "" }, "alert-instance-details": { @@ -436,6 +437,7 @@ }, "this-might-take-a-while": "", "title-failed-to-load-configuration-history": "", + "title-restore-version": "", "title-restoring-alertmanager-configuration": "" }, "alerts-folder-view": { @@ -510,6 +512,7 @@ "label-continue-matching-subsequent-sibling-nodes": "", "label-group-by": "", "label-label": "", + "label-operator": "", "label-override-general-timings": "", "label-override-grouping": "", "label-value": "", @@ -801,7 +804,8 @@ "title-failed-to-fetch-notification-template": "" }, "error-modal": { - "failed-to-update-your-configuration": "" + "failed-to-update-your-configuration": "", + "title-something-went-wrong": "" }, "error-summary-button": { "error": "" @@ -822,7 +826,8 @@ }, "evaluation-group-creation-modal": { "description-often-rules-group-evaluated": "", - "placeholder-enter-a-name": "" + "placeholder-enter-a-name": "", + "title-new-evaluation-group": "" }, "evaluation-group-loader": { "title-something-wrong-trying-fetch-group-details": "" @@ -894,7 +899,8 @@ "generate-alert-data-modal": { "add-alert-data": "", "add-alert-data-to-payload": "", - "review-alert-payload": "" + "review-alert-payload": "", + "title-add-custom-alerts": "" }, "get-preview-results": { "title-error": "" @@ -925,6 +931,7 @@ "grafana-evaluation-behavior-step": { "missing-series-resolve-placeholder": "", "or": "", + "placeholder-select-an-evaluation-group": "", "text-configure-no-data-and-error-handling": "", "title-set-evaluation-behavior": "" }, @@ -956,6 +963,9 @@ "grafana-rule-design-export-preview": { "text-loading": "" }, + "grafana-rule-design-exporter": { + "title-export-group": "" + }, "grafana-rule-export-preview": { "text-loading": "" }, @@ -988,6 +998,7 @@ "namespace-picker-label-namespace": "" }, "group-by": { + "aria-label-group-by-label-keys": "", "placeholder-group-by": "" }, "group-details": { @@ -1046,12 +1057,15 @@ "alert-rules": "", "confirm-modal": { "confirm": "", + "loading": "", + "loading-body": "", "no-rules-body": "", "no-rules-title": "", "plugin-rules-warning": { "text": "", "title": "" }, + "target-folder-rules": "", "title": "" }, "datasource": { @@ -1312,6 +1326,10 @@ "title-error-loading-alertmanager-config": "", "title-notification-policies-have-changed": "" }, + "notification-policies-tabs": { + "label-mute-timings": "", + "label-notification-policies": "" + }, "notification-policy-matchers": { "default-policy": "", "no-matchers": "" @@ -1524,7 +1542,8 @@ "label-target-data-source": "目標資料來源" }, "recording-rules-name-space-and-group-step": { - "description-select-namespace-group-recording": "" + "description-select-namespace-group-recording": "", + "title-add-namespace-and-group": "" }, "redirect-to-clone-rule": { "title-copy-provisioned-alert-rule": "" @@ -1799,7 +1818,9 @@ "dashboard": "", "data-source-picker-inline-help-title-search-by-data-sources-help": "", "filter-options": { - "aria-label-show-filters": "" + "aria-label-show-filters": "", + "label-custom-filter": "", + "label-saved-searches": "" }, "health": "", "placeholder-all-data-sources": "", @@ -1868,7 +1889,8 @@ "title-silence-alert-rule": "" }, "silence-period": { - "label-silence-start-and-end": "" + "label-silence-start-and-end": "", + "placeholder-select-time-range": "" }, "silenced-instances-preview": { "no-firing-alert-instances-found": "", @@ -1913,7 +1935,9 @@ }, "state-history": { "filter-group": "", - "placeholder-search": "" + "placeholder-search": "", + "text-loading-history": "", + "title-failed-to-fetch-alert-state-history": "" }, "string-array-input": { "add": "", @@ -1938,6 +1962,7 @@ }, "template-form": { "aria-label-template-form": "", + "label-examples-documentation": "", "label-template-group": "", "label-template-group-name": "", "new-template-name-placeholder-give-your-template-group-a-name": "", @@ -1982,7 +2007,8 @@ }, "test-contact-point-modal": { "notification-message": "", - "send-test-notification": "" + "send-test-notification": "", + "title-test-contact-point": "" }, "threshold": { "to": "至" @@ -2003,6 +2029,9 @@ "title-warning": "" } }, + "unknown-rule-list-item": { + "title-unknown-rule-type": "" + }, "unused-contact-point-badge": { "aria-label-unused": "", "text-unused": "" @@ -2611,6 +2640,7 @@ "group-panels": "", "new-row": "", "new-tab": "", + "paste-panel": "", "paste-row": "", "paste-tab": "", "un-group-panels": "" @@ -2686,7 +2716,8 @@ }, "dash-nav": { "render-left-actions": { - "text-public": "" + "text-public": "", + "tooltip-view-as-scene": "" } }, "dashboard-loading": { @@ -2775,7 +2806,8 @@ "rows": "列", "tab": "分頁", "tabs": "分頁", - "variable": "" + "variable": "", + "variable-set": "" }, "open": "開啟選項窗格", "row": { @@ -2783,6 +2815,10 @@ "hide": "隱藏", "title": "行標題" } + }, + "variables": { + "add-variable": "", + "select-variable": "" } }, "empty": { @@ -2901,13 +2937,13 @@ "title-option": "標題" }, "options-pane-options": { + "placeholder-search-options": "", "Recent options-title-recent-options": "" }, "outline": { - "tree": { - "item": { - "empty": "(空)" - } + "tree-item": { + "empty": "", + "no-title": "" } }, "override-category-title": { @@ -3019,7 +3055,13 @@ }, "save-dashboard-diff": { "full-json-diff": "", - "no-changes-in-the-dashboard-json": "" + "no-changes-in-the-dashboard-json": "", + "title-because-dashboard-migrated-grafana-format": "", + "title-folder": "" + }, + "save-dashboard-drawer": { + "label-changes": "", + "label-details": "" }, "save-dashboard-error-proxy": { "title-conflict": "" @@ -3491,7 +3533,7 @@ }, "provisioned-resource-delete-modal": { "file-path": "檔案路徑:", - "managed-by-version-control": "此 {type} 由版本控制管理,無法刪除。若要移除,請從存放庫中刪除,並同步以套用變更。", + "managed-by-version-control": "", "ok": "確定", "title-cannot-delete-provisioned-resource": "無法刪除已佈建的資源" }, @@ -3887,6 +3929,7 @@ }, "resource-dimension-editor": { "label-field": "", + "label-mappings": "", "label-source": "" }, "resource-picker": { @@ -3912,7 +3955,8 @@ "text-dimension-editor": { "label-field": "", "label-source": "", - "label-template": "" + "label-template": "", + "label-value": "" }, "thresholds-editor": { "add-threshold": "", @@ -4026,6 +4070,10 @@ "description": "正在尋找 Grafana Explore 應用程式嗎?現在稱之為 Grafana Drilldown 應用程式,可在<1>功能表 > 詳細資料下方找到", "title": "Explore 指標、日誌、追蹤和設定檔已移動!" }, + "drop-down-menu": { + "aria-label-links": "", + "links": "" + }, "explore": { "title-flame-graph": "", "title-graph": "", @@ -4074,6 +4122,9 @@ "scan-for-older-logs": "掃描舊的日誌", "stop-scan": "停止掃描" }, + "logs-column-search": { + "placeholder-search-fields-by-name": "" + }, "logs-container": { "label-logs": "" }, @@ -4100,6 +4151,7 @@ "title-drag-and-drop-to-reorder": "" }, "logs-table-wrap": { + "aria-label-select-query-by-name": "", "label-select-query": "", "tooltip-select-query-visualize-table": "" }, @@ -4131,6 +4183,9 @@ "raw-list-item": { "tooltip-copy-to-clipboard": "" }, + "references": { + "label-attributes": "" + }, "rich-history": { "close-tooltip": "關閉查詢歷史記錄", "datasource-a-z": "資料來源 A 到 Z", @@ -4258,6 +4313,7 @@ "title-span-bar": "" }, "span-bar-settings": { + "aria-label-selectlabelname": "", "label-label": "", "label-tag-key": "", "placeholder-duration": "", @@ -4337,8 +4393,12 @@ }, "trace-page-actions": { "give-feedback": "", + "label-export": "", "title-share-thoughts-about-tracing-grafana": "" }, + "trace-page-header": { + "text-partial-trace": "" + }, "trace-view": { "no-data": "" }, @@ -4353,6 +4413,7 @@ "label-unique-labels": "", "label-wrap-lines": "", "title-feedback": "", + "title-logs": "", "title-logs-volume": "" }, "unthemed-viewing-layer": { @@ -5196,6 +5257,10 @@ "tooltip-show-log-line": "" } }, + "log-labels": { + "collapse": "", + "expand": "" + }, "log-line-menu": { "copy-link": "將連結複製到日誌行", "copy-log": "複製日誌行", @@ -5248,11 +5313,13 @@ "txt": "" }, "enable-highlighting": "", + "escape-newlines": "", "hide-timestamps": "", "hide-unique-labels": "", "newest-first": "", "oldest-first": "", "prettify-json": "", + "remove-escaping": "", "scroll-bottom": "", "scroll-top": "", "show-timestamps": "", @@ -5319,14 +5386,20 @@ "when-complete": "快照完成後,您就能夠將其上傳到您的雲端堆疊。" }, "building-snapshot": { - "description": "我們正在建立此安裝目前狀態的點時快照。快照完成後,您就可以將其上傳至 Grafana Cloud。", + "description": "", "description-eta": "建立快照通常只需不到兩分鐘。", - "title": "正在建立安裝快照集" + "title": "" }, "can-i-move": { - "body": "將此安裝連接到雲端堆疊後,您就能夠上傳資料來源和儀表板。", - "link-title": "了解移轉其他設定", - "title": "我可以將此安裝移動至 Grafana Cloud 嗎?" + "body": "", + "link-title": "", + "title": "" + }, + "configure-snapshot": { + "description": "", + "description-sub-line": "", + "resource-include-all": "", + "title": "" }, "connect-modal": { "body-cloud-stack": "您還需要一個雲端堆疊。若您剛剛註冊,我們會自動建立您的第一個堆疊。若您已有帳戶,則需要選擇或建立一個堆疊。", @@ -5335,7 +5408,7 @@ "body-token": "您的自我管理 Grafana 安裝需要特殊存取權限,才能安全地移轉內容。您需要在所選的雲端堆疊上建立一個移轉權杖。", "body-token-field": "移轉權杖", "body-token-field-placeholder": "在此處貼上權杖", - "body-token-instructions": "登入您的雲端堆疊,並瀏覽至「管理」、「一般」、「移轉至 Grafana Cloud」。在該畫面上建立一個移轉權杖,並將該權杖貼到此處。", + "body-token-instructions": "", "body-view-stacks": "檢視我的雲端堆疊", "cancel": "取消", "connect": "連接到此堆疊", @@ -5379,11 +5452,12 @@ "body": "遷移程序必須從您自行管理的 Grafana 執行個體中啟動。", "configure-pdc-link": "為此堆疊設定 PDC", "link-title": "進一步了解 Private Data Source Connect", - "step-1": "登入您的自我管理執行個體,並前往「管理」、「一般」、「移轉至 Grafana Cloud」。", + "step-1": "", "step-2": "選擇「將此執行個體移轉至雲端」。", - "step-3": "系統會提示您輸入移轉權杖。從此畫面產生一個。", - "step-4": "在您的自我管理執行個體中,選擇「上傳所有內容」,將資料來源和儀表板上傳至此雲端堆疊。", - "step-5": "若您的某些資料來源無法透過公用網際網路使用,則需要在自我管理環境中安裝 Private Data Source Connect。", + "step-3": "", + "step-4": "", + "step-5": "", + "step-6": "", "title": "執行移轉" }, "is-it-secure": { @@ -5392,7 +5466,7 @@ "title": "這安全嗎?" }, "migrate-to-this-stack": { - "body": "您可以將某些資源從自我管理的 Grafana 安裝移轉至此雲端堆疊。為了安全地執行此動作,您需要產生一個移轉權杖。您的自我管理執行個體將使用權杖驗證此雲端堆疊。", + "body": "", "link-title": "檢視完整的移轉指南", "title": "讓我們協助您移轉至此堆疊" }, @@ -5410,7 +5484,7 @@ "modal-close": "關閉", "modal-copy-and-close": "複製至剪貼簿並關閉", "modal-copy-button": "複製至剪貼簿", - "modal-field-description": "請立即複製權杖,因為您將無法再次看到它。若您遺失權杖,則需要建立新的權杖。", + "modal-field-description": "", "modal-field-label": "權杖", "modal-title": "已建立移轉權杖", "status": "目前狀態:<2>" @@ -5427,7 +5501,7 @@ "get-session-error-title": "載入移轉設定時發生錯誤", "get-snapshot-error-title": "載入快照集時發生錯誤", "migration-finished-with-caveat-title": "資源移轉完成", - "migration-finished-with-errors-body": "移轉已完成,但某些項目無法移轉至雲端堆疊。檢查失敗的資源以了解更多詳細資訊", + "migration-finished-with-errors-body": "", "migration-finished-with-warnings-body": "移轉已完成,但有一些警告。檢查個別資源以了解更多詳細資訊", "snapshot-error-status-body": "建立快照或開始移轉流程時發生錯誤。請參閱 Grafana 伺服器日誌,以了解更多詳細資訊", "snapshot-error-status-title": "移轉資源時發生錯誤", @@ -5439,7 +5513,7 @@ "pdc": { "body": "將您的資料來源公開到網際網路可能會引起安全性問題。私人資料來源連接 (PDC) 允許 Grafana Cloud 透過安全的網路通道存取您現有的資料來源。", "link-title": "了解 PDC", - "title": "並非我所有的資料來源都在公用網際網路上" + "title": "" }, "pricing": { "body": "Grafana Cloud 有好康的免費方案和 14 天無限制使用試用。試用期過後,我們會根據免費方案限制的使用情況向您收取費用。", @@ -5517,7 +5591,7 @@ "disconnect": "取消連線", "errored-resource-count": "錯誤", "page-loading": "正在載入…", - "rebuild-snapshot": "重新建立快照集", + "rebuild-snapshot": "", "show-errors": "", "snapshot-date": "快照時間戳記", "snapshot-not-created": "尚未建立", @@ -5774,7 +5848,7 @@ "subtitle": "管理資料夾儀表板及權限" }, "migrate-to-cloud": { - "subtitle": "將設定從自我管理安裝複製到雲端堆疊", + "subtitle": "", "title": "移轉至 Grafana Cloud" }, "monitoring": { @@ -6138,7 +6212,8 @@ "aria-label-plugin-type-filter": "", "label-search": "", "label-state": "", - "label-type": "" + "label-type": "", + "subtitle": "" }, "catalog": { "no-updates-available": "沒有可用的更新", @@ -6322,6 +6397,7 @@ "text-loading-organizations": "" }, "user-sessions": { + "browser-details": "", "now": "", "sessions": "" }, @@ -6336,21 +6412,14 @@ }, "provisioning": { "bootstrap-step": { - "dashboards-unavailable-while-running-process": "", "description-clear-repository-connection": "", "empty": "", "error-field-required": "", + "ext-storage": "", "grafana": "", - "include-history": "", "label-display-name": "", - "label-migrate-options": "", "placeholder-my-repository-connection": "", - "repository": "", - "resources-will-be-added": "", "text-loading-resource-information": "", - "title-files-exist-in-the-target": "", - "title-note": "", - "tooltip-include-history": "", "files-count_other": "" }, "check-repository": { @@ -6361,13 +6430,11 @@ "alert-repository-settings-updated": "", "button-save": "", "button-saving": "", - "description-branch": "", "description-enabled": "", "description-path": "", + "description-read-only": "", "description-repository-url": "", "description-title": "", - "description-workflows-makes-repository": "", - "description-write": "", "error-required": "", "error-save-repository": "", "error-valid-github-url": "", @@ -6378,25 +6445,21 @@ "label-interval-seconds": "", "label-local-path": "", "label-path": "", + "label-pr-workflow": "", "label-repository-type": "", "label-repository-url": "", "label-target": "", "label-title": "", - "label-workflows": "", - "option-branch": "", "option-entire-instance": "", "option-github": "", "option-local": "", "option-managed-folder": "", - "option-write": "", "placeholder-branch": "", "placeholder-github-token": "", "placeholder-github-url": "", "placeholder-interval-seconds": "", "placeholder-local-path": "", "placeholder-my-config": "", - "placeholder-path": "", - "placeholder-readonly-repository": "", "placeholder-select-repository-type": "" }, "config-form-github-collapse": { @@ -6414,12 +6477,13 @@ "title-webhook-will-be-created": "" }, "connect-repository-button": { + "configure": "", "configure-file": "", "configure-git-sync": "", "repository-limit-info-alert": "" }, "connect-step": { - "description-choose-storage-resources": "", + "description-branch": "", "description-github-path": "", "description-paste-your-git-hub-personal-access-token": "", "description-repository-url": "", @@ -6430,14 +6494,10 @@ "label-local-path": "", "label-path": "", "label-repository-url": "", - "label-storage-type": "", "placeholder-branch": "", - "placeholder-github-path": "", "placeholder-github-token": "", "placeholder-github-url": "", - "placeholder-local-path": "", - "storage-type-github": "", - "storage-type-local": "" + "placeholder-local-path": "" }, "delete-repository-button": { "button-delete": "", @@ -6475,7 +6535,6 @@ }, "manage-dashboards-provision-updates-automatically": "", "manage-your-dashboards-with-remote-provisioning": "", - "migrate-existing-dashboards-storage-provisioning": "", "store-dashboards-in-version-controlled-storage": "" }, "file-history-page": { @@ -6493,19 +6552,19 @@ "placeholder-search": "" }, "finish-step": { - "description-dashboard-previews": "", - "description-enable-webhooks": "", + "description-enable-previews": "", + "description-image-rendering": "", "description-often-shall-instance-updates-git-hub": "", - "description-select-workflows-allowed-within-repository": "", - "error-field-required": "", - "label-enable-dashboard-previews": "", - "label-enable-webhooks": "", + "description-pr-enable-description": "", + "description-read-only": "", + "description-webhooks-enable": "", + "label-enable-previews": "", + "label-pr-workflow": "", + "label-read-only": "", "label-update-instance-interval-seconds": "", - "label-workflows": "", - "link-setup-image-rendering": "", "placeholder": "", - "placeholder-readonly-repository": "", - "text-requires-image-rendering": "" + "text-setup-later": "", + "title-enhance-github": "" }, "folder-repository-list": { "no-results-matching-your-query": "", @@ -6563,16 +6622,17 @@ }, "summary": "" }, - "job-step": { - "error-failed-to-start": "", - "error-invalid-response": "", - "error-job-failed": "" - }, - "migrate-step": { - "description-migrating-dashboards": "" - }, - "pull-step": { - "description-pulling-content": "" + "mode-options": { + "folder": { + "description": "", + "label": "", + "subtitle": "" + }, + "instance": { + "description": "", + "label": "", + "subtitle": "" + } }, "recent-jobs": { "active-jobs": "", @@ -6602,8 +6662,7 @@ "title-repository-is-unhealthy": "" }, "repository-link": { - "grafana-repository": "", - "view-folder": "", + "grafana-repository-synced": "", "view-repository": "" }, "repository-overview": { @@ -6682,30 +6741,36 @@ "title-pull-not-enabled": "", "tooltip-unhealthy-repository": "" }, + "synchronize-step": { + "error-no-job-id": "", + "error-no-repository-name": "", + "error-starting-job": "", + "synchronization-description": "", + "synchronization-options": "" + }, "token-permissions-info": { - "access": "", - "contents": "", - "github-instructions": "", - "metadata": "", - "permission": "", - "pull-requests": "", - "read-and-write": "", - "readonly": "", - "webhooks": "" + "and-click": "", + "go-to": "", + "make-sure": "" }, "wizard": { - "button-finish": "", + "alert-point-1": "", + "alert-point-2": "", + "alert-point-3": "", + "alert-point-4": "", + "alert-title": "", "button-next": "", "button-start": "", "step-bootstrap": "", "step-connect": "", "step-finish": "", - "step-resources": "", + "step-synchronize": "", + "sync-description": "", + "sync-option-history": "", "title-bootstrap": "", "title-connect": "", "title-finish": "", - "title-migrate": "", - "title-pull": "" + "title-synchronize": "" }, "wizard-content": { "button-cancel": "", @@ -6909,6 +6974,7 @@ }, "query-group": { "add-query": "", + "expression": "", "title-data-source-help": "" }, "query-group-options-editor": { @@ -7183,6 +7249,7 @@ "copy-clipboard": "", "copy-to-clipboard-and-close": "", "description-name-to-easily-identify-the-token": "", + "description-token": "", "generate-token": "", "label-display-name": "", "label-expiration": "", @@ -7191,8 +7258,10 @@ }, "get-actions-cell": { "add-token": "", + "aria-label-delete-button": "", "disable": "", - "enable": "" + "enable": "", + "tooltip-managed-service-account-cannot-modified": "" }, "get-role-cell": { "aria-label-role": "" @@ -7202,7 +7271,9 @@ "aria-label-role": "", "disable": "", "enable": "", - "title-tokens": "" + "title-tokens": "", + "tooltip-delete-button": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-page-unconnected": { "add-service-account-token": "", @@ -7211,7 +7282,8 @@ "enable-service-account": "", "title-delete-service-account": "", "title-disable-service-account": "", - "tokens": "" + "tokens": "", + "tooltip-managed-service-account-cannot-modified": "" }, "service-account-permissions": { "title-permissions": "" @@ -7237,6 +7309,7 @@ "service-accounts-list-page-unconnected": { "add-service-account": "", "placeholder-search-service-account-by-name": "", + "sub-title": "", "title-delete-service-account": "", "title-disable-service-account": "" }, @@ -7519,16 +7592,21 @@ }, "support-bundles-create-unconnected": { "cancel": "", - "create": "" + "create": "", + "sub-title": "" }, "support-bundles-unconnected": { "created-on": "", "download": "", "expires": "", "requested-by": "", + "sub-title": "", "tooltip-remove-bundle": "" } }, + "swagger": { + "login": "" + }, "table": { "container": { "content": "在單個表格中顯示太多欄位可能會影響效能,並使資料難以讀取。請考慮縮小您的查詢範圍。", @@ -7551,6 +7629,8 @@ "teams": { "create-team": { "create": "", + "description-email": "", + "label-email": "", "label-name": "", "label-role": "" }, @@ -7562,24 +7642,36 @@ }, "team-group-sync": { "add-group": "", + "add-group-button": "", + "aria-label-remove": "", "external-group-id": "", "external-group-sync": "", - "title-there-external-groups": "" + "label-add-external-group": "", + "team-sync-upgrade": "", + "title-there-external-groups": "", + "tooltip-add-external-group": "" }, "team-list": { "columns": { - "title-edit-team": "" + "aria-label-delete-button": "", + "aria-label-edit-team": "", + "title-edit-team": "", + "tooltip-edit-team": "" }, "new-team": "", "placeholder-search-teams": "" }, "team-settings": { + "description-email": "", "label-email": "", "label-name": "", "label-numerical-identifier": "", "label-role": "", "label-team-details": "", "save": "" + }, + "team-sync-upgrade-content": { + "description": "" } }, "theme-preview": { @@ -7678,6 +7770,7 @@ "date-created": "建立日期:" }, "create-bookmark-saved-notification": { + "description": "", "view-bookmarks": "" }, "data-trail-card": { @@ -7699,11 +7792,13 @@ }, "label-breakdown-scene": { "label-search": "", - "label-view": "" + "label-view": "", + "title-warning-there-missing-telemetry-resource-attributes": "" }, "metric-action-bar": { "open": "", "select-new-metric": "", + "tooltip-bookmark": "", "tooltip-open-in-explore": "", "tooltip-remove-existing-metric-choose": "" }, @@ -7715,6 +7810,7 @@ }, "metric-select-scene": { "label-otel-experience": "", + "label-search-metrics": "", "placeholder-search-metrics": "", "title-unable-to-retrieve-all-metric-names": "", "unable-to-retrieve-metric-names": "" @@ -7735,7 +7831,8 @@ "now": "現在:", "previously": "之前:", "see-examples": "> 查看範例", - "sentence": "Prometheus 原生直方圖提供高解析度、高精度、儀器中的簡單使用方法,以及在查詢和 Grafana 中組合和操縱直方圖的方法。" + "sentence": "Prometheus 原生直方圖提供高解析度、高精度、儀器中的簡單使用方法,以及在查詢和 Grafana 中組合和操縱直方圖的方法。", + "title-native-histogram-support": "" }, "recent-metrics": { "or-view-a-recent-exploration": "或檢視最近的探索" @@ -7743,11 +7840,15 @@ "related-logs-scene": { "tooltip-navigate-to-the-explore-logs-app": "" }, + "search-input": { + "title-clear-search": "" + }, "settings": { "always-keep-selected-metric-graph-in-view": "隨時在檢視中保留所選的指標圖表", "show-previews-of-metric-graphs": "顯示指標圖表的預覽" }, "sort-by-scene": { + "placeholder-choose-criteria": "", "tooltip-sorts-values-using-standard-smart-series": "" } }, @@ -7785,11 +7886,17 @@ "label-config-query": "" }, "convert-field-type-transformer-editor": { + "aria-label-add-a-convert-field-type-transformer": "", + "aria-label-remove-convert-field-type-transformer": "", + "label-as": "", "label-date-format": "", + "label-field": "", "label-input-format": "", "label-join-with": "", "label-set-timezone": "", + "placeholder-type": "", "tooltip-explicit-separator-joining-array-values": "", + "tooltip-input-format": "", "tooltip-specify-the-output-format": "", "tooltip-timezone-manually": "" }, @@ -7799,6 +7906,7 @@ "placeholder-field": "" }, "draggable-field-name": { + "rename-placeholder": "", "title-drag-and-drop-to-reorder": "" }, "enum-mapping-editor": { @@ -7811,8 +7919,20 @@ }, "extract-fields-transformer-editor": { "label-delimiter": "", + "label-format": "", + "label-keep-time": "", + "label-replace-all-fields": "", + "label-source": "", + "placeholder-auto": "", "placeholder-select-delimiter": "" }, + "field-config-mapping-handler-arguments-editor": { + "aria-label-threshold-color": "" + }, + "field-lookup-transformer-editor": { + "label-field": "", + "label-lookup": "" + }, "field-to-config-mapping-editor": { "additional-settings": "", "field": "", @@ -7839,6 +7959,7 @@ "label-filter-type": "" }, "format-string-transfomer-editor": { + "label-field": "", "label-format": "", "label-substring-range": "" }, @@ -7855,8 +7976,13 @@ "placeholder-ignored": "", "placeholder-select-stats": "" }, + "group-by-transformer-editor": { + "title-calc-alert": "" + }, "group-to-nested-table-transformer-editor": { - "label-show-field-names-in-nested-tables": "" + "description-show-field-names": "", + "label-show-field-names-in-nested-tables": "", + "title-calc-alert": "" }, "grouping-to-matrix-transformer-editor": { "label-cell-value": "", @@ -7865,7 +7991,9 @@ "label-row": "" }, "histogram-transformer-editor": { - "placeholder-default": "" + "placeholder-auto": "", + "placeholder-default": "", + "placeholder-none": "" }, "index-options-editor": { "label-as-percentile": "", @@ -7878,6 +8006,8 @@ "title-no-input-found": "", "title-no-labels-found": "" }, + "label-join": "", + "label-value": "", "tooltip-select-label-indicating-values": "" }, "jsonpath-editor": { @@ -7888,6 +8018,9 @@ "tooltip-remove-path": "" }, "labels-as-fields-transformer-editor": { + "label-labels": "", + "label-mode": "", + "label-value-field-name": "", "placeholder-optional-select-label": "", "tooltip-replace-value-field-label": "" }, @@ -7898,21 +8031,32 @@ "ascending-order-duplicates": "", "multiple-value-fields-may-exist": "", "single-frame": "", - "st-field-is-time": "" + "st-field-is-time": "", + "string-values-separate": "" }, "merge-transformer-editor": { "merge-effect-applied-single-frame": "" }, "multi-info": { "all-values-are-numeric": "", + "frame-fields-value": "", "multiple-frames": "", "string-values-are-represented-as-labels": "", "time-in-ascending-order": "" }, + "organize-fields-transformer-editor": { + "first-frame-warning": "" + }, + "partion-by-values-editor": { + "tooltip-naming": "" + }, "partition-by-values-editor": { "label-field": "", + "label-keep-fields": "", + "label-naming": "", "label-select-field": "", - "partition-values-works-single-frame": "" + "partition-values-works-single-frame": "", + "tooltip-keeps-partition-fields-frames": "" }, "prepare-time-series-editor": { "label-format": "", @@ -7942,7 +8086,8 @@ "label-model-type": "", "label-predicted-points": "", "label-x-field": "", - "label-y-field": "" + "label-y-field": "", + "tooltip-number-of-xy-points-to-predict": "" }, "rename-by-regex-transformer-editor": { "label-match": "", @@ -7962,11 +8107,15 @@ "time-series-table-transform-editor": { "label-stat": "", "label-time-field": "", - "tooltip-statistic-should-calculated-series": "" + "tooltip-statistic-should-calculated-series": "", + "tooltip-time-field": "" }, "transpose-transfomer-editor": { + "label-first-field-name": "", + "label-remaining-fields-name": "", "placeholder-field": "", - "placeholder-value": "" + "placeholder-value": "", + "tooltip-name-for-value-fields": "" }, "unary-operation-editor": { "label-operation": "", @@ -8034,6 +8183,7 @@ "message": "沒有找到使用者" }, "token-revoked-modal": { + "resume-message": "", "sign-in": "", "title-you-have-been-automatically-signed-out": "" }, @@ -8048,6 +8198,11 @@ "shared-dashboard-users-tab-title": "共用儀表板使用者" } }, + "variabels": { + "variable-usages-button": { + "usage-title": "" + } + }, "variable": { "adhoc": { "placeholder": "選擇值" @@ -8058,7 +8213,7 @@ "picker": { "link-all": "全部", "option-all": "全部", - "option-selected-values": "已選取", + "option-selected-values": "", "option-tooltip": "清除選取項目" }, "textbox": { @@ -8084,6 +8239,9 @@ "no-unknowns": { "no-renamed-or-missing-variables-found": "" }, + "picker-renderer": { + "couldnt-load-variable": "" + }, "query-variable-refresh-select": { "description-update-values-variable": "", "label-refresh": "" @@ -8136,9 +8294,12 @@ "title-dependencies": "" }, "variables-unknown-button": { + "usage-title": "", "VariablesUnknownButton-tooltip-show-usages": "" }, "variables-unknown-table": { + "collapse-label": "", + "collapse-tooltip": "", "loading": "" } }, From 3607356f65e9a7da9383122da4242f9d5c13b67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Fern=C3=A1ndez?= Date: Fri, 11 Apr 2025 11:24:40 +0200 Subject: [PATCH 26/37] i18n: Update `internationalization/dates.ts` to use `locale` (#103731) --- packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 8 ++++++++ pkg/api/index.go | 3 ++- pkg/services/preference/prefimpl/pref_test.go | 12 ++++++------ public/app/app.ts | 2 +- public/app/core/internationalization/dates.ts | 18 +++++++++--------- 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index 2426ed3338c..dbfbc5eeeb3 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -246,6 +246,7 @@ export interface GrafanaConfig { * Grafana's supported language. */ language: string | undefined; + locale: string; } export interface SqlConnectionLimits { diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index e6444cdb80c..8b0beab714f 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -208,6 +208,12 @@ export class GrafanaBootConfig implements GrafanaConfig { */ language: string | undefined; + /** + * Locale used in Grafana's UI. Default to 'es-US' in the backend and overwritten when the user select a different one in SharedPreferences. + * This is the locale that is used for date formatting and other locale-specific features. + */ + locale: string; + constructor(options: GrafanaBootConfig) { this.bootData = options.bootData; @@ -243,6 +249,8 @@ export class GrafanaBootConfig implements GrafanaConfig { this.theme2 = getThemeById(this.bootData.user.theme); this.bootData.user.lightTheme = this.theme2.isLight; this.theme = this.theme2.v1; + + this.locale = options.bootData.user.locale; } geomapDefaultBaseLayer?: MapLayerOptions | undefined; listDashboardScopesEndpoint?: string | undefined; diff --git a/pkg/api/index.go b/pkg/api/index.go index 68423b8d0f8..016a78366c6 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -69,7 +69,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV // Locale is used for some number and date/time formatting, whereas language is used just for // translating words in the interface acceptLangHeader := c.Req.Header.Get("Accept-Language") - locale := "en-US" // default to en-US formatting, but use the accept-lang header or user's preference + locale := "en-US" // default to en formatting, but use the accept-lang header or user's preference language := "" // frontend will set the default language urlPrefs := getURLPrefs(c) @@ -85,6 +85,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV } if hs.Features.IsEnabled(c.Req.Context(), featuremgmt.FlagLocaleFormatPreference) { + locale = "en" // default to "en", not "en-US", matching the locale code if urlPrefs.Locale != "" { locale = urlPrefs.Locale } else if prefs.JSONData.Locale != "" { diff --git a/pkg/services/preference/prefimpl/pref_test.go b/pkg/services/preference/prefimpl/pref_test.go index afc40f9e37f..85a859935ed 100644 --- a/pkg/services/preference/prefimpl/pref_test.go +++ b/pkg/services/preference/prefimpl/pref_test.go @@ -92,7 +92,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) { WeekStart: &weekStartOne, JSONData: &pref.PreferenceJSONData{ Language: "en-GB", - Locale: "en-US", + Locale: "en", }, }, pref.Preference{ @@ -104,7 +104,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) { WeekStart: &weekStartTwo, JSONData: &pref.PreferenceJSONData{ Language: "en-AU", - Locale: "es-ES", + Locale: "es", }, }, ) @@ -120,7 +120,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) { HomeDashboardID: 4, JSONData: &pref.PreferenceJSONData{ Language: "en-AU", - Locale: "es-ES", + Locale: "es", }, } if diff := cmp.Diff(expected, preference); diff != "" { @@ -140,7 +140,7 @@ func TestGetWithDefaults_withUserAndOrgPrefs(t *testing.T) { HomeDashboardID: 1, JSONData: &pref.PreferenceJSONData{ Language: "en-GB", - Locale: "en-US", + Locale: "en", }, } if diff := cmp.Diff(expected, preference); diff != "" { @@ -162,7 +162,7 @@ func TestGetDefaults_JSONData(t *testing.T) { Language: "en-GB", } orgPreferencesWithLocaleJsonData := pref.PreferenceJSONData{ - Locale: "en-US", + Locale: "en", } team2PreferencesJsonData := pref.PreferenceJSONData{} team1PreferencesJsonData := pref.PreferenceJSONData{} @@ -248,7 +248,7 @@ func TestGetDefaults_JSONData(t *testing.T) { require.Equal(t, &pref.Preference{ WeekStart: &weekStart, JSONData: &pref.PreferenceJSONData{ - Locale: "en-US", + Locale: "en", QueryHistory: queryPreference, }, }, preference) diff --git a/public/app/app.ts b/public/app/app.ts index ef7460001e7..647124ce8dc 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -135,7 +135,7 @@ export class GrafanaApp { // This needs to be done after the `initEchoSrv` since it is being used under the hood. startMeasure('frontend_app_init'); - setLocale(config.bootData.user.locale); + setLocale(config.locale); setWeekStart(config.bootData.user.weekStart); setPanelRenderer(PanelRenderer); setPluginPage(PluginPage); diff --git a/public/app/core/internationalization/dates.ts b/public/app/core/internationalization/dates.ts index 8f0aaa14d7c..bdadd9ba863 100644 --- a/public/app/core/internationalization/dates.ts +++ b/public/app/core/internationalization/dates.ts @@ -2,16 +2,16 @@ import '@formatjs/intl-durationformat/polyfill'; import deepEqual from 'fast-deep-equal'; import memoize from 'micro-memoize'; -import { getI18next } from './index'; +import { config } from 'app/core/config'; const deepMemoize: typeof memoize = (fn) => memoize(fn, { isEqual: deepEqual }); -const createDateTimeFormatter = deepMemoize((language: string, options: Intl.DateTimeFormatOptions) => { - return new Intl.DateTimeFormat(language, options); +const createDateTimeFormatter = deepMemoize((locale: string, options: Intl.DateTimeFormatOptions) => { + return new Intl.DateTimeFormat(locale, options); }); -const createDurationFormatter = deepMemoize((language: string, options: Intl.DurationFormatOptions) => { - return new Intl.DurationFormat(language, options); +const createDurationFormatter = deepMemoize((locale: string, options: Intl.DurationFormatOptions) => { + return new Intl.DurationFormat(locale, options); }); export const formatDate = deepMemoize( @@ -20,17 +20,17 @@ export const formatDate = deepMemoize( return formatDate(new Date(value), format); } - const i18n = getI18next(); - const dateFormatter = createDateTimeFormatter(i18n.language, format); + const locale = config.locale; + const dateFormatter = createDateTimeFormatter(locale, format); return dateFormatter.format(value); } ); export const formatDuration = deepMemoize( (duration: Intl.DurationInput, options: Intl.DurationFormatOptions = {}): string => { - const i18n = getI18next(); + const locale = config.locale; - const dateFormatter = createDurationFormatter(i18n.language, options); + const dateFormatter = createDurationFormatter(locale, options); return dateFormatter.format(duration); } ); From 8ebce76535bef5bbaf1c20e8c8f913517473e681 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Fri, 11 Apr 2025 11:32:31 +0200 Subject: [PATCH 27/37] UserStorage: multiple improvements (#103779) --- packages/grafana-runtime/src/internal/index.ts | 2 ++ .../src/utils/DataSourceWithBackend.test.ts | 17 +++++++++++++++++ .../src/utils/DataSourceWithBackend.ts | 4 ++++ .../src/utils/userStorage.test.tsx | 10 +++++----- .../grafana-runtime/src/utils/userStorage.tsx | 10 +++++----- 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/grafana-runtime/src/internal/index.ts b/packages/grafana-runtime/src/internal/index.ts index e7119717bf3..de669f7af74 100644 --- a/packages/grafana-runtime/src/internal/index.ts +++ b/packages/grafana-runtime/src/internal/index.ts @@ -25,3 +25,5 @@ export { setGetObservablePluginLinks, type GetObservablePluginLinks, } from '../services/pluginExtensions/getObservablePluginLinks'; + +export { UserStorage } from '../utils/userStorage'; diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts index c256f77baea..e249d727a68 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.test.ts @@ -37,6 +37,14 @@ class MyDataSource extends DataSourceWithBackend { applyTemplateVariables(query: MyQuery, scopedVars: ScopedVars, filters?: AdHocVariableFilter[] | undefined): MyQuery { return { ...query, applyTemplateVariablesCalled: true, filters }; } + + async getValue(key: string) { + return await this.userStorage.getItem(key); + } + + async setValue(key: string, value: string) { + await this.userStorage.setItem(key, value); + } } const mockDatasourceRequest = jest.fn, BackendSrvRequest[]>(); @@ -536,6 +544,15 @@ describe('DataSourceWithBackend', () => { expect(publicDashboardQueryHandler).toHaveBeenCalledWith(request); }); }); + + describe('user storage', () => { + test('sets and gets a value', async () => { + const { ds } = createMockDatasource(); + + await ds.setValue('multiplier', '1'); + expect(await ds.getValue('multiplier')).toBe('1'); + }); + }); }); function createMockDatasource() { diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index 3d5a32dc252..ab3a2b7670d 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -34,6 +34,7 @@ import { import { publicDashboardQueryHandler } from './publicDashboardQueryHandler'; import { BackendDataSourceResponse, toDataQueryResponse } from './queryResponse'; +import { UserStorage } from './userStorage'; /** * @internal @@ -121,8 +122,11 @@ class DataSourceWithBackend< TQuery extends DataQuery = DataQuery, TOptions extends DataSourceJsonData = DataSourceJsonData, > extends DataSourceApi { + protected userStorage: UserStorage; + constructor(instanceSettings: DataSourceInstanceSettings) { super(instanceSettings); + this.userStorage = new UserStorage(instanceSettings.type); } /** diff --git a/packages/grafana-runtime/src/utils/userStorage.test.tsx b/packages/grafana-runtime/src/utils/userStorage.test.tsx index 46999241686..91fe2374508 100644 --- a/packages/grafana-runtime/src/utils/userStorage.test.tsx +++ b/packages/grafana-runtime/src/utils/userStorage.test.tsx @@ -48,15 +48,15 @@ describe('userStorage', () => { it('use localStorage if the user is not logged in', async () => { config.bootData.user.isSignedIn = false; const storage = usePluginUserStorage(); - storage.getItem('key'); - expect(localStorage.getItem).toHaveBeenCalled(); + await storage.getItem('key'); + expect(localStorage.getItem).toHaveBeenCalledWith('plugin-id:abc:key'); }); it('use localStorage if the user storage is not found', async () => { request.mockReturnValue(Promise.reject({ status: 404 } as FetchError)); const storage = usePluginUserStorage(); await storage.getItem('key'); - expect(localStorage.getItem).toHaveBeenCalled(); + expect(localStorage.getItem).toHaveBeenCalledWith('plugin-id:abc:key'); }); it('returns the value from the user storage', async () => { @@ -73,8 +73,8 @@ describe('userStorage', () => { it('use localStorage if the user is not logged in', async () => { config.bootData.user.isSignedIn = false; const storage = usePluginUserStorage(); - storage.setItem('key', 'value'); - expect(localStorage.setItem).toHaveBeenCalled(); + await storage.setItem('key', 'value'); + expect(localStorage.setItem).toHaveBeenCalledWith('plugin-id:abc:key', 'value'); }); it('creates a new user storage if it does not exist', async () => { diff --git a/packages/grafana-runtime/src/utils/userStorage.tsx b/packages/grafana-runtime/src/utils/userStorage.tsx index 57ed8354404..245322e0a68 100644 --- a/packages/grafana-runtime/src/utils/userStorage.tsx +++ b/packages/grafana-runtime/src/utils/userStorage.tsx @@ -37,9 +37,9 @@ async function apiRequest(requestOptions: RequestOptions) { /** * A class for interacting with the backend user storage. - * Unexported because it is currently only be used through the useUserStorage hook. + * Exposed internally only to avoid misuse (wrong service name).. */ -class UserStorage { +export class UserStorage { private service: string; private resourceName: string; private userUID: string; @@ -76,13 +76,13 @@ class UserStorage { async getItem(key: string): Promise { if (!this.canUseUserStorage) { // Fallback to localStorage - return localStorage.getItem(this.resourceName); + return localStorage.getItem(`${this.resourceName}:${key}`); } // Ensure this.storageSpec is initialized await this.init(); if (!this.storageSpec) { // Also, fallback to localStorage for backward compatibility - return localStorage.getItem(this.resourceName); + return localStorage.getItem(`${this.resourceName}:${key}`); } return this.storageSpec.data[key]; } @@ -90,7 +90,7 @@ class UserStorage { async setItem(key: string, value: string): Promise { if (!this.canUseUserStorage) { // Fallback to localStorage - localStorage.setItem(key, value); + localStorage.setItem(`${this.resourceName}:${key}`, value); return; } From 00dcf482cf1118deb2f125d481e9c6966d45f0b8 Mon Sep 17 00:00:00 2001 From: Alexa V <239999+axelavargas@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:42:48 +0200 Subject: [PATCH 28/37] Dashboard -Schema V2 Stateless annotations (dsRef independent) (#102949) * Implement basic behavior of stateless annotation * Fix ds defined getting lost * Fix linter * adjust test to be aligned with datasources not automatically being assigned to the model * Add unit test for annotations --------- Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> Co-authored-by: Ivan Ortega Alba --- .../DashboardSceneSerializer.test.ts | 45 +++++++++ .../serialization/DashboardSceneSerializer.ts | 16 +++ ...sformSceneToSaveModelSchemaV2.test.ts.snap | 4 - .../transformSceneToSaveModelSchemaV2.test.ts | 76 +++++++++++++- .../transformSceneToSaveModelSchemaV2.ts | 98 +++++++++++++------ 5 files changed, 203 insertions(+), 36 deletions(-) diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts index 69484792891..084121cc917 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.test.ts @@ -1078,6 +1078,51 @@ describe('DashboardSceneSerializer', () => { serializer.initializeDSReferencesMapping({ elements: {} } as DashboardV2Spec); expect(serializer.getDSReferencesMapping().panels.size).toBe(0); }); + + it('should initialize datasource references mapping when annotations dont have datasources', () => { + const saveModel: DashboardV2Spec = { + ...defaultDashboardV2Spec(), + title: 'Dashboard with annotations without datasource', + annotations: [ + { + kind: 'AnnotationQuery', + spec: { + name: 'Annotation 1', + query: { kind: 'prometheus', spec: {} }, + enable: true, + hide: false, + iconColor: 'red', + }, + }, + ], + }; + + serializer.initializeDSReferencesMapping(saveModel); + + const dsReferencesMap = serializer.getDSReferencesMapping(); + + // Annotation 1 should have no datasource + expect(dsReferencesMap.annotations.has('Annotation 1')).toBe(true); + }); + + it('should return early if the saveModel is not a V2 dashboard', () => { + const v1SaveModel: Dashboard = { + title: 'Test Dashboard', + uid: 'my-uid', + schemaVersion: 30, + panels: [ + { id: 1, title: 'Panel 1', type: 'text' }, + { id: 2, title: 'Panel 2', type: 'text' }, + ], + }; + serializer.initializeDSReferencesMapping(v1SaveModel as unknown as DashboardV2Spec); + expect(serializer.getDSReferencesMapping()).toEqual({ + panels: new Map(), + variables: new Set(), + annotations: new Set(), + }); + expect(serializer.getDSReferencesMapping().panels.size).toBe(0); + }); }); describe('V1DashboardSerializer', () => { diff --git a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts index edf0fa2125c..e6f29dc9d68 100644 --- a/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/DashboardSceneSerializer.ts @@ -2,6 +2,7 @@ import { Dashboard } from '@grafana/schema'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { AnnoKeyDashboardSnapshotOriginalUrl } from 'app/features/apiserver/types'; import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types'; +import { isDashboardV2Spec } from 'app/features/dashboard/api/utils'; import { SaveDashboardAsOptions } from 'app/features/dashboard/components/SaveDashboard/types'; import { DASHBOARD_SCHEMA_VERSION } from 'app/features/dashboard/state/DashboardMigrator'; import { @@ -235,6 +236,12 @@ export class V2DashboardSerializer } initializeDSReferencesMapping(saveModel: DashboardV2Spec | undefined) { + // The saveModel could be undefined or not a DashboardV2Spec + // when dashboardsNewLayout is enabled, saveModel could be v1 + // in those cases, only when saving we will convert to v2 + if (saveModel === undefined || (saveModel && !isDashboardV2Spec(saveModel))) { + return; + } // initialize the object this.defaultDsReferencesMap = { panels: new Map>(), @@ -274,6 +281,15 @@ export class V2DashboardSerializer } } } + + // initialize annotations ds references map + if (saveModel?.annotations) { + for (const annotation of saveModel.annotations) { + if (!annotation.spec.datasource) { + this.defaultDsReferencesMap.annotations.add(annotation.spec.name); + } + } + } } getDSReferencesMapping() { diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap index 41482314925..1f802147429 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModelSchemaV2.test.ts.snap @@ -35,10 +35,6 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model "kind": "AnnotationQuery", "spec": { "builtIn": false, - "datasource": { - "type": "loki", - "uid": "Loki", - }, "enable": true, "hide": true, "iconColor": "green", diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts index 7417d108d63..727964416e3 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.test.ts @@ -20,6 +20,7 @@ import { SceneDataQuery, SceneQueryRunner, sceneUtils, + dataLayers, } from '@grafana/scenes'; import { DashboardCursorSync as DashboardCursorSyncV1, @@ -57,6 +58,7 @@ import { transformSceneToSaveModelSchemaV2, validateDashboardSchemaV2, getDataQueryKind, + getAutoAssignedDSRef, } from './transformSceneToSaveModelSchemaV2'; // Mock dependencies @@ -421,8 +423,8 @@ describe('transformSceneToSaveModelSchemaV2', () => { // Check that the annotation layers are correctly transformed expect(result.annotations).toHaveLength(3); - // check annotation layer 3 with no datasource has the default datasource defined as type - expect(result.annotations?.[2].spec.datasource?.type).toBe('loki'); + // Check annotation layer 3 without initial data source isn't updated with runtime default + expect(result.annotations?.[2].spec.datasource?.type).toBe(undefined); }); it('should transform the minimum scene to save model schema v2', () => { @@ -748,6 +750,58 @@ describe('getElementDatasource', () => { expect(result).toBeUndefined(); }); + it('should handle annotation datasources correctly', () => { + // Use the dataLayers.AnnotationsDataLayer directly + const annotationLayer = new dataLayers.AnnotationsDataLayer({ + key: 'annotation-1', + name: 'Test Annotation', + isEnabled: true, + isHidden: false, + query: { + name: 'Test Annotation', + enable: true, + hide: false, + iconColor: 'red', + datasource: { uid: 'prometheus', type: 'prometheus' }, + }, + }); + + // Create an annotation query without datasource + const annotationWithoutDS = { + name: 'No DS Annotation', + enable: true, + hide: false, + iconColor: 'blue', + }; + + // Mock dsReferencesMapping + const dsReferencesMapping = { + panels: new Map([['panel-1', new Set(['A'])]]), + variables: new Set(), + annotations: new Set(['No DS Annotation']), + }; + + // Test with annotation that has datasource defined + const resultWithDS = getElementDatasource( + annotationLayer, + annotationLayer.state.query, + 'annotation', + undefined, + dsReferencesMapping + ); + expect(resultWithDS).toEqual({ uid: 'prometheus', type: 'prometheus' }); + + // Test with annotation that has no datasource defined + const resultWithoutDS = getElementDatasource( + annotationLayer, + annotationWithoutDS, + 'annotation', + undefined, + dsReferencesMapping + ); + expect(resultWithoutDS).toBeUndefined(); + }); + it('should handle invalid input combinations', () => { const vizPanel = new VizPanel({ key: 'panel-1', @@ -772,6 +826,24 @@ describe('getElementDatasource', () => { // Variable set with query expect(getElementDatasource(variableSet, query, 'variable')).toBeUndefined(); }); + + it('should throw error when invalid type is passed to getAutoAssignedDSRef', () => { + const vizPanel = new VizPanel({ + key: 'panel-1', + pluginId: 'timeseries', + }); + + const dsReferencesMapping = { + panels: new Map([['panel-1', new Set(['A'])]]), + variables: new Set(), + annotations: new Set(), + }; + + expect(() => { + // @ts-expect-error - intentionally passing invalid type to test error handling + getAutoAssignedDSRef(vizPanel, 'invalid-type', dsReferencesMapping); + }).toThrow('Invalid type invalid-type for getAutoAssignedDSRef'); + }); }); function getMinimalSceneState(body: DashboardLayoutManager): Partial { diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts index 435129e10fe..6dfd5a1c157 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModelSchemaV2.ts @@ -12,7 +12,6 @@ import { SceneVariables, SceneVariableSet, VizPanel, - sceneUtils, } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema'; import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object'; @@ -107,7 +106,7 @@ export function transformSceneToSaveModelSchemaV2(scene: DashboardScene, isSnaps // EOF elements // annotations - annotations: getAnnotations(sceneDash), + annotations: getAnnotations(sceneDash, dsReferencesMapping), // EOF annotations // layout @@ -392,7 +391,7 @@ function getVariables(oldDash: DashboardSceneState, dsReferencesMapping?: DSRefe return variables; } -function getAnnotations(state: DashboardSceneState): AnnotationQueryKind[] { +function getAnnotations(state: DashboardSceneState, dsReferencesMapping?: DSReferencesMapping): AnnotationQueryKind[] { const data = state.$data; if (!(data instanceof DashboardDataLayerSet)) { return []; @@ -407,7 +406,7 @@ function getAnnotations(state: DashboardSceneState): AnnotationQueryKind[] { spec: { builtIn: Boolean(layer.state.query.builtIn), name: layer.state.query.name, - datasource: layer.state.query.datasource || getDefaultDataSourceRef(), + datasource: getElementDatasource(layer, layer.state.query, 'annotation', undefined, dsReferencesMapping), enable: Boolean(layer.state.isEnabled), hide: Boolean(layer.state.isHidden), iconColor: layer.state.query.iconColor, @@ -657,9 +656,9 @@ function validateRowsLayout(layout: unknown) { } } -function getAutoAssignedDSRef( - element: VizPanel | SceneVariables, - type: 'panels' | 'variables', +export function getAutoAssignedDSRef( + element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer, + type: 'panels' | 'variables' | 'annotations', elementMapReferences?: DSReferencesMapping ): Set { if (!elementMapReferences) { @@ -670,16 +669,25 @@ function getAutoAssignedDSRef( return elementMapReferences.panels.get(elementKey) || new Set(); } - return elementMapReferences.variables; + if (type === 'variables') { + return elementMapReferences.variables; + } + + if (type === 'annotations') { + return elementMapReferences.annotations; + } + + // if type is not panels, annotations, or variables, throw error + throw new Error(`Invalid type ${type} for getAutoAssignedDSRef`); } /** * Determines if a data source reference should be persisted for a query or variable */ -export function getPersistedDSFor( +export function getPersistedDSFor( element: T, autoAssignedDsRef: Set, - type: 'query' | 'variable', + type: 'query' | 'variable' | 'annotation', context?: SceneQueryRunner ): DataSourceRef | undefined { // Get the element identifier - refId for queries, name for variables @@ -705,6 +713,10 @@ export function getPersistedDSFor( return element.state.datasource || {}; } + if (type === 'annotation' && 'datasource' in element) { + return element.datasource || {}; + } + return undefined; } @@ -713,32 +725,52 @@ export function getPersistedDSFor( * @returns refId for queries, name for variables * TODO: we will add annotations in the future */ -function getElementIdentifier( +function getElementIdentifier( element: T, - type: 'query' | 'variable' + type: 'query' | 'variable' | 'annotation' ): string { // when is type query look for refId if (type === 'query') { return 'refId' in element ? element.refId : ''; } - // when is type variable look for the name of the variable - return 'state' in element && 'name' in element.state ? element.state.name : ''; + + if (type === 'variable') { + // when is type variable look for the name of the variable + return 'state' in element && 'name' in element.state ? element.state.name : ''; + } + + // when is type annotation look for annotation name + if (type === 'annotation') { + return 'name' in element ? element.name : ''; + } + + throw new Error(`Invalid type ${type} for getElementIdentifier`); } -function isVizPanel(element: VizPanel | SceneVariables): element is VizPanel { +function isVizPanel(element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer): element is VizPanel { // FIXME: is there another way to do this? return 'pluginId' in element.state; } -function isSceneVariables(element: VizPanel | SceneVariables): element is SceneVariables { +function isSceneVariables( + element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer +): element is SceneVariables { // Check for properties unique to SceneVariables but not in VizPanel return !('pluginId' in element.state) && ('variables' in element.state || 'getValue' in element); } -function isSceneDataQuery(query: SceneDataQuery | QueryVariable): query is SceneDataQuery { +function isSceneDataQuery(query: SceneDataQuery | QueryVariable | AnnotationQuery): query is SceneDataQuery { return 'refId' in query && !('state' in query); } +function isAnnotationQuery(query: SceneDataQuery | QueryVariable | AnnotationQuery): query is AnnotationQuery { + return 'datasource' in query && 'name' in query; +} + +function isQueryVariable(query: SceneDataQuery | QueryVariable | AnnotationQuery): query is QueryVariable { + return 'state' in query && 'name' in query.state; +} + /** * Get the persisted datasource for a query or variable * When a query or variable is created it could not have a datasource set @@ -747,35 +779,41 @@ function isSceneDataQuery(query: SceneDataQuery | QueryVariable): query is Scene * */ export function getElementDatasource( - element: VizPanel | SceneVariables, - queryElement: SceneDataQuery | QueryVariable, - type: 'panel' | 'variable', + element: VizPanel | SceneVariables | dataLayers.AnnotationsDataLayer, + queryElement: SceneDataQuery | QueryVariable | AnnotationQuery, + type: 'panel' | 'variable' | 'annotation', queryRunner?: SceneQueryRunner, dsReferencesMapping?: DSReferencesMapping ): DataSourceRef | undefined { + let result: DataSourceRef | undefined; if (type === 'panel') { if (!queryRunner || !isVizPanel(element) || !isSceneDataQuery(queryElement)) { return undefined; } // Get datasource for panel query const autoAssignedRefs = getAutoAssignedDSRef(element, 'panels', dsReferencesMapping); - return getPersistedDSFor(queryElement, autoAssignedRefs, 'query', queryRunner); + result = getPersistedDSFor(queryElement, autoAssignedRefs, 'query', queryRunner); } if (type === 'variable') { - if (!isSceneVariables(element) || isSceneDataQuery(queryElement)) { + if (!isSceneVariables(element) || !isQueryVariable(queryElement)) { return undefined; } // Get datasource for variable - if (!sceneUtils.isQueryVariable(queryElement)) { - return undefined; - } const autoAssignedRefs = getAutoAssignedDSRef(element, 'variables', dsReferencesMapping); - // Important: Only return the datasource if it's not in auto-assigned refs - // and if the result would not be an empty object - const result = getPersistedDSFor(queryElement, autoAssignedRefs, 'variable'); - return Object.keys(result || {}).length > 0 ? result : undefined; + + result = getPersistedDSFor(queryElement, autoAssignedRefs, 'variable'); } - return undefined; + if (type === 'annotation') { + if (!isAnnotationQuery(queryElement)) { + return undefined; + } + // Get datasource for annotation + const autoAssignedRefs = getAutoAssignedDSRef(element, 'annotations', dsReferencesMapping); + result = getPersistedDSFor(queryElement, autoAssignedRefs, 'annotation'); + } + // Important: Only return the datasource if it's not in auto-assigned refs + // and if the result would not be an empty object + return Object.keys(result || {}).length > 0 ? result : undefined; } From 0d206806958be88afa59ba9149d8ce8a13c5c649 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 11 Apr 2025 13:06:16 +0300 Subject: [PATCH 29/37] Provisioning: Improve PR feedback (#103537) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix ref * dryRun before save * reuse code for create vs update * update comments * can update PRs * return useful errors * update preview links * lint fix * update some wording * update mocks * fixed * Update pkg/registry/apis/provisioning/resources/dualwriter.go Co-authored-by: Roberto Jiménez Sánchez * more updates * fix link * error string * dry run * refactor galore * with template test * very basic tests * more test cases * remove generator * more tests * fix lint * multiple files now * merge main * merge main * fix snapshots * fix snapshots * slugify title --------- Co-authored-by: Roberto Jiménez Sánchez --- .../apis/provisioning/jobs/migrate/worker.go | 6 +- .../provisioning/jobs/pullrequest/changes.go | 220 ++++++++++++++++++ .../jobs/pullrequest/changes_test.go | 204 ++++++++++++++++ .../provisioning/jobs/pullrequest/comment.go | 124 ++++++++++ .../jobs/pullrequest/comment_test.go | 145 ++++++++++++ .../provisioning/jobs/pullrequest/preview.go | 192 --------------- .../jobs/pullrequest/preview_renderer_mock.go | 142 ----------- .../jobs/pullrequest/previewer_mock.go | 156 ------------- .../provisioning/jobs/pullrequest/render.go | 58 +++-- .../jobs/pullrequest/render_mock.go | 144 ++++++++++++ .../pullrequest/testdata/multiple-files.md | 11 + .../pullrequest/testdata/new-dashboard.md | 8 + .../update-dashboard-missing-renderer.md | 7 + .../pullrequest/testdata/update-dashboard.md | 9 + .../provisioning/jobs/pullrequest/worker.go | 117 +++++----- pkg/registry/apis/provisioning/register.go | 5 +- .../apis/provisioning/repository/local.go | 9 +- .../provisioning/repository/local_test.go | 4 +- .../provisioning/repository/repository.go | 9 +- .../apis/provisioning/resources/dualwriter.go | 111 ++++----- .../apis/provisioning/resources/parser.go | 12 +- .../apis/provisioning/provisioning_test.go | 16 +- .../GettingStarted/GettingStarted.tsx | 1 - 23 files changed, 1052 insertions(+), 658 deletions(-) create mode 100644 pkg/registry/apis/provisioning/jobs/pullrequest/changes.go create mode 100644 pkg/registry/apis/provisioning/jobs/pullrequest/changes_test.go create mode 100644 pkg/registry/apis/provisioning/jobs/pullrequest/comment.go create mode 100644 pkg/registry/apis/provisioning/jobs/pullrequest/comment_test.go delete mode 100644 pkg/registry/apis/provisioning/jobs/pullrequest/preview.go delete mode 100644 pkg/registry/apis/provisioning/jobs/pullrequest/preview_renderer_mock.go delete mode 100644 pkg/registry/apis/provisioning/jobs/pullrequest/previewer_mock.go create mode 100644 pkg/registry/apis/provisioning/jobs/pullrequest/render_mock.go create mode 100755 pkg/registry/apis/provisioning/jobs/pullrequest/testdata/multiple-files.md create mode 100755 pkg/registry/apis/provisioning/jobs/pullrequest/testdata/new-dashboard.md create mode 100755 pkg/registry/apis/provisioning/jobs/pullrequest/testdata/update-dashboard-missing-renderer.md create mode 100755 pkg/registry/apis/provisioning/jobs/pullrequest/testdata/update-dashboard.md diff --git a/pkg/registry/apis/provisioning/jobs/migrate/worker.go b/pkg/registry/apis/provisioning/jobs/migrate/worker.go index b2992e6d261..657b4348731 100644 --- a/pkg/registry/apis/provisioning/jobs/migrate/worker.go +++ b/pkg/registry/apis/provisioning/jobs/migrate/worker.go @@ -142,7 +142,7 @@ func (w *MigrationWorker) migrateFromLegacy(ctx context.Context, rw repository.R } namespace := rw.Config().Namespace - progress.SetMessage(ctx, "loading legacy folders") + progress.SetMessage(ctx, "loading folders from SQL") reader := NewLegacyFolderReader(w.legacyMigrator, rw.Config().Name, namespace) if err = reader.Read(ctx, w.legacyMigrator, rw.Config().Name, namespace); err != nil { return fmt.Errorf("error loading folder tree: %w", err) @@ -154,7 +154,7 @@ func (w *MigrationWorker) migrateFromLegacy(ctx context.Context, rw repository.R } folders := resources.NewFolderManager(rw, folderClient, resources.NewEmptyFolderTree()) - progress.SetMessage(ctx, "exporting legacy folders") + progress.SetMessage(ctx, "exporting folders from SQL") err = folders.EnsureFolderTreeExists(ctx, "", "", reader.Tree(), func(folder resources.Folder, created bool, err error) error { result := jobs.JobResourceResult{ Action: repository.FileActionCreated, @@ -176,7 +176,7 @@ func (w *MigrationWorker) migrateFromLegacy(ctx context.Context, rw repository.R return fmt.Errorf("error exporting legacy folders: %w", err) } - progress.SetMessage(ctx, "exporting legacy resources") + progress.SetMessage(ctx, "exporting resources from SQL") resourceManager := resources.NewResourcesManager(rw, folders, parser, clients, userInfo) for _, kind := range resources.SupportedProvisioningResources { if kind == resources.FolderResource { diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/changes.go b/pkg/registry/apis/provisioning/jobs/pullrequest/changes.go new file mode 100644 index 00000000000..7946f969789 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/changes.go @@ -0,0 +1,220 @@ +package pullrequest + +import ( + "context" + "fmt" + "net/url" + "path" + "strings" + + "github.com/grafana/grafana-app-sdk/logging" + dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/infra/slugify" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" +) + +type changeInfo struct { + GrafanaBaseURL string + + // Files we tried to read + Changes []fileChangeInfo + + // More files changed than we processed + SkippedFiles int + + // Requested image render, but it is not available + MissingImageRenderer bool + HasScreenshot bool +} + +type fileChangeInfo struct { + Change repository.VersionedFileChange + Error string + + // The parsed value + Parsed *resources.ParsedResource + + // The title from inside the resource (or name if not found) + Title string + + // The URL where this will appear (target) + GrafanaURL string + GrafanaScreenshotURL string + + // URL where we can see a preview of this particular change + PreviewURL string + PreviewScreenshotURL string +} + +type changeOptions struct { + grafanaBaseURL string + pullRequest provisioning.PullRequestJobOptions + changes []repository.VersionedFileChange + parser resources.Parser + reader repository.Reader + progress jobs.JobProgressRecorder + render ScreenshotRenderer // from config +} + +// This will process the list of versioned file changes into changeInfo +func processChangedFiles(ctx context.Context, opts changeOptions) (changeInfo, error) { + info := changeInfo{ + GrafanaBaseURL: opts.grafanaBaseURL, + } + + if opts.render != nil { + if !opts.render.IsAvailable(ctx) { + info.MissingImageRenderer = true + opts.render = nil + } + + // Only render images when there is just one change + if len(opts.changes) > 1 { + opts.render = nil + } + } + + logger := logging.FromContext(ctx) + for i, change := range opts.changes { + // process maximum 10 files + if i >= 10 { + info.SkippedFiles = len(opts.changes) - i + break + } + + opts.progress.SetMessage(ctx, fmt.Sprintf("processing: %s", change.Path)) + logger.With("action", change.Action).With("path", change.Path) + + v, err := calculateFileChangeInfo(ctx, info.GrafanaBaseURL, change, opts) + if err != nil { + return info, fmt.Errorf("error calculating changes %w", err) + } + + // If everything applied OK, then render screenshots + if opts.render != nil && v.GrafanaURL != "" && v.Parsed != nil && v.Parsed.DryRunResponse != nil { + opts.progress.SetMessage(ctx, fmt.Sprintf("rendering screenshots: %s", change.Path)) + if err = v.renderScreenshots(ctx, info.GrafanaBaseURL, opts.render); err != nil { + info.MissingImageRenderer = true + if v.Error == "" { + v.Error = "Error running image rendering" + } + + if v.GrafanaScreenshotURL != "" || v.PreviewScreenshotURL != "" { + info.HasScreenshot = true + } + } + } + + info.Changes = append(info.Changes, v) + } + return info, nil +} + +var dashboardKind = dashboard.DashboardResourceInfo.GroupVersionKind().Kind + +func calculateFileChangeInfo(ctx context.Context, baseURL string, change repository.VersionedFileChange, opts changeOptions) (fileChangeInfo, error) { + if change.Action == repository.FileActionDeleted { + return calculateFileDeleteInfo(ctx, baseURL, change, opts) + } + + info := fileChangeInfo{Change: change} + fileInfo, err := opts.reader.Read(ctx, change.Path, change.Ref) + if err != nil { + logger.Info("unable to read file", "err", err) + info.Error = err.Error() + return info, nil + } + + // Read the file as a resource + info.Parsed, err = opts.parser.Parse(ctx, fileInfo) + if err != nil { + info.Error = err.Error() + return info, nil + } + + // Find a name within the file + obj := info.Parsed.Obj + info.Title = info.Parsed.Meta.FindTitle(obj.GetName()) + + // Check what happens when we apply changes + // NOTE: this will also invoke any server side validation + err = info.Parsed.DryRun(ctx) + if err != nil { + info.Error = err.Error() + return info, nil + } + + // Dashboards get special handling + if info.Parsed.GVK.Kind == dashboardKind { + if info.Parsed.Existing != nil { + info.GrafanaURL = fmt.Sprintf("%sd/%s/%s", baseURL, obj.GetName(), + slugify.Slugify(info.Title)) + } + + // Load this file directly + info.PreviewURL = baseURL + path.Join("admin/provisioning", + info.Parsed.Repo.Name, "dashboard/preview", info.Parsed.Info.Path) + + query := url.Values{} + query.Set("ref", info.Parsed.Info.Ref) + if opts.pullRequest.URL != "" { + query.Set("pull_request_url", url.QueryEscape(opts.pullRequest.URL)) + } + info.PreviewURL += "?" + query.Encode() + } + + return info, nil +} + +func calculateFileDeleteInfo(_ context.Context, _ string, change repository.VersionedFileChange, opts changeOptions) (fileChangeInfo, error) { + // TODO -- read the old and verify + return fileChangeInfo{Change: change, Error: "delete feedback not yet implemented"}, nil +} + +// This will update render the linked screenshots and update the screenshotURLs +func (f *fileChangeInfo) renderScreenshots(ctx context.Context, baseURL string, renderer ScreenshotRenderer) (err error) { + if f.GrafanaURL != "" { + f.GrafanaScreenshotURL, err = renderScreenshotFromGrafanaURL(ctx, baseURL, renderer, f.Parsed.Repo, f.GrafanaURL) + if err != nil { + return err + } + } + if f.PreviewURL != "" { + f.PreviewScreenshotURL, err = renderScreenshotFromGrafanaURL(ctx, baseURL, renderer, f.Parsed.Repo, f.PreviewURL) + if err != nil { + return err + } + } + return nil +} + +func renderScreenshotFromGrafanaURL(ctx context.Context, + baseURL string, + renderer ScreenshotRenderer, + repo provisioning.ResourceRepositoryInfo, + grafanaURL string, +) (string, error) { + parsed, err := url.Parse(grafanaURL) + if err != nil { + logging.FromContext(ctx).Warn("invalid", "url", grafanaURL, "err", err) + return "", err + } + snap, err := renderer.RenderScreenshot(ctx, repo, strings.TrimPrefix(parsed.Path, "/"), parsed.Query()) + if err != nil { + logging.FromContext(ctx).Warn("render failed", "url", grafanaURL, "err", err) + return "", fmt.Errorf("error rendering screenshot %w", err) + } + if strings.Contains(snap, "://") { + return snap, nil // it is a full URL already (can happen when the blob storage returns CDN urls) + } + base, err := url.Parse(baseURL) + if err != nil { + logger.Warn("invalid base", "url", baseURL, "err", err) + return "", err + } + return base.JoinPath(snap).String(), nil +} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/changes_test.go b/pkg/registry/apis/provisioning/jobs/pullrequest/changes_test.go new file mode 100644 index 00000000000..9f0e04947f0 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/changes_test.go @@ -0,0 +1,204 @@ +package pullrequest + +import ( + "context" + "crypto/sha256" + "encoding/binary" + "fmt" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" +) + +func TestCalculateChanges(t *testing.T) { + parser := resources.NewMockParser(t) + reader := repository.NewMockReader(t) + progress := jobs.NewMockJobProgressRecorder(t) + + finfo := &repository.FileInfo{ + Path: "path/to/file.json", + Ref: "ref", + Data: []byte("xxxx"), // not a valid JSON! + } + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": resources.DashboardResource.GroupVersion().String(), + "kind": dashboardKind, // will trigger creating a URL + "metadata": map[string]interface{}{ + "name": "the-uid", + }, + "spec": map[string]interface{}{ + "title": "hello world", // has spaces + }, + }, + } + meta, _ := utils.MetaAccessor(obj) + + progress.On("SetMessage", mock.Anything, mock.Anything).Return() + reader.On("Read", mock.Anything, "path/to/file.json", "ref").Return(finfo, nil) + parser.On("Parse", mock.Anything, finfo).Return(&resources.ParsedResource{ + Info: finfo, + Repo: v0alpha1.ResourceRepositoryInfo{ + Namespace: "x", + Name: "y", + }, + GVK: schema.GroupVersionKind{ + Kind: dashboardKind, + }, + Obj: obj, + Existing: obj, + Meta: meta, + DryRunResponse: obj, // avoid hitting the client + }, nil) + + pullRequest := v0alpha1.PullRequestJobOptions{ + Ref: "ref", + PR: 123, + URL: "http://github.com/pr/", + } + createdFileChange := repository.VersionedFileChange{ + Action: repository.FileActionCreated, + Path: "path/to/file.json", + Ref: "ref", + } + + t.Run("with-screenshot", func(t *testing.T) { + renderer := NewMockScreenshotRenderer(t) + renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(true) + renderer.On("RenderScreenshot", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(getDummyRenderedURL("x"), nil) + + options := changeOptions{ + grafanaBaseURL: "http://host/", + pullRequest: pullRequest, + changes: []repository.VersionedFileChange{createdFileChange}, + parser: parser, + reader: reader, + progress: progress, + render: renderer, + } + + info, err := processChangedFiles(context.Background(), options) + require.NoError(t, err) + + require.False(t, info.MissingImageRenderer) + require.Equal(t, map[string]string{ + "Grafana": "http://host/d/the-uid/hello-world", + "GrafanaSnapshot": "https://cdn2.thecatapi.com/images/9e2.jpg", + "Preview": "http://host/admin/provisioning/y/dashboard/preview/path/to/file.json?pull_request_url=http%253A%252F%252Fgithub.com%252Fpr%252F&ref=ref", + "PreviewSnapshot": "https://cdn2.thecatapi.com/images/9e2.jpg", + }, map[string]string{ + "Grafana": info.Changes[0].GrafanaURL, + "GrafanaSnapshot": info.Changes[0].GrafanaScreenshotURL, + "Preview": info.Changes[0].PreviewURL, + "PreviewSnapshot": info.Changes[0].PreviewScreenshotURL, + }) + }) + + t.Run("without-screenshot", func(t *testing.T) { + renderer := NewMockScreenshotRenderer(t) + renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(false) + options := changeOptions{ + grafanaBaseURL: "http://host/", + pullRequest: pullRequest, + changes: []repository.VersionedFileChange{createdFileChange}, + parser: parser, + reader: reader, + progress: progress, + render: renderer, + } + + info, err := processChangedFiles(context.Background(), options) + require.NoError(t, err) + + require.True(t, info.MissingImageRenderer) + require.Equal(t, map[string]string{ + "Grafana": "http://host/d/the-uid/hello-world", + "GrafanaSnapshot": "", + "Preview": "http://host/admin/provisioning/y/dashboard/preview/path/to/file.json?pull_request_url=http%253A%252F%252Fgithub.com%252Fpr%252F&ref=ref", + "PreviewSnapshot": "", + }, map[string]string{ + "Grafana": info.Changes[0].GrafanaURL, + "GrafanaSnapshot": info.Changes[0].GrafanaScreenshotURL, + "Preview": info.Changes[0].PreviewURL, + "PreviewSnapshot": info.Changes[0].PreviewScreenshotURL, + }) + }) + + t.Run("process first 10 files", func(t *testing.T) { + renderer := NewMockScreenshotRenderer(t) + renderer.On("IsAvailable", mock.Anything, mock.Anything).Return(true) + + options := changeOptions{ + grafanaBaseURL: "http://host/", + pullRequest: pullRequest, + parser: parser, + reader: reader, + progress: progress, + render: renderer, // not used + } + for range 15 { + options.changes = append(options.changes, createdFileChange) + } + + info, err := processChangedFiles(context.Background(), options) + require.NoError(t, err) + + require.False(t, info.MissingImageRenderer) + require.Equal(t, 10, len(info.Changes)) + require.Equal(t, 5, info.SkippedFiles) + + // Make sure we linked a URL, but no screenshot for each item + for _, change := range info.Changes { + require.NotEmpty(t, change.GrafanaURL) + require.Empty(t, change.GrafanaScreenshotURL) + } + }) +} + +func TestDummyImageURL(t *testing.T) { + urls := []string{} + for i := range 10 { + urls = append(urls, getDummyRenderedURL(fmt.Sprintf("http://%d", i))) + } + require.Equal(t, []string{ + "https://cdn2.thecatapi.com/images/9e2.jpg", + "https://cdn2.thecatapi.com/images/bhs.jpg", + "https://cdn2.thecatapi.com/images/d54.jpg", + "https://cdn2.thecatapi.com/images/99c.jpg", + "https://cdn2.thecatapi.com/images/9e2.jpg", + "https://cdn2.thecatapi.com/images/bhs.jpg", + "https://cdn2.thecatapi.com/images/d54.jpg", + "https://cdn2.thecatapi.com/images/99c.jpg", + "https://cdn2.thecatapi.com/images/9e2.jpg", + "https://cdn2.thecatapi.com/images/bhs.jpg", + }, urls) +} + +// Returns a random (but stable) image for a string +func getDummyRenderedURL(url string) string { + dummy := []string{ + "https://cdn2.thecatapi.com/images/9e2.jpg", + "https://cdn2.thecatapi.com/images/bhs.jpg", + "https://cdn2.thecatapi.com/images/d54.jpg", + "https://cdn2.thecatapi.com/images/99c.jpg", + } + + idx := 0 + hash := sha256.New() + bytes := hash.Sum([]byte(url)) + if len(bytes) > 8 { + v := binary.BigEndian.Uint64(bytes[0:8]) + idx = int(v) % len(dummy) + } + return dummy[idx] +} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/comment.go b/pkg/registry/apis/provisioning/jobs/pullrequest/comment.go new file mode 100644 index 00000000000..e7c098af776 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/comment.go @@ -0,0 +1,124 @@ +package pullrequest + +import ( + "bytes" + "context" + "fmt" + "html/template" + "path/filepath" + "strings" +) + +type commentBuilder struct { + templateDashboard *template.Template + templateTable *template.Template + templateRenderInfo *template.Template +} + +func newCommentBuilder() *commentBuilder { + return &commentBuilder{ + templateDashboard: template.Must(template.New("dashboard").Parse(commentTemplateSingleDashboard)), + templateTable: template.Must(template.New("table").Parse(commentTemplateTable)), + templateRenderInfo: template.Must(template.New("setup").Parse(commentTemplateMissingImageRenderer)), + } +} + +func (c *commentBuilder) Comment(ctx context.Context, prRepo PullRequestRepo, pr int, info changeInfo) error { + comment, err := c.generateComment(ctx, info) + if err != nil { + return fmt.Errorf("unable to generate comment text: %w", err) + } + + if err := prRepo.CommentPullRequest(ctx, pr, comment); err != nil { + return fmt.Errorf("comment pull request: %w", err) + } + return nil +} + +func (c *commentBuilder) generateComment(_ context.Context, info changeInfo) (string, error) { + if len(info.Changes) == 0 { + return "no changes found", nil + } + + var buf bytes.Buffer + + if len(info.Changes) == 1 && info.Changes[0].Parsed.GVK.Kind == dashboardKind { + if err := c.templateDashboard.Execute(&buf, info.Changes[0]); err != nil { + return "", fmt.Errorf("unable to execute template: %w", err) + } + } else { + if err := c.templateTable.Execute(&buf, info); err != nil { + return "", fmt.Errorf("unable to execute template: %w", err) + } + } + + if info.MissingImageRenderer { + if err := c.templateRenderInfo.Execute(&buf, info); err != nil { + return "", fmt.Errorf("unable to execute template: %w", err) + } + } + + return strings.TrimSpace(buf.String()), nil +} + +const commentTemplateSingleDashboard = `Hey there! 🎉 +Grafana spotted some changes to your dashboard. + +{{- if and .GrafanaScreenshotURL .PreviewScreenshotURL}} +### Side by Side Comparison of {{.Parsed.Info.Path}} +| Before | After | +|----------|---------| +| ![Before]({{.GrafanaScreenshotURL}}) | ![Preview]({{.PreviewScreenshotURL}}) | +{{- else if .GrafanaScreenshotURL}} +### Original of {{.Title}} +![Original]({{.GrafanaScreenshotURL}}) +{{- else if .PreviewScreenshotURL}} +### Preview of {{.Parsed.Info.Path}} +![Preview]({{.PreviewScreenshotURL}}) +{{ end}} + +{{ if and .GrafanaURL .PreviewURL}} +See the [original]({{.GrafanaURL}}) and [preview]({{.PreviewURL}}) of {{.Parsed.Info.Path}}. +{{- else if .GrafanaURL}} +See the [original]({{.GrafanaURL}}) of {{.Title}}. +{{- else if .PreviewURL}} +See the [preview]({{.PreviewURL}}) of {{.Parsed.Info.Path}}. +{{- end}} +` + +const commentTemplateTable = `Hey there! 🎉 +Grafana spotted some changes. + +| Action | Kind | Resource | Preview | +|--------|------|----------|---------| +{{- range .Changes}} +| {{.Parsed.Action}} | {{.Kind}} | {{.ExistingLink}} | {{ if .PreviewURL}}[preview]({{.PreviewURL}}){{ end }} | +{{- end}} + +{{ if .SkippedFiles }} +and {{ .SkippedFiles }} more files. +{{ end}} +` + +// TODO: this should expand and show links to setup docs +const commentTemplateMissingImageRenderer = ` +NOTE: The image renderer is not configured +` + +func (f *fileChangeInfo) Kind() string { + if f.Parsed == nil { + return filepath.Ext(f.Change.Path) + } + v := f.Parsed.GVK.Kind + if v == "" { + return filepath.Ext(f.Parsed.Info.Path) + } + return f.Parsed.GVK.Kind +} + +func (f *fileChangeInfo) ExistingLink() string { + if f.GrafanaURL != "" { + return fmt.Sprintf("[%s](%s)", f.Title, f.GrafanaURL) + } + return f.Title +} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/comment_test.go b/pkg/registry/apis/provisioning/jobs/pullrequest/comment_test.go new file mode 100644 index 00000000000..9d5271ed0a3 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/comment_test.go @@ -0,0 +1,145 @@ +package pullrequest + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" + "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" +) + +func TestGenerateComment(t *testing.T) { + builder := newCommentBuilder() + + for _, tc := range []struct { + Name string + Input changeInfo + }{ + {"new dashboard", changeInfo{ + GrafanaBaseURL: "http://host/", + Changes: []fileChangeInfo{ + { + Parsed: &resources.ParsedResource{ + Info: &repository.FileInfo{ + Path: "file.json", + }, + GVK: schema.GroupVersionKind{Kind: "Dashboard"}, + Action: v0alpha1.ResourceActionCreate, + }, + Title: "New Dashboard", + PreviewURL: "http://grafana/admin/preview", + PreviewScreenshotURL: getDummyRenderedURL("http://grafana/admin/preview"), + }, + }, + }}, + {"update dashboard", changeInfo{ + GrafanaBaseURL: "http://host/", + Changes: []fileChangeInfo{ + { + Parsed: &resources.ParsedResource{ + Info: &repository.FileInfo{ + Path: "file.json", + }, + Action: v0alpha1.ResourceActionUpdate, + GVK: schema.GroupVersionKind{Kind: "Dashboard"}, + }, + Title: "Existing Dashboard", + GrafanaURL: "http://grafana/d/uid", + PreviewURL: "http://grafana/admin/preview", + + GrafanaScreenshotURL: getDummyRenderedURL("http://grafana/d/uid"), + PreviewScreenshotURL: getDummyRenderedURL("http://grafana/admin/preview"), + }, + }, + }}, + {"update dashboard missing renderer", changeInfo{ + GrafanaBaseURL: "http://host/", + Changes: []fileChangeInfo{ + { + Parsed: &resources.ParsedResource{ + Info: &repository.FileInfo{ + Path: "file.json", + }, + Action: v0alpha1.ResourceActionUpdate, + GVK: schema.GroupVersionKind{Kind: "Dashboard"}, + }, + Title: "Existing Dashboard", + GrafanaURL: "http://grafana/d/uid", + PreviewURL: "http://grafana/admin/preview", + }, + }, + MissingImageRenderer: true, + }}, + {"multiple files", changeInfo{ + GrafanaBaseURL: "http://host/", + SkippedFiles: 5, + Changes: []fileChangeInfo{ + { + Parsed: &resources.ParsedResource{ + Info: &repository.FileInfo{ + Path: "aaa.json", + }, + Action: v0alpha1.ResourceActionCreate, + GVK: schema.GroupVersionKind{Kind: "Dashboard"}, + }, + Title: "Dash A", + PreviewURL: "http://grafana/admin/preview", + }, + { + Parsed: &resources.ParsedResource{ + Info: &repository.FileInfo{ + Path: "bbb.json", + }, + Action: v0alpha1.ResourceActionUpdate, + GVK: schema.GroupVersionKind{Kind: "Dashboard"}, + }, + Title: "Dash B", + GrafanaURL: "http://grafana/d/bbb", + PreviewURL: "http://grafana/admin/preview", + }, + { + Parsed: &resources.ParsedResource{ + Info: &repository.FileInfo{ + Path: "bbb.json", + }, + Action: v0alpha1.ResourceActionCreate, + GVK: schema.GroupVersionKind{Kind: "Playlist"}, + }, + Title: "My Playlist", + }, + }, + }}, + } { + t.Run(tc.Name, func(t *testing.T) { + comment, err := builder.generateComment(context.Background(), tc.Input) + require.NoError(t, err) + + fpath := filepath.Join("testdata", strings.ReplaceAll(tc.Name, " ", "-")+".md") + update := false + + // We can ignore the gosec G304 because this is only for tests + // nolint:gosec + expect, err := os.ReadFile(fpath) + if err != nil || len(expect) < 1 { + update = true + t.Error("missing " + fpath) + } else { + if diff := cmp.Diff(string(expect), comment); diff != "" { + t.Errorf("%s: %s", fpath, diff) + update = true + } + } + if update { + _ = os.WriteFile(fpath, []byte(comment), 0777) + } + }) + } +} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/preview.go b/pkg/registry/apis/provisioning/jobs/pullrequest/preview.go deleted file mode 100644 index 7a124f384cc..00000000000 --- a/pkg/registry/apis/provisioning/jobs/pullrequest/preview.go +++ /dev/null @@ -1,192 +0,0 @@ -package pullrequest - -import ( - "bytes" - "context" - "fmt" - "html/template" - "net/url" - "path" - - "github.com/grafana/grafana-app-sdk/logging" - "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" - "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" -) - -// resourcePreview represents a resource that has changed in a pull request. -type resourcePreview struct { - Filename string - Path string - Action string - Kind string - OriginalURL string - OriginalScreenshotURL string - PreviewURL string - PreviewScreenshotURL string -} - -const previewsCommentTemplate = `Hey there! 🎉 -Grafana spotted some changes in your dashboard. - -{{- if and .OriginalScreenshotURL .PreviewScreenshotURL}} -### Side by Side Comparison of {{.Filename}} -| Original | Preview | -|----------|---------| -| ![Original]({{.OriginalScreenshotURL}}) | ![Preview]({{.PreviewScreenshotURL}}) | -{{- else if .OriginalScreenshotURL}} -### Original of {{.Filename}} -![Original]({{.OriginalScreenshotURL}}) -{{- else if .PreviewScreenshotURL}} -### Preview of {{.Filename}} -![Preview]({{.PreviewScreenshotURL}}) -{{ end}} - -{{ if and .OriginalURL .PreviewURL}} -See the [original]({{.OriginalURL}}) and [preview]({{.PreviewURL}}) of {{.Filename}}. -{{- else if .OriginalURL}} -See the [original]({{.OriginalURL}}) of {{.Filename}}. -{{- else if .PreviewURL}} -See the [preview]({{.PreviewURL}}) of {{.Filename}}. -{{- end}}` - -// PreviewRenderer is an interface for rendering a preview of a file -// -//go:generate mockery --name PreviewRenderer --structname MockPreviewRenderer --inpackage --filename preview_renderer_mock.go --with-expecter -type PreviewRenderer interface { - IsAvailable(ctx context.Context) bool - RenderDashboardPreview(ctx context.Context, namespace, repoName, path, ref string) (string, error) -} - -// Previewer is a service for previewing dashboard changes in a pull request -// -//go:generate mockery --name Previewer --structname MockPreviewer --inpackage --filename previewer_mock.go --with-expecter -type Previewer interface { - Preview(ctx context.Context, f repository.VersionedFileChange, namespace, repoName, base, ref, pullRequestURL string, generatePreview bool) (resourcePreview, error) - GenerateComment(preview resourcePreview) (string, error) -} - -type previewer struct { - template *template.Template - urlProvider func(namespace string) string - renderer PreviewRenderer -} - -func NewPreviewer(renderer PreviewRenderer, urlProvider func(namespace string) string) *previewer { - return &previewer{ - template: template.Must(template.New("comment").Parse(previewsCommentTemplate)), - urlProvider: urlProvider, - renderer: renderer, - } -} - -// GenerateComment creates a formatted comment for dashboard previews -func (p *previewer) GenerateComment(preview resourcePreview) (string, error) { - var buf bytes.Buffer - if err := p.template.Execute(&buf, preview); err != nil { - return "", fmt.Errorf("execute previews comment template: %w", err) - } - return buf.String(), nil -} - -// getOriginalURL returns the URL for the original version of the file based on the action -func (p *previewer) getOriginalURL(ctx context.Context, f repository.VersionedFileChange, baseURL *url.URL, repoName, base, pullRequestURL string) string { - switch f.Action { - case repository.FileActionCreated: - return "" // No original URL for new files - case repository.FileActionUpdated: - return p.previewURL(baseURL, repoName, base, f.Path, pullRequestURL) - case repository.FileActionRenamed: - return p.previewURL(baseURL, repoName, base, f.PreviousPath, pullRequestURL) - case repository.FileActionDeleted: - return p.previewURL(baseURL, repoName, base, f.Path, pullRequestURL) - default: - logging.FromContext(ctx).Error("unknown file action for original URL", "action", f.Action) - return "" - } -} - -// getPreviewURL returns the URL for the preview version of the file based on the action -func (p *previewer) getPreviewURL(ctx context.Context, f repository.VersionedFileChange, baseURL *url.URL, repoName, ref, pullRequestURL string) string { - switch f.Action { - case repository.FileActionCreated, repository.FileActionUpdated, repository.FileActionRenamed: - return p.previewURL(baseURL, repoName, ref, f.Path, pullRequestURL) - case repository.FileActionDeleted: - return "" // No preview URL for deleted files - default: - logging.FromContext(ctx).Error("unknown file action for preview URL", "action", f.Action) - return "" - } -} - -// previewURL returns the URL to preview the file in Grafana -func (p *previewer) previewURL(u *url.URL, repoName, ref, filePath, pullRequestURL string) string { - baseURL := *u - baseURL = *baseURL.JoinPath("/admin/provisioning", repoName, "dashboard/preview", filePath) - - query := baseURL.Query() - if ref != "" { - query.Set("ref", ref) - } - if pullRequestURL != "" { - query.Set("pull_request_url", url.QueryEscape(pullRequestURL)) - } - baseURL.RawQuery = query.Encode() - - return baseURL.String() -} - -// Preview creates a preview for a single file change -func (p *previewer) Preview( - ctx context.Context, - f repository.VersionedFileChange, - namespace string, - repoName string, - base string, - ref string, - pullRequestURL string, - generatePreview bool, -) (resourcePreview, error) { - baseURL, err := url.Parse(p.urlProvider(namespace)) - if err != nil { - return resourcePreview{}, fmt.Errorf("error parsing base url: %w", err) - } - - preview := resourcePreview{ - Filename: path.Base(f.Path), - Path: f.Path, - Kind: "dashboard", // TODO: add more kinds - Action: string(f.Action), - OriginalURL: p.getOriginalURL(ctx, f, baseURL, repoName, base, pullRequestURL), - PreviewURL: p.getPreviewURL(ctx, f, baseURL, repoName, ref, pullRequestURL), - } - - if !generatePreview { - logger.Info("skipping dashboard preview generation", "path", f.Path) - return preview, nil - } - - if preview.PreviewURL != "" { - screenshotURL, err := p.renderer.RenderDashboardPreview(ctx, namespace, repoName, f.Path, ref) - if err != nil { - return resourcePreview{}, fmt.Errorf("render dashboard preview: %w", err) - } - preview.PreviewScreenshotURL = screenshotURL - logger.Info("dashboard preview screenshot generated", "screenshotURL", screenshotURL) - } - - if preview.OriginalURL != "" { - originalPath := f.PreviousPath - if originalPath == "" { - originalPath = f.Path - } - - screenshotURL, err := p.renderer.RenderDashboardPreview(ctx, namespace, repoName, originalPath, base) - if err != nil { - return resourcePreview{}, fmt.Errorf("render dashboard preview: %w", err) - } - preview.OriginalScreenshotURL = screenshotURL - logger.Info("original dashboard screenshot generated", "screenshotURL", screenshotURL) - } - - return preview, nil -} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/preview_renderer_mock.go b/pkg/registry/apis/provisioning/jobs/pullrequest/preview_renderer_mock.go deleted file mode 100644 index 529430cc72c..00000000000 --- a/pkg/registry/apis/provisioning/jobs/pullrequest/preview_renderer_mock.go +++ /dev/null @@ -1,142 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package pullrequest - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// MockPreviewRenderer is an autogenerated mock type for the PreviewRenderer type -type MockPreviewRenderer struct { - mock.Mock -} - -type MockPreviewRenderer_Expecter struct { - mock *mock.Mock -} - -func (_m *MockPreviewRenderer) EXPECT() *MockPreviewRenderer_Expecter { - return &MockPreviewRenderer_Expecter{mock: &_m.Mock} -} - -// IsAvailable provides a mock function with given fields: ctx -func (_m *MockPreviewRenderer) IsAvailable(ctx context.Context) bool { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for IsAvailable") - } - - var r0 bool - if rf, ok := ret.Get(0).(func(context.Context) bool); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// MockPreviewRenderer_IsAvailable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsAvailable' -type MockPreviewRenderer_IsAvailable_Call struct { - *mock.Call -} - -// IsAvailable is a helper method to define mock.On call -// - ctx context.Context -func (_e *MockPreviewRenderer_Expecter) IsAvailable(ctx interface{}) *MockPreviewRenderer_IsAvailable_Call { - return &MockPreviewRenderer_IsAvailable_Call{Call: _e.mock.On("IsAvailable", ctx)} -} - -func (_c *MockPreviewRenderer_IsAvailable_Call) Run(run func(ctx context.Context)) *MockPreviewRenderer_IsAvailable_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockPreviewRenderer_IsAvailable_Call) Return(_a0 bool) *MockPreviewRenderer_IsAvailable_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockPreviewRenderer_IsAvailable_Call) RunAndReturn(run func(context.Context) bool) *MockPreviewRenderer_IsAvailable_Call { - _c.Call.Return(run) - return _c -} - -// RenderDashboardPreview provides a mock function with given fields: ctx, namespace, repoName, path, ref -func (_m *MockPreviewRenderer) RenderDashboardPreview(ctx context.Context, namespace string, repoName string, path string, ref string) (string, error) { - ret := _m.Called(ctx, namespace, repoName, path, ref) - - if len(ret) == 0 { - panic("no return value specified for RenderDashboardPreview") - } - - var r0 string - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) (string, error)); ok { - return rf(ctx, namespace, repoName, path, ref) - } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string) string); ok { - r0 = rf(ctx, namespace, repoName, path, ref) - } else { - r0 = ret.Get(0).(string) - } - - if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string) error); ok { - r1 = rf(ctx, namespace, repoName, path, ref) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockPreviewRenderer_RenderDashboardPreview_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RenderDashboardPreview' -type MockPreviewRenderer_RenderDashboardPreview_Call struct { - *mock.Call -} - -// RenderDashboardPreview is a helper method to define mock.On call -// - ctx context.Context -// - namespace string -// - repoName string -// - path string -// - ref string -func (_e *MockPreviewRenderer_Expecter) RenderDashboardPreview(ctx interface{}, namespace interface{}, repoName interface{}, path interface{}, ref interface{}) *MockPreviewRenderer_RenderDashboardPreview_Call { - return &MockPreviewRenderer_RenderDashboardPreview_Call{Call: _e.mock.On("RenderDashboardPreview", ctx, namespace, repoName, path, ref)} -} - -func (_c *MockPreviewRenderer_RenderDashboardPreview_Call) Run(run func(ctx context.Context, namespace string, repoName string, path string, ref string)) *MockPreviewRenderer_RenderDashboardPreview_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string), args[4].(string)) - }) - return _c -} - -func (_c *MockPreviewRenderer_RenderDashboardPreview_Call) Return(_a0 string, _a1 error) *MockPreviewRenderer_RenderDashboardPreview_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockPreviewRenderer_RenderDashboardPreview_Call) RunAndReturn(run func(context.Context, string, string, string, string) (string, error)) *MockPreviewRenderer_RenderDashboardPreview_Call { - _c.Call.Return(run) - return _c -} - -// NewMockPreviewRenderer creates a new instance of MockPreviewRenderer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockPreviewRenderer(t interface { - mock.TestingT - Cleanup(func()) -}) *MockPreviewRenderer { - mock := &MockPreviewRenderer{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/previewer_mock.go b/pkg/registry/apis/provisioning/jobs/pullrequest/previewer_mock.go deleted file mode 100644 index e8e8aedcdab..00000000000 --- a/pkg/registry/apis/provisioning/jobs/pullrequest/previewer_mock.go +++ /dev/null @@ -1,156 +0,0 @@ -// Code generated by mockery v2.52.4. DO NOT EDIT. - -package pullrequest - -import ( - context "context" - - repository "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" - mock "github.com/stretchr/testify/mock" -) - -// MockPreviewer is an autogenerated mock type for the Previewer type -type MockPreviewer struct { - mock.Mock -} - -type MockPreviewer_Expecter struct { - mock *mock.Mock -} - -func (_m *MockPreviewer) EXPECT() *MockPreviewer_Expecter { - return &MockPreviewer_Expecter{mock: &_m.Mock} -} - -// GenerateComment provides a mock function with given fields: preview -func (_m *MockPreviewer) GenerateComment(preview resourcePreview) (string, error) { - ret := _m.Called(preview) - - if len(ret) == 0 { - panic("no return value specified for GenerateComment") - } - - var r0 string - var r1 error - if rf, ok := ret.Get(0).(func(resourcePreview) (string, error)); ok { - return rf(preview) - } - if rf, ok := ret.Get(0).(func(resourcePreview) string); ok { - r0 = rf(preview) - } else { - r0 = ret.Get(0).(string) - } - - if rf, ok := ret.Get(1).(func(resourcePreview) error); ok { - r1 = rf(preview) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockPreviewer_GenerateComment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateComment' -type MockPreviewer_GenerateComment_Call struct { - *mock.Call -} - -// GenerateComment is a helper method to define mock.On call -// - preview resourcePreview -func (_e *MockPreviewer_Expecter) GenerateComment(preview interface{}) *MockPreviewer_GenerateComment_Call { - return &MockPreviewer_GenerateComment_Call{Call: _e.mock.On("GenerateComment", preview)} -} - -func (_c *MockPreviewer_GenerateComment_Call) Run(run func(preview resourcePreview)) *MockPreviewer_GenerateComment_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(resourcePreview)) - }) - return _c -} - -func (_c *MockPreviewer_GenerateComment_Call) Return(_a0 string, _a1 error) *MockPreviewer_GenerateComment_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockPreviewer_GenerateComment_Call) RunAndReturn(run func(resourcePreview) (string, error)) *MockPreviewer_GenerateComment_Call { - _c.Call.Return(run) - return _c -} - -// Preview provides a mock function with given fields: ctx, f, namespace, repoName, base, ref, pullRequestURL, generatePreview -func (_m *MockPreviewer) Preview(ctx context.Context, f repository.VersionedFileChange, namespace string, repoName string, base string, ref string, pullRequestURL string, generatePreview bool) (resourcePreview, error) { - ret := _m.Called(ctx, f, namespace, repoName, base, ref, pullRequestURL, generatePreview) - - if len(ret) == 0 { - panic("no return value specified for Preview") - } - - var r0 resourcePreview - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, repository.VersionedFileChange, string, string, string, string, string, bool) (resourcePreview, error)); ok { - return rf(ctx, f, namespace, repoName, base, ref, pullRequestURL, generatePreview) - } - if rf, ok := ret.Get(0).(func(context.Context, repository.VersionedFileChange, string, string, string, string, string, bool) resourcePreview); ok { - r0 = rf(ctx, f, namespace, repoName, base, ref, pullRequestURL, generatePreview) - } else { - r0 = ret.Get(0).(resourcePreview) - } - - if rf, ok := ret.Get(1).(func(context.Context, repository.VersionedFileChange, string, string, string, string, string, bool) error); ok { - r1 = rf(ctx, f, namespace, repoName, base, ref, pullRequestURL, generatePreview) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// MockPreviewer_Preview_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Preview' -type MockPreviewer_Preview_Call struct { - *mock.Call -} - -// Preview is a helper method to define mock.On call -// - ctx context.Context -// - f repository.VersionedFileChange -// - namespace string -// - repoName string -// - base string -// - ref string -// - pullRequestURL string -// - generatePreview bool -func (_e *MockPreviewer_Expecter) Preview(ctx interface{}, f interface{}, namespace interface{}, repoName interface{}, base interface{}, ref interface{}, pullRequestURL interface{}, generatePreview interface{}) *MockPreviewer_Preview_Call { - return &MockPreviewer_Preview_Call{Call: _e.mock.On("Preview", ctx, f, namespace, repoName, base, ref, pullRequestURL, generatePreview)} -} - -func (_c *MockPreviewer_Preview_Call) Run(run func(ctx context.Context, f repository.VersionedFileChange, namespace string, repoName string, base string, ref string, pullRequestURL string, generatePreview bool)) *MockPreviewer_Preview_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(repository.VersionedFileChange), args[2].(string), args[3].(string), args[4].(string), args[5].(string), args[6].(string), args[7].(bool)) - }) - return _c -} - -func (_c *MockPreviewer_Preview_Call) Return(_a0 resourcePreview, _a1 error) *MockPreviewer_Preview_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockPreviewer_Preview_Call) RunAndReturn(run func(context.Context, repository.VersionedFileChange, string, string, string, string, string, bool) (resourcePreview, error)) *MockPreviewer_Preview_Call { - _c.Call.Return(run) - return _c -} - -// NewMockPreviewer creates a new instance of MockPreviewer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockPreviewer(t interface { - mock.TestingT - Cleanup(func()) -}) *MockPreviewer { - mock := &MockPreviewer{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/render.go b/pkg/registry/apis/provisioning/jobs/pullrequest/render.go index cf4a6b43275..644a8660973 100644 --- a/pkg/registry/apis/provisioning/jobs/pullrequest/render.go +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/render.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "mime" + "net/url" "os" "path/filepath" "strings" @@ -16,34 +17,45 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/resource" ) -type screenshotRenderer struct { - render rendering.Service - blobstore resource.BlobStoreClient - urlProvider func(namespace string) string - isPublic bool +// ScreenshotRenderer is an interface for rendering a preview of a file +// +//go:generate mockery --name ScreenshotRenderer --structname MockScreenshotRenderer --inpackage --filename render_mock.go --with-expecter +type ScreenshotRenderer interface { + IsAvailable(ctx context.Context) bool + RenderScreenshot(ctx context.Context, repo provisioning.ResourceRepositoryInfo, path string, values url.Values) (string, error) } -func NewScreenshotRenderer(render rendering.Service, blobstore resource.BlobStoreClient, isPublic bool, urlProvider func(namespace string) string) *screenshotRenderer { +type screenshotRenderer struct { + render rendering.Service + blobstore resource.BlobStoreClient +} + +func NewScreenshotRenderer(render rendering.Service, blobstore resource.BlobStoreClient) ScreenshotRenderer { return &screenshotRenderer{ - render: render, - blobstore: blobstore, - urlProvider: urlProvider, - isPublic: isPublic, + render: render, + blobstore: blobstore, } } func (r *screenshotRenderer) IsAvailable(ctx context.Context) bool { - return r.render != nil && r.render.IsAvailable(ctx) && r.blobstore != nil && r.isPublic + return r.render != nil && r.render.IsAvailable(ctx) && r.blobstore != nil } -func (r *screenshotRenderer) RenderDashboardPreview(ctx context.Context, namespace, repoName, path, ref string) (string, error) { - url := fmt.Sprintf("admin/provisioning/%s/dashboard/preview/%s?kiosk&ref=%s", repoName, path, ref) - - // TODO: why were we using a different context? - // renderContext := identity.WithRequester(context.Background(), r.id) +func (r *screenshotRenderer) RenderScreenshot(ctx context.Context, repo provisioning.ResourceRepositoryInfo, path string, values url.Values) (string, error) { + if strings.Contains(path, "://") { + return "", fmt.Errorf("path should be relative to the system root url") + } + if strings.HasPrefix(path, "/") { + return "", fmt.Errorf("path should not start with slash") + } + if len(values) > 0 { + path = path + "?" + values.Encode() + "&kiosk" + } else { + path = path + "?kiosk" + } result, err := r.render.Render(ctx, rendering.RenderPNG, rendering.Opts{ CommonOpts: rendering.CommonOpts{ - Path: url, + Path: path, AuthOpts: rendering.AuthOpts{ OrgID: 1, // TODO!!!, use the worker identity UserID: 1, @@ -69,10 +81,10 @@ func (r *screenshotRenderer) RenderDashboardPreview(ctx context.Context, namespa rsp, err := r.blobstore.PutBlob(ctx, &resource.PutBlobRequest{ Resource: &resource.ResourceKey{ - Namespace: namespace, + Namespace: repo.Namespace, Group: provisioning.GROUP, Resource: provisioning.RepositoryResourceInfo.GroupResource().Resource, - Name: repoName, + Name: repo.Name, }, Method: resource.PutBlobRequest_GRPC, ContentType: mime.TypeByExtension(ext), // image/png @@ -84,10 +96,6 @@ func (r *screenshotRenderer) RenderDashboardPreview(ctx context.Context, namespa if rsp.Url != "" { return rsp.Url, nil } - base := r.urlProvider(namespace) - if !strings.HasSuffix(base, "/") { - base += "/" - } - return fmt.Sprintf("%sapis/%s/namespaces/%s/repositories/%s/render/%s", - base, provisioning.APIVERSION, namespace, repoName, rsp.Uid), nil + return fmt.Sprintf("apis/%s/namespaces/%s/repositories/%s/render/%s", + provisioning.APIVERSION, repo.Namespace, repo.Name, rsp.Uid), nil } diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/render_mock.go b/pkg/registry/apis/provisioning/jobs/pullrequest/render_mock.go new file mode 100644 index 00000000000..0bb99e8a740 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/render_mock.go @@ -0,0 +1,144 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package pullrequest + +import ( + context "context" + url "net/url" + + mock "github.com/stretchr/testify/mock" + + v0alpha1 "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" +) + +// MockScreenshotRenderer is an autogenerated mock type for the ScreenshotRenderer type +type MockScreenshotRenderer struct { + mock.Mock +} + +type MockScreenshotRenderer_Expecter struct { + mock *mock.Mock +} + +func (_m *MockScreenshotRenderer) EXPECT() *MockScreenshotRenderer_Expecter { + return &MockScreenshotRenderer_Expecter{mock: &_m.Mock} +} + +// IsAvailable provides a mock function with given fields: ctx +func (_m *MockScreenshotRenderer) IsAvailable(ctx context.Context) bool { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for IsAvailable") + } + + var r0 bool + if rf, ok := ret.Get(0).(func(context.Context) bool); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// MockScreenshotRenderer_IsAvailable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsAvailable' +type MockScreenshotRenderer_IsAvailable_Call struct { + *mock.Call +} + +// IsAvailable is a helper method to define mock.On call +// - ctx context.Context +func (_e *MockScreenshotRenderer_Expecter) IsAvailable(ctx interface{}) *MockScreenshotRenderer_IsAvailable_Call { + return &MockScreenshotRenderer_IsAvailable_Call{Call: _e.mock.On("IsAvailable", ctx)} +} + +func (_c *MockScreenshotRenderer_IsAvailable_Call) Run(run func(ctx context.Context)) *MockScreenshotRenderer_IsAvailable_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockScreenshotRenderer_IsAvailable_Call) Return(_a0 bool) *MockScreenshotRenderer_IsAvailable_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockScreenshotRenderer_IsAvailable_Call) RunAndReturn(run func(context.Context) bool) *MockScreenshotRenderer_IsAvailable_Call { + _c.Call.Return(run) + return _c +} + +// RenderScreenshot provides a mock function with given fields: ctx, repo, path, values +func (_m *MockScreenshotRenderer) RenderScreenshot(ctx context.Context, repo v0alpha1.ResourceRepositoryInfo, path string, values url.Values) (string, error) { + ret := _m.Called(ctx, repo, path, values) + + if len(ret) == 0 { + panic("no return value specified for RenderScreenshot") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, v0alpha1.ResourceRepositoryInfo, string, url.Values) (string, error)); ok { + return rf(ctx, repo, path, values) + } + if rf, ok := ret.Get(0).(func(context.Context, v0alpha1.ResourceRepositoryInfo, string, url.Values) string); ok { + r0 = rf(ctx, repo, path, values) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, v0alpha1.ResourceRepositoryInfo, string, url.Values) error); ok { + r1 = rf(ctx, repo, path, values) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockScreenshotRenderer_RenderScreenshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RenderScreenshot' +type MockScreenshotRenderer_RenderScreenshot_Call struct { + *mock.Call +} + +// RenderScreenshot is a helper method to define mock.On call +// - ctx context.Context +// - repo v0alpha1.ResourceRepositoryInfo +// - path string +// - values url.Values +func (_e *MockScreenshotRenderer_Expecter) RenderScreenshot(ctx interface{}, repo interface{}, path interface{}, values interface{}) *MockScreenshotRenderer_RenderScreenshot_Call { + return &MockScreenshotRenderer_RenderScreenshot_Call{Call: _e.mock.On("RenderScreenshot", ctx, repo, path, values)} +} + +func (_c *MockScreenshotRenderer_RenderScreenshot_Call) Run(run func(ctx context.Context, repo v0alpha1.ResourceRepositoryInfo, path string, values url.Values)) *MockScreenshotRenderer_RenderScreenshot_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(v0alpha1.ResourceRepositoryInfo), args[2].(string), args[3].(url.Values)) + }) + return _c +} + +func (_c *MockScreenshotRenderer_RenderScreenshot_Call) Return(_a0 string, _a1 error) *MockScreenshotRenderer_RenderScreenshot_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockScreenshotRenderer_RenderScreenshot_Call) RunAndReturn(run func(context.Context, v0alpha1.ResourceRepositoryInfo, string, url.Values) (string, error)) *MockScreenshotRenderer_RenderScreenshot_Call { + _c.Call.Return(run) + return _c +} + +// NewMockScreenshotRenderer creates a new instance of MockScreenshotRenderer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockScreenshotRenderer(t interface { + mock.TestingT + Cleanup(func()) +}) *MockScreenshotRenderer { + mock := &MockScreenshotRenderer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/testdata/multiple-files.md b/pkg/registry/apis/provisioning/jobs/pullrequest/testdata/multiple-files.md new file mode 100755 index 00000000000..b7ce21ef16b --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/testdata/multiple-files.md @@ -0,0 +1,11 @@ +Hey there! 🎉 +Grafana spotted some changes. + +| Action | Kind | Resource | Preview | +|--------|------|----------|---------| +| create | Dashboard | Dash A | [preview](http://grafana/admin/preview) | +| update | Dashboard | [Dash B](http://grafana/d/bbb) | [preview](http://grafana/admin/preview) | +| create | Playlist | My Playlist | | + + +and 5 more files. \ No newline at end of file diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/testdata/new-dashboard.md b/pkg/registry/apis/provisioning/jobs/pullrequest/testdata/new-dashboard.md new file mode 100755 index 00000000000..deedc18c04d --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/testdata/new-dashboard.md @@ -0,0 +1,8 @@ +Hey there! 🎉 +Grafana spotted some changes to your dashboard. +### Preview of file.json +![Preview](https://cdn2.thecatapi.com/images/99c.jpg) + + + +See the [preview](http://grafana/admin/preview) of file.json. \ No newline at end of file diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/testdata/update-dashboard-missing-renderer.md b/pkg/registry/apis/provisioning/jobs/pullrequest/testdata/update-dashboard-missing-renderer.md new file mode 100755 index 00000000000..79d31c86cee --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/testdata/update-dashboard-missing-renderer.md @@ -0,0 +1,7 @@ +Hey there! 🎉 +Grafana spotted some changes to your dashboard. + + +See the [original](http://grafana/d/uid) and [preview](http://grafana/admin/preview) of file.json. + +NOTE: The image renderer is not configured \ No newline at end of file diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/testdata/update-dashboard.md b/pkg/registry/apis/provisioning/jobs/pullrequest/testdata/update-dashboard.md new file mode 100755 index 00000000000..2597017e685 --- /dev/null +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/testdata/update-dashboard.md @@ -0,0 +1,9 @@ +Hey there! 🎉 +Grafana spotted some changes to your dashboard. +### Side by Side Comparison of file.json +| Before | After | +|----------|---------| +| ![Before](https://cdn2.thecatapi.com/images/99c.jpg) | ![Preview](https://cdn2.thecatapi.com/images/99c.jpg) | + + +See the [original](http://grafana/d/uid) and [preview](http://grafana/admin/preview) of file.json. \ No newline at end of file diff --git a/pkg/registry/apis/provisioning/jobs/pullrequest/worker.go b/pkg/registry/apis/provisioning/jobs/pullrequest/worker.go index 485234e2598..0fea17d09b6 100644 --- a/pkg/registry/apis/provisioning/jobs/pullrequest/worker.go +++ b/pkg/registry/apis/provisioning/jobs/pullrequest/worker.go @@ -24,17 +24,22 @@ type PullRequestRepo interface { } type PullRequestWorker struct { - parsers resources.ParserFactory - previewer Previewer + parsers resources.ParserFactory + renderer ScreenshotRenderer + urlProvider func(namespace string) string + commenter *commentBuilder } func NewPullRequestWorker( parsers resources.ParserFactory, - previewer Previewer, + renderer ScreenshotRenderer, + urlProvider func(namespace string) string, ) *PullRequestWorker { return &PullRequestWorker{ - parsers: parsers, - previewer: previewer, + parsers: parsers, + renderer: renderer, + urlProvider: urlProvider, + commenter: newCommentBuilder(), } } @@ -42,7 +47,6 @@ func (c *PullRequestWorker) IsSupported(ctx context.Context, job provisioning.Jo return job.Spec.Action == provisioning.JobActionPullRequest } -//nolint:gocyclo func (c *PullRequestWorker) Process(ctx context.Context, repo repository.Repository, job provisioning.Job, @@ -54,6 +58,14 @@ func (c *PullRequestWorker) Process(ctx context.Context, return apierrors.NewBadRequest("missing spec.pr") } + if options.Ref == "" { + return apierrors.NewBadRequest("missing spec.ref") + } + + if cfg.GitHub == nil { + return apierrors.NewBadRequest("expecting github configuration") + } + prRepo, ok := repo.(PullRequestRepo) if !ok { return fmt.Errorf("repository is not a github repository") @@ -64,82 +76,71 @@ func (c *PullRequestWorker) Process(ctx context.Context, return errors.New("pull request job submitted targeting repository that is not a Reader") } - parser, err := c.parsers.GetParser(ctx, reader) - if err != nil { - return fmt.Errorf("failed to get parser for %s: %w", repo.Config().Name, err) - } - logger := logging.FromContext(ctx).With("pr", options.PR) logger.Info("process pull request") defer logger.Info("pull request processed") progress.SetMessage(ctx, "listing pull request files") base := cfg.GitHub.Branch - ref := options.Hash - files, err := prRepo.CompareFiles(ctx, base, ref) + files, err := prRepo.CompareFiles(ctx, base, options.Ref) if err != nil { return fmt.Errorf("failed to list pull request files: %s", err.Error()) } - progress.SetMessage(ctx, "clearing pull request comments") - if err := prRepo.ClearAllPullRequestFileComments(ctx, options.PR); err != nil { - return fmt.Errorf("failed to clear pull request comments: %+v", err) - } + files = onlySupportedFiles(files) if len(files) == 0 { progress.SetFinalMessage(ctx, "no files to process") return nil } - if len(files) > 1 { - progress.SetFinalMessage(ctx, "too many files to preview") - return nil - } - - f := files[0] - progress.SetMessage(ctx, "processing file preview") - - if err := resources.IsPathSupported(f.Path); err != nil { - progress.SetFinalMessage(ctx, "file path is not supported") - return nil - } - - fileInfo, err := prRepo.Read(ctx, f.Path, ref) + parser, err := c.parsers.GetParser(ctx, reader) if err != nil { - return fmt.Errorf("read file: %w", err) + return fmt.Errorf("failed to get parser for %s: %w", repo.Config().Name, err) } - _, err = parser.Parse(ctx, fileInfo) + var render ScreenshotRenderer + if cfg.GitHub.GenerateDashboardPreviews { + render = c.renderer + } + + changeInfo, err := processChangedFiles(ctx, changeOptions{ + grafanaBaseURL: c.urlProvider(repo.Config().Namespace), + pullRequest: *options, + changes: files, + parser: parser, + reader: reader, + progress: progress, + render: render, + }) if err != nil { - if errors.Is(err, resources.ErrUnableToReadResourceBytes) { - progress.SetFinalMessage(ctx, "file changes is not valid resource") - return nil - } else { - return fmt.Errorf("parse resource: %w", err) - } + return fmt.Errorf("unable to calculate changes: %w", err) } - // Preview should be the branch name if provided, otherwise use the commit hash - previewRef := options.Ref - if previewRef == "" { - previewRef = ref - } - - preview, err := c.previewer.Preview(ctx, f, job.Namespace, repo.Config().Name, cfg.GitHub.Branch, previewRef, options.URL, cfg.GitHub.GenerateDashboardPreviews) - if err != nil { - return fmt.Errorf("generate preview: %w", err) - } - - progress.SetMessage(ctx, "generating previews comment") - comment, err := c.previewer.GenerateComment(preview) - if err != nil { - return fmt.Errorf("generate comment: %w", err) - } - - if err := prRepo.CommentPullRequest(ctx, options.PR, comment); err != nil { + if err := c.commenter.Comment(ctx, prRepo, options.PR, changeInfo); err != nil { return fmt.Errorf("comment pull request: %w", err) } logger.Info("preview comment added") - return nil } + +// Remove files we should not try to process +func onlySupportedFiles(files []repository.VersionedFileChange) (ret []repository.VersionedFileChange) { + for _, file := range files { + if file.Action == repository.FileActionIgnored { + continue + } + + if err := resources.IsPathSupported(file.Path); err == nil { + ret = append(ret, file) + continue + } + if file.PreviousPath != "" { + if err := resources.IsPathSupported(file.PreviousPath); err != nil { + ret = append(ret, file) + continue + } + } + } + return +} diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index 26e6e617a67..9807476cbb4 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -558,9 +558,8 @@ func (b *APIBuilder) GetPostStartHooks() (map[string]genericapiserver.PostStartH ) // Pull request worker - renderer := pullrequest.NewScreenshotRenderer(b.render, b.unified, b.isPublic, b.urlProvider) - previewer := pullrequest.NewPreviewer(renderer, b.urlProvider) - pullRequestWorker := pullrequest.NewPullRequestWorker(b.parsers, previewer) + renderer := pullrequest.NewScreenshotRenderer(b.render, b.unified) + pullRequestWorker := pullrequest.NewPullRequestWorker(b.parsers, renderer, b.urlProvider) driver := jobs.NewJobDriver(time.Second*28, time.Second*30, time.Second*30, b.jobs, b, b.jobHistory, exportWorker, syncWorker, migrationWorker, pullRequestWorker) diff --git a/pkg/registry/apis/provisioning/repository/local.go b/pkg/registry/apis/provisioning/repository/local.go index c68da67d80f..d28663e4680 100644 --- a/pkg/registry/apis/provisioning/repository/local.go +++ b/pkg/registry/apis/provisioning/repository/local.go @@ -19,6 +19,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" @@ -312,18 +313,18 @@ func (r *localRepository) calculateFileHash(path string) (string, int64, error) return hex.EncodeToString(hasher.Sum(nil)), size, nil } -func (r *localRepository) Create(ctx context.Context, fpath string, ref string, data []byte, comment string) error { +func (r *localRepository) Create(ctx context.Context, filepath string, ref string, data []byte, comment string) error { if err := r.validateRequest(ref); err != nil { return err } - fpath = safepath.Join(r.path, fpath) + fpath := safepath.Join(r.path, filepath) _, err := os.Stat(fpath) if !errors.Is(err, os.ErrNotExist) { if err != nil { return apierrors.NewInternalError(fmt.Errorf("failed to check if file exists: %w", err)) } - return apierrors.NewAlreadyExists(provisioning.RepositoryResourceInfo.GroupResource(), fpath) + return apierrors.NewAlreadyExists(schema.GroupResource{}, filepath) } if safepath.IsDir(fpath) { @@ -356,7 +357,7 @@ func (r *localRepository) Update(ctx context.Context, path string, ref string, d } if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("file does not exist") + return ErrFileNotFound } return os.WriteFile(path, data, 0600) } diff --git a/pkg/registry/apis/provisioning/repository/local_test.go b/pkg/registry/apis/provisioning/repository/local_test.go index c2825ff4f84..4fc6c6545a1 100644 --- a/pkg/registry/apis/provisioning/repository/local_test.go +++ b/pkg/registry/apis/provisioning/repository/local_test.go @@ -2,11 +2,11 @@ package repository import ( "context" - "io/fs" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + apierrors "k8s.io/apimachinery/pkg/api/errors" "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" ) @@ -69,7 +69,7 @@ func TestLocalResolver(t *testing.T) { // read unknown file _, err = r.Read(context.Background(), "testdata/missing", "") - require.ErrorIs(t, err, fs.ErrNotExist) + require.True(t, apierrors.IsNotFound(err)) // 404 error _, err = r.Read(context.Background(), "testdata/webhook-push-nested.json/", "") require.Error(t, err) // not a directory diff --git a/pkg/registry/apis/provisioning/repository/repository.go b/pkg/registry/apis/provisioning/repository/repository.go index db5f6a45b2a..0d15d28a9d8 100644 --- a/pkg/registry/apis/provisioning/repository/repository.go +++ b/pkg/registry/apis/provisioning/repository/repository.go @@ -5,10 +5,10 @@ import ( "errors" "fmt" "io" - "io/fs" "net/http" "time" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" @@ -29,7 +29,12 @@ type Repository interface { } // ErrFileNotFound indicates that a path could not be found in the repository. -var ErrFileNotFound error = fs.ErrNotExist +var ErrFileNotFound error = &apierrors.StatusError{ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + Message: "file not found", +}} type FileInfo struct { // Path to the file on disk. diff --git a/pkg/registry/apis/provisioning/resources/dualwriter.go b/pkg/registry/apis/provisioning/resources/dualwriter.go index ea819149165..b8313aa99d7 100644 --- a/pkg/registry/apis/provisioning/resources/dualwriter.go +++ b/pkg/registry/apis/provisioning/resources/dualwriter.go @@ -170,60 +170,16 @@ func (r *DualReadWriter) CreateFolder(ctx context.Context, path string, ref stri // CreateResource creates a new resource in the repository func (r *DualReadWriter) CreateResource(ctx context.Context, path string, ref string, message string, data []byte) (*ParsedResource, error) { - if err := repository.IsWriteAllowed(r.repo.Config(), ref); err != nil { - return nil, err - } - - info := &repository.FileInfo{ - Data: data, - Path: path, - Ref: ref, - } - - parsed, err := r.parser.Parse(ctx, info) - if err != nil { - return nil, fmt.Errorf("parse file: %w", err) - } - - if err = r.authorize(ctx, parsed, utils.VerbCreate); err != nil { - return nil, err - } - - data, err = parsed.ToSaveBytes() - if err != nil { - return nil, err - } - - if err := r.repo.Create(ctx, path, ref, data, message); err != nil { - return nil, fmt.Errorf("create resource in repository: %w", err) - } - - // Directly update the grafana database - // Behaves the same running sync after writing - // FIXME: to make sure if behaves in the same way as in sync, we should - // we should refactor the code to use the same function. - if ref == "" { - if _, err := r.folders.EnsureFolderPathExist(ctx, path); err != nil { - return nil, fmt.Errorf("ensure folder path exists: %w", err) - } - - if err := parsed.Run(ctx); err != nil { - return nil, fmt.Errorf("run resource: %w", err) - } - } else { - if err := parsed.DryRun(ctx); err != nil { - logger := logging.FromContext(ctx).With("path", path, "name", parsed.Obj.GetName(), "ref", ref) - logger.Warn("failed to dry run resource on create", "error", err) - // Do not fail here as it's purely informational - parsed.Errors = append(parsed.Errors, err.Error()) - } - } - - return parsed, nil + return r.createOrUpdate(ctx, true, path, ref, message, data) } // UpdateResource updates a resource in the repository func (r *DualReadWriter) UpdateResource(ctx context.Context, path string, ref string, message string, data []byte) (*ParsedResource, error) { + return r.createOrUpdate(ctx, false, path, ref, message, data) +} + +// Create or updates a resource in the repository +func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, path string, ref string, message string, data []byte) (*ParsedResource, error) { if err := repository.IsWriteAllowed(r.repo.Config(), ref); err != nil { return nil, err } @@ -234,47 +190,68 @@ func (r *DualReadWriter) UpdateResource(ctx context.Context, path string, ref st Ref: ref, } - // TODO: improve parser to parse out of reader parsed, err := r.parser.Parse(ctx, info) if err != nil { - return nil, fmt.Errorf("parse file: %w", err) + return nil, err } - if err = r.authorize(ctx, parsed, utils.VerbUpdate); err != nil { + // Verify that we can create (or update) the referenced resource + verb := utils.VerbUpdate + if create { + verb = utils.VerbCreate + } + if err = r.authorize(ctx, parsed, verb); err != nil { return nil, err } + // Always use the provisioning identity when writing + ctx, _, err = identity.WithProvisioningIdentity(ctx, parsed.Obj.GetNamespace()) + if err != nil { + return nil, fmt.Errorf("unable to use provisioning identity %w", err) + } + + // Make sure the value is valid + if err := parsed.DryRun(ctx); err != nil { + logger := logging.FromContext(ctx).With("path", path, "name", parsed.Obj.GetName(), "ref", ref) + logger.Warn("failed to dry run resource on create", "error", err) + + // TODO: return this as a 400 rather than 500 + return nil, fmt.Errorf("error running dryRun %w", err) + } + + if len(parsed.Errors) > 0 { + // TODO: return this as a 400 rather than 500 + return nil, fmt.Errorf("errors while parsing file [%v]", parsed.Errors) + } + data, err = parsed.ToSaveBytes() if err != nil { return nil, err } - if err = r.repo.Update(ctx, path, ref, data, message); err != nil { - return nil, fmt.Errorf("update resource in repository: %w", err) + // Create or update + if create { + err = r.repo.Create(ctx, path, ref, data, message) + } else { + err = r.repo.Update(ctx, path, ref, data, message) + } + if err != nil { + return nil, err // raw error is useful } // Directly update the grafana database // Behaves the same running sync after writing // FIXME: to make sure if behaves in the same way as in sync, we should // we should refactor the code to use the same function. - if ref == "" { + if ref == "" && parsed.Client != nil { if _, err := r.folders.EnsureFolderPathExist(ctx, path); err != nil { return nil, fmt.Errorf("ensure folder path exists: %w", err) } - if err := parsed.Run(ctx); err != nil { - return nil, fmt.Errorf("run resource: %w", err) - } - } else { - if err := parsed.DryRun(ctx); err != nil { - // Do not fail here as it's purely informational - logger := logging.FromContext(ctx).With("path", path, "name", parsed.Obj.GetName(), "ref", ref) - logger.Warn("failed to dry run resource on update", "error", err) - parsed.Errors = append(parsed.Errors, err.Error()) - } + err = parsed.Run(ctx) } - return parsed, nil + return parsed, err } func (r *DualReadWriter) authorize(ctx context.Context, parsed *ParsedResource, verb string) error { diff --git a/pkg/registry/apis/provisioning/resources/parser.go b/pkg/registry/apis/provisioning/resources/parser.go index 505ed7ba996..cdca95ea405 100644 --- a/pkg/registry/apis/provisioning/resources/parser.go +++ b/pkg/registry/apis/provisioning/resources/parser.go @@ -212,6 +212,10 @@ func (r *parser) Parse(ctx context.Context, info *repository.FileInfo) (parsed * } func (f *ParsedResource) DryRun(ctx context.Context) error { + if f.DryRunResponse != nil { + return nil // this already ran (and helpful for testing) + } + // FIXME: remove this check once we have better unit tests if f.Client == nil { return fmt.Errorf("no client configured") @@ -252,9 +256,13 @@ func (f *ParsedResource) Run(ctx context.Context) error { return err } - // FIXME: shouldn't we check for the specific error? + // We may have already called DryRun that also calls get + if f.DryRunResponse != nil && f.Action != "" { + // FIXME: shouldn't we check for the specific error? + f.Existing, _ = f.Client.Get(ctx, f.Obj.GetName(), metav1.GetOptions{}) + } + // Run update or create - f.Existing, _ = f.Client.Get(ctx, f.Obj.GetName(), metav1.GetOptions{}) if f.Existing == nil { f.Action = provisioning.ResourceActionCreate f.Upsert, err = f.Client.Create(ctx, f.Obj, metav1.CreateOptions{}) diff --git a/pkg/tests/apis/provisioning/provisioning_test.go b/pkg/tests/apis/provisioning/provisioning_test.go index 2ffaba5ea53..6b531807381 100644 --- a/pkg/tests/apis/provisioning/provisioning_test.go +++ b/pkg/tests/apis/provisioning/provisioning_test.go @@ -270,7 +270,21 @@ func TestIntegrationProvisioning_RunLocalRepository(t *testing.T) { // Write a file -- this will create it *both* in the local file system, and in grafana t.Run("write all panels", func(t *testing.T) { code := 0 - result := helper.AdminREST.Post(). + + // Check that we can not (yet) UPDATE the target path + result := helper.AdminREST.Put(). + Namespace("default"). + Resource("repositories"). + Name(repo). + SubResource("files", targetPath). + Body(helper.LoadFile("testdata/all-panels.json")). + SetHeader("Content-Type", "application/json"). + Do(ctx).StatusCode(&code) + require.Equal(t, http.StatusNotFound, code) + require.True(t, apierrors.IsNotFound(result.Error())) + + // Now try again with POST + result = helper.AdminREST.Post(). Namespace("default"). Resource("repositories"). Name(repo). diff --git a/public/app/features/provisioning/GettingStarted/GettingStarted.tsx b/public/app/features/provisioning/GettingStarted/GettingStarted.tsx index ab4de428af7..d74719dcb5d 100644 --- a/public/app/features/provisioning/GettingStarted/GettingStarted.tsx +++ b/public/app/features/provisioning/GettingStarted/GettingStarted.tsx @@ -17,7 +17,6 @@ const featureIni = `# In your custom.ini file [feature_toggles] provisioning = true -unifiedStorageSearch = true kubernetesClientDashboardsFolders = true kubernetesDashboards = true ; use k8s from browser From 335a55047b2aee183baf3fe7e6f79e1435f33071 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Fri, 11 Apr 2025 12:11:05 +0200 Subject: [PATCH 30/37] Advisor: Move the missing plugin check to its own step (#103861) --- .../pkg/app/checkregistry/checkregistry.go | 1 + .../pkg/app/checks/datasourcecheck/check.go | 83 +++++-- .../app/checks/datasourcecheck/check_test.go | 210 +++++++++++------- 3 files changed, 199 insertions(+), 95 deletions(-) diff --git a/apps/advisor/pkg/app/checkregistry/checkregistry.go b/apps/advisor/pkg/app/checkregistry/checkregistry.go index 31be8ae936e..5b07b827cdd 100644 --- a/apps/advisor/pkg/app/checkregistry/checkregistry.go +++ b/apps/advisor/pkg/app/checkregistry/checkregistry.go @@ -53,6 +53,7 @@ func (s *Service) Checks() []checks.Check { s.pluginStore, s.pluginContextProvider, s.pluginClient, + s.pluginRepo, ), plugincheck.New( s.pluginStore, diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check.go b/apps/advisor/pkg/app/checks/datasourcecheck/check.go index edb37410299..92a325f3211 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/util" @@ -20,6 +21,7 @@ const ( CheckID = "datasource" HealthCheckStepID = "health-check" UIDValidationStepID = "uid-validation" + MissingPluginStepID = "missing-plugin" ) type check struct { @@ -27,6 +29,7 @@ type check struct { PluginStore pluginstore.Store PluginContextProvider pluginContextProvider PluginClient plugins.Client + PluginRepo repo.Service log log.Logger } @@ -35,12 +38,14 @@ func New( pluginStore pluginstore.Store, pluginContextProvider pluginContextProvider, pluginClient plugins.Client, + pluginRepo repo.Service, ) checks.Check { return &check{ DatasourceSvc: datasourceSvc, PluginStore: pluginStore, PluginContextProvider: pluginContextProvider, PluginClient: pluginClient, + PluginRepo: pluginRepo, log: log.New("advisor.datasourcecheck"), } } @@ -69,6 +74,11 @@ func (c *check) Steps() []checks.Step { PluginClient: c.PluginClient, log: c.log, }, + &missingPluginStep{ + PluginStore: c.PluginStore, + PluginRepo: c.PluginRepo, + log: c.log, + }, } } @@ -145,22 +155,8 @@ func (s *healthCheckStep) Run(ctx context.Context, obj *advisor.CheckSpec, i any pCtx, err := s.PluginContextProvider.GetWithDataSource(ctx, ds.Type, requester, ds) if err != nil { if errors.Is(err, plugins.ErrPluginNotRegistered) { - // The plugin is not installed - return checks.NewCheckReportFailure( - advisor.CheckReportFailureSeverityHigh, - s.ID(), - ds.Name, - []advisor.CheckErrorLink{ - { - Message: "Delete data source", - Url: fmt.Sprintf("/connections/datasources/edit/%s", ds.UID), - }, - { - Message: "Install plugin", - Url: fmt.Sprintf("/plugins/%s", ds.Type), - }, - }, - ), nil + // The plugin is not installed, handle this in the missing plugin step + return nil, nil } // Unable to check health check s.log.Error("Failed to get plugin context", "datasource_uid", ds.UID, "error", err) @@ -196,6 +192,61 @@ func (s *healthCheckStep) Run(ctx context.Context, obj *advisor.CheckSpec, i any return nil, nil } +type missingPluginStep struct { + PluginStore pluginstore.Store + PluginRepo repo.Service + log log.Logger +} + +func (s *missingPluginStep) Title() string { + return "Missing plugin check" +} + +func (s *missingPluginStep) Description() string { + return "Checks if the plugin associated with the data source is installed." +} + +func (s *missingPluginStep) Resolution() string { + return "Delete the datasource or install the plugin." +} + +func (s *missingPluginStep) ID() string { + return MissingPluginStepID +} + +func (s *missingPluginStep) Run(ctx context.Context, obj *advisor.CheckSpec, i any) (*advisor.CheckReportFailure, error) { + ds, ok := i.(*datasources.DataSource) + if !ok { + return nil, fmt.Errorf("invalid item type %T", i) + } + + _, exists := s.PluginStore.Plugin(ctx, ds.Type) + if !exists { + links := []advisor.CheckErrorLink{ + { + Message: "Delete data source", + Url: fmt.Sprintf("/connections/datasources/edit/%s", ds.UID), + }, + } + _, err := s.PluginRepo.PluginInfo(ctx, ds.Type) + if err == nil { + // Plugin is available in the repo + links = append(links, advisor.CheckErrorLink{ + Message: "Install plugin", + Url: fmt.Sprintf("/plugins/%s", ds.Type), + }) + } + // The plugin is not installed + return checks.NewCheckReportFailure( + advisor.CheckReportFailureSeverityHigh, + s.ID(), + ds.Name, + links, + ), nil + } + return nil, nil +} + type pluginContextProvider interface { GetWithDataSource(ctx context.Context, pluginID string, user identity.Requester, ds *datasources.DataSource) (backend.PluginContext, error) } diff --git a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go index 09a7487bd8b..7c538da83f7 100644 --- a/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go +++ b/apps/advisor/pkg/app/checks/datasourcecheck/check_test.go @@ -2,6 +2,7 @@ package datasourcecheck import ( "context" + "errors" "testing" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -9,11 +10,37 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/plugins/repo" "github.com/grafana/grafana/pkg/services/datasources" + "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/user" "github.com/stretchr/testify/assert" ) +// runChecks executes all steps for all items and returns the failures +func runChecks(check *check) ([]advisor.CheckReportFailure, error) { + ctx := identity.WithRequester(context.Background(), &user.SignedInUser{}) + items, err := check.Items(ctx) + if err != nil { + return nil, err + } + + failures := []advisor.CheckReportFailure{} + for _, step := range check.Steps() { + for _, item := range items { + stepFailures, err := step.Run(ctx, &advisor.CheckSpec{}, item) + if err != nil { + return nil, err + } + if stepFailures != nil { + failures = append(failures, *stepFailures) + } + } + } + + return failures, nil +} + func TestCheck_Run(t *testing.T) { t.Run("should return no failures when all datasources are healthy", func(t *testing.T) { datasources := []*datasources.DataSource{ @@ -24,30 +51,20 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} + mockPluginRepo := &MockPluginRepo{exists: true} + mockPluginStore := &MockPluginStore{exists: true} check := &check{ DatasourceSvc: mockDatasourceSvc, PluginContextProvider: mockPluginContextProvider, PluginClient: mockPluginClient, + PluginRepo: mockPluginRepo, + PluginStore: mockPluginStore, log: log.New("advisor.datasourcecheck"), } - ctx := identity.WithRequester(context.Background(), &user.SignedInUser{}) - items, err := check.Items(ctx) + failures, err := runChecks(check) assert.NoError(t, err) - failures := []advisor.CheckReportFailure{} - for _, step := range check.Steps() { - for _, item := range items { - stepFailures, err := step.Run(ctx, &advisor.CheckSpec{}, item) - assert.NoError(t, err) - if stepFailures != nil { - failures = append(failures, *stepFailures) - } - } - } - - assert.NoError(t, err) - assert.Equal(t, 2, len(items)) assert.Empty(t, failures) }) @@ -59,30 +76,20 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} + mockPluginRepo := &MockPluginRepo{exists: true} + mockPluginStore := &MockPluginStore{exists: true} check := &check{ DatasourceSvc: mockDatasourceSvc, PluginContextProvider: mockPluginContextProvider, PluginClient: mockPluginClient, + PluginRepo: mockPluginRepo, + PluginStore: mockPluginStore, log: log.New("advisor.datasourcecheck"), } - ctx := identity.WithRequester(context.Background(), &user.SignedInUser{}) - items, err := check.Items(ctx) + failures, err := runChecks(check) assert.NoError(t, err) - failures := []advisor.CheckReportFailure{} - for _, step := range check.Steps() { - for _, item := range items { - stepFailures, err := step.Run(ctx, &advisor.CheckSpec{}, item) - assert.NoError(t, err) - if stepFailures != nil { - failures = append(failures, *stepFailures) - } - } - } - - assert.NoError(t, err) - assert.Equal(t, 1, len(items)) assert.Len(t, failures, 1) assert.Equal(t, "uid-validation", failures[0].StepID) }) @@ -95,30 +102,20 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusError}} + mockPluginRepo := &MockPluginRepo{exists: true} + mockPluginStore := &MockPluginStore{exists: true} check := &check{ DatasourceSvc: mockDatasourceSvc, PluginContextProvider: mockPluginContextProvider, PluginClient: mockPluginClient, + PluginRepo: mockPluginRepo, + PluginStore: mockPluginStore, log: log.New("advisor.datasourcecheck"), } - ctx := identity.WithRequester(context.Background(), &user.SignedInUser{}) - items, err := check.Items(ctx) + failures, err := runChecks(check) assert.NoError(t, err) - failures := []advisor.CheckReportFailure{} - for _, step := range check.Steps() { - for _, item := range items { - stepFailures, err := step.Run(ctx, &advisor.CheckSpec{}, item) - assert.NoError(t, err) - if stepFailures != nil { - failures = append(failures, *stepFailures) - } - } - } - - assert.NoError(t, err) - assert.Equal(t, 1, len(items)) assert.Len(t, failures, 1) assert.Equal(t, "health-check", failures[0].StepID) }) @@ -130,31 +127,21 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} mockPluginClient := &MockPluginClient{err: plugins.ErrMethodNotImplemented} + mockPluginRepo := &MockPluginRepo{exists: true} + mockPluginStore := &MockPluginStore{exists: true} check := &check{ DatasourceSvc: mockDatasourceSvc, PluginContextProvider: mockPluginContextProvider, PluginClient: mockPluginClient, + PluginRepo: mockPluginRepo, + PluginStore: mockPluginStore, log: log.New("advisor.datasourcecheck"), } - ctx := identity.WithRequester(context.Background(), &user.SignedInUser{}) - items, err := check.Items(ctx) + failures, err := runChecks(check) assert.NoError(t, err) - failures := []advisor.CheckReportFailure{} - for _, step := range check.Steps() { - for _, item := range items { - stepFailures, err := step.Run(ctx, &advisor.CheckSpec{}, item) - assert.NoError(t, err) - if stepFailures != nil { - failures = append(failures, *stepFailures) - } - } - } - - assert.NoError(t, err) - assert.Equal(t, 1, len(items)) - assert.Len(t, failures, 0) + assert.Empty(t, failures) }) t.Run("should return failure when plugin is not installed", func(t *testing.T) { @@ -164,33 +151,75 @@ func TestCheck_Run(t *testing.T) { mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} mockPluginClient := &MockPluginClient{err: plugins.ErrPluginNotRegistered} + mockPluginRepo := &MockPluginRepo{exists: true} + mockPluginStore := &MockPluginStore{exists: true} check := &check{ DatasourceSvc: mockDatasourceSvc, PluginContextProvider: mockPluginContextProvider, PluginClient: mockPluginClient, + PluginRepo: mockPluginRepo, + PluginStore: mockPluginStore, log: log.New("advisor.datasourcecheck"), } - ctx := identity.WithRequester(context.Background(), &user.SignedInUser{}) - items, err := check.Items(ctx) + failures, err := runChecks(check) assert.NoError(t, err) - failures := []advisor.CheckReportFailure{} - for _, step := range check.Steps() { - for _, item := range items { - stepFailures, err := step.Run(ctx, &advisor.CheckSpec{}, item) - assert.NoError(t, err) - if stepFailures != nil { - failures = append(failures, *stepFailures) - } - } - } - - assert.NoError(t, err) - assert.Equal(t, 1, len(items)) assert.Len(t, failures, 1) assert.Equal(t, "health-check", failures[0].StepID) }) + + t.Run("should return failure when plugin is not installed and the plugin is available in the repo", func(t *testing.T) { + datasources := []*datasources.DataSource{ + {UID: "valid-uid-1", Type: "prometheus", Name: "Prometheus"}, + } + mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} + mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} + mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} + mockPluginRepo := &MockPluginRepo{exists: true} + mockPluginStore := &MockPluginStore{exists: false} + + check := &check{ + DatasourceSvc: mockDatasourceSvc, + PluginContextProvider: mockPluginContextProvider, + PluginClient: mockPluginClient, + PluginRepo: mockPluginRepo, + PluginStore: mockPluginStore, + log: log.New("advisor.datasourcecheck"), + } + + failures, err := runChecks(check) + assert.NoError(t, err) + assert.Len(t, failures, 1) + assert.Equal(t, MissingPluginStepID, failures[0].StepID) + assert.Len(t, failures[0].Links, 2) + }) + + t.Run("should return failure when plugin is not installed and the plugin is not available in the repo", func(t *testing.T) { + datasources := []*datasources.DataSource{ + {UID: "valid-uid-1", Type: "prometheus", Name: "Prometheus"}, + } + mockDatasourceSvc := &MockDatasourceSvc{dss: datasources} + mockPluginContextProvider := &MockPluginContextProvider{pCtx: backend.PluginContext{}} + mockPluginClient := &MockPluginClient{res: &backend.CheckHealthResult{Status: backend.HealthStatusOk}} + mockPluginRepo := &MockPluginRepo{exists: false} + mockPluginStore := &MockPluginStore{exists: false} + + check := &check{ + DatasourceSvc: mockDatasourceSvc, + PluginContextProvider: mockPluginContextProvider, + PluginClient: mockPluginClient, + PluginRepo: mockPluginRepo, + PluginStore: mockPluginStore, + log: log.New("advisor.datasourcecheck"), + } + + failures, err := runChecks(check) + assert.NoError(t, err) + assert.Len(t, failures, 1) + assert.Equal(t, MissingPluginStepID, failures[0].StepID) + assert.Len(t, failures[0].Links, 1) + }) } type MockDatasourceSvc struct { @@ -199,7 +228,7 @@ type MockDatasourceSvc struct { dss []*datasources.DataSource } -func (m *MockDatasourceSvc) GetAllDataSources(ctx context.Context, query *datasources.GetAllDataSourcesQuery) ([]*datasources.DataSource, error) { +func (m *MockDatasourceSvc) GetAllDataSources(context.Context, *datasources.GetAllDataSourcesQuery) ([]*datasources.DataSource, error) { return m.dss, nil } @@ -207,7 +236,7 @@ type MockPluginContextProvider struct { pCtx backend.PluginContext } -func (m *MockPluginContextProvider) GetWithDataSource(ctx context.Context, pluginID string, user identity.Requester, ds *datasources.DataSource) (backend.PluginContext, error) { +func (m *MockPluginContextProvider) GetWithDataSource(context.Context, string, identity.Requester, *datasources.DataSource) (backend.PluginContext, error) { return m.pCtx, nil } @@ -218,6 +247,29 @@ type MockPluginClient struct { err error } -func (m *MockPluginClient) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { +func (m *MockPluginClient) CheckHealth(context.Context, *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { return m.res, m.err } + +type MockPluginStore struct { + pluginstore.Store + + exists bool +} + +func (m *MockPluginStore) Plugin(context.Context, string) (pluginstore.Plugin, bool) { + return pluginstore.Plugin{}, m.exists +} + +type MockPluginRepo struct { + repo.Service + + exists bool +} + +func (m *MockPluginRepo) PluginInfo(context.Context, string) (*repo.PluginInfo, error) { + if !m.exists { + return nil, errors.New("plugin not found") + } + return &repo.PluginInfo{}, nil +} From 9b6021c4904982b3f2cbfa5e29f89d416b629673 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Fri, 11 Apr 2025 11:17:32 +0100 Subject: [PATCH 31/37] AutoGridLayoutManager: Fix viz panel key duplication in auto and custom layouts (#103727) * fix viz panel key duplication in auto and custom layouts * clean up --- .../AutoGridLayoutManager.tsx | 28 ++++++++++---- .../DefaultGridLayoutManager.tsx | 37 +++++++++++++------ 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx index ad7440182c4..b81b34d2d1f 100644 --- a/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManager.tsx @@ -114,18 +114,30 @@ export class AutoGridLayoutManager } public duplicate(): DashboardLayoutManager { + const children = this.state.layout.state.children; + const clonedChildren: AutoGridItem[] = []; + + if (children.length) { + let panelId = dashboardSceneGraph.getNextPanelId(children[0].state.body); + + children.forEach((child) => { + const clone = child.clone({ + key: undefined, + body: child.state.body.clone({ + key: getVizPanelKeyForPanelId(panelId), + }), + }); + + clonedChildren.push(clone); + panelId++; + }); + } + return this.clone({ key: undefined, layout: this.state.layout.clone({ key: undefined, - children: this.state.layout.state.children.map((child) => - child.clone({ - key: undefined, - body: child.state.body.clone({ - key: getVizPanelKeyForPanelId(dashboardSceneGraph.getNextPanelId(child.state.body)), - }), - }) - ), + children: clonedChildren, }), }); } diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index caba4bcb6a3..e5b00abc627 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -229,22 +229,35 @@ export class DefaultGridLayoutManager } public duplicate(): DashboardLayoutManager { + const children = this.state.grid.state.children; + const hasGridItem = children.find((child) => child instanceof DashboardGridItem); + const clonedChildren: SceneGridItemLike[] = []; + + if (children.length) { + let panelId = hasGridItem ? dashboardSceneGraph.getNextPanelId(hasGridItem.state.body) : 1; + + children.forEach((child) => { + if (child instanceof DashboardGridItem) { + const clone = child.clone({ + key: undefined, + body: child.state.body.clone({ + key: getVizPanelKeyForPanelId(panelId), + }), + }); + + clonedChildren.push(clone); + panelId++; + } else { + clonedChildren.push(child.clone({ key: undefined })); + } + }); + } + const clone = this.clone({ key: undefined, grid: this.state.grid.clone({ key: undefined, - children: this.state.grid.state.children.map((child) => { - if (child instanceof DashboardGridItem) { - return child.clone({ - key: undefined, - body: child.state.body.clone({ - key: getVizPanelKeyForPanelId(dashboardSceneGraph.getNextPanelId(child.state.body)), - }), - }); - } - - return child.clone({ key: undefined }); - }), + children: clonedChildren, }), }); From 35e78feffade57d61416e70e9f10bcab697edba9 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Fri, 11 Apr 2025 13:50:44 +0300 Subject: [PATCH 32/37] Dynamic Dashboards: Add repeats for tabs (#103348) --- .../kinds/v2alpha1/dashboard_spec.cue | 10 +- .../dashboard/v2alpha1/dashboard_spec_gen.go | 12 + .../v2alpha1/zz_generated.openapi.go | 35 ++- .../dashboard/v2alpha0/dashboard.schema.cue | 8 +- .../schema/dashboard/v2alpha0/types.gen.ts | 14 +- .../dashboard/v2alpha1/types.spec.gen.ts | 14 +- .../dashboard.grafana.app-v2alpha1.json | 20 ++ .../scene/layout-rows/RowsLayoutManager.tsx | 41 ++- .../scene/layout-tabs/TabItem.tsx | 26 ++ .../scene/layout-tabs/TabItemEditor.tsx | 78 ++++- .../scene/layout-tabs/TabItemRenderer.tsx | 6 +- .../TabItemRepeaterBehavior.test.tsx | 272 ++++++++++++++++++ .../layout-tabs/TabItemRepeaterBehavior.ts | 161 +++++++++++ .../scene/layout-tabs/TabsLayoutManager.tsx | 47 ++- .../layoutSerializers/RowsLayoutSerializer.ts | 13 +- .../layoutSerializers/TabsLayoutSerializer.ts | 20 +- public/locales/en-US/grafana.json | 13 +- 17 files changed, 760 insertions(+), 30 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.test.tsx create mode 100644 public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.ts diff --git a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue index 5b52aef5a6e..7ac0428493b 100644 --- a/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue +++ b/apps/dashboard/kinds/v2alpha1/dashboard_spec.cue @@ -495,6 +495,11 @@ RowRepeatOptions: { value: string } +TabRepeatOptions: { + mode: RepeatMode + value: string +} + AutoGridRepeatOptions: { mode: RepeatMode value: string @@ -523,8 +528,8 @@ GridLayoutRowSpec: { y: int collapsed: bool title: string - elements: [...GridLayoutItemKind] // Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard. - repeat?: RowRepeatOptions + elements: [...GridLayoutItemKind] // Grid items in the row will have their Y value be relative to the rows Y value. This means a panel positioned at Y: 0 in a row with Y: 10 will be positioned at Y: 11 (row header has a heigh of 1) in the dashboard. + repeat?: RowRepeatOptions } GridLayoutSpec: { @@ -604,6 +609,7 @@ TabsLayoutTabSpec: { title?: string layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind conditionalRendering?: ConditionalRenderingGroupKind + repeat?: TabRepeatOptions } PanelSpec: { diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go index 6b5511a751d..f104c3fdf45 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/dashboard_spec_gen.go @@ -1069,6 +1069,7 @@ type DashboardTabsLayoutTabSpec struct { Title *string `json:"title,omitempty"` Layout DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind `json:"layout"` ConditionalRendering *DashboardConditionalRenderingGroupKind `json:"conditionalRendering,omitempty"` + Repeat *DashboardTabRepeatOptions `json:"repeat,omitempty"` } // NewDashboardTabsLayoutTabSpec creates a new DashboardTabsLayoutTabSpec object. @@ -1078,6 +1079,17 @@ func NewDashboardTabsLayoutTabSpec() *DashboardTabsLayoutTabSpec { } } +// +k8s:openapi-gen=true +type DashboardTabRepeatOptions struct { + Mode string `json:"mode"` + Value string `json:"value"` +} + +// NewDashboardTabRepeatOptions creates a new DashboardTabRepeatOptions object. +func NewDashboardTabRepeatOptions() *DashboardTabRepeatOptions { + return &DashboardTabRepeatOptions{} +} + // Links with references to other dashboards or external resources // +k8s:openapi-gen=true type DashboardDashboardLink struct { diff --git a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go index 0bed9c70b5c..e2f3b302f99 100644 --- a/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v2alpha1/zz_generated.openapi.go @@ -100,6 +100,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStatus": schema_pkg_apis_dashboard_v2alpha1_DashboardStatus(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrArrayOfString": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrArrayOfString(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardStringOrFloat64": schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrFloat64(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabRepeatOptions": schema_pkg_apis_dashboard_v2alpha1_DashboardTabRepeatOptions(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutKind(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutSpec": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutSpec(ref), "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabsLayoutTabKind": schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabKind(ref), @@ -3921,6 +3922,33 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardStringOrFloat64(ref common.Refe } } +func schema_pkg_apis_dashboard_v2alpha1_DashboardTabRepeatOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "mode": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "value": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"mode", "value"}, + }, + }, + } +} + func schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutKind(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -4027,12 +4055,17 @@ func schema_pkg_apis_dashboard_v2alpha1_DashboardTabsLayoutTabSpec(ref common.Re Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind"), }, }, + "repeat": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabRepeatOptions"), + }, + }, }, Required: []string{"layout"}, }, }, Dependencies: []string{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind"}, + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardConditionalRenderingGroupKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind", "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1.DashboardTabRepeatOptions"}, } } diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue index 41fd55839ac..4e15cffeb3d 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/dashboard.schema.cue @@ -495,6 +495,11 @@ RowRepeatOptions: { value: string } +TabRepeatOptions: { + mode: RepeatMode, + value: string +} + AutoGridRepeatOptions: { mode: RepeatMode value: string @@ -603,6 +608,7 @@ TabsLayoutTabKind: { TabsLayoutTabSpec: { title?: string layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind + repeat?: TabRepeatOptions conditionalRendering?: ConditionalRenderingGroupKind } @@ -962,4 +968,4 @@ ConditionalRenderingTimeRangeSizeKind: { ConditionalRenderingTimeRangeSizeSpec: { value: string -} \ No newline at end of file +} diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts index a53da1e3aad..3106f1b6f8f 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha0/types.gen.ts @@ -327,7 +327,7 @@ export interface FieldConfig { description?: string; // An explicit path to the field in the datasource. When the frame meta includes a path, // This will default to `${frame.meta.path}/${field.name} - // + // // When defined, this value can be used as an identifier within the datasource scope, and // may be used to update the results path?: string; @@ -916,6 +916,7 @@ export const defaultTabsLayoutTabKind = (): TabsLayoutTabKind => ({ export interface TabsLayoutTabSpec { title?: string; layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind; + repeat?: TabRepeatOptions; conditionalRendering?: ConditionalRenderingGroupKind; } @@ -923,6 +924,16 @@ export const defaultTabsLayoutTabSpec = (): TabsLayoutTabSpec => ({ layout: defaultGridLayoutKind(), }); +export interface TabRepeatOptions { + mode: "variable"; + value: string; +} + +export const defaultTabRepeatOptions = (): TabRepeatOptions => ({ + mode: RepeatMode, + value: "", +}); + // Links with references to other dashboards or external resources export interface DashboardLink { // Title to display with the link @@ -1492,4 +1503,3 @@ export const defaultVariableValueOption = (): VariableValueOption => ({ label: "", value: defaultVariableValueSingle(), }); - diff --git a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts index fb3b3b17e33..1a4fad82b96 100644 --- a/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts +++ b/packages/grafana-schema/src/schema/dashboard/v2alpha1/types.spec.gen.ts @@ -282,7 +282,7 @@ export interface FieldConfig { description?: string; // An explicit path to the field in the datasource. When the frame meta includes a path, // This will default to `${frame.meta.path}/${field.name} - // + // // When defined, this value can be used as an identifier within the datasource scope, and // may be used to update the results path?: string; @@ -872,12 +872,23 @@ export interface TabsLayoutTabSpec { title?: string; layout: GridLayoutKind | RowsLayoutKind | AutoGridLayoutKind | TabsLayoutKind; conditionalRendering?: ConditionalRenderingGroupKind; + repeat?: TabRepeatOptions; } export const defaultTabsLayoutTabSpec = (): TabsLayoutTabSpec => ({ layout: defaultGridLayoutKind(), }); +export interface TabRepeatOptions { + mode: "variable"; + value: string; +} + +export const defaultTabRepeatOptions = (): TabRepeatOptions => ({ + mode: RepeatMode, + value: "", +}); + // Links with references to other dashboards or external resources export interface DashboardLink { // Title to display with the link @@ -1402,4 +1413,3 @@ export const defaultSpec = (): Spec => ({ title: "", variables: [], }); - diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json index a179f013d4d..556cc089928 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v2alpha1.json @@ -3445,6 +3445,23 @@ } } }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabRepeatOptions": { + "type": "object", + "required": [ + "mode", + "value" + ], + "properties": { + "mode": { + "type": "string", + "default": "" + }, + "value": { + "type": "string", + "default": "" + } + } + }, "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabsLayoutKind": { "type": "object", "required": [ @@ -3518,6 +3535,9 @@ "layout": { "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind" }, + "repeat": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v2alpha1.DashboardTabRepeatOptions" + }, "title": { "type": "string" } diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index 264eb8d236f..7cd516afd99 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -1,4 +1,11 @@ -import { SceneGridItemLike, SceneGridRow, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes'; +import { + sceneGraph, + SceneGridItemLike, + SceneGridRow, + SceneObjectBase, + SceneObjectState, + VizPanel, +} from '@grafana/scenes'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { t } from 'app/core/internationalization'; @@ -8,12 +15,13 @@ import { ObjectsReorderedOnCanvasEvent, } from '../../edit-pane/shared'; import { serializeRowsLayout } from '../../serialization/layoutSerializers/RowsLayoutSerializer'; -import { isClonedKey } from '../../utils/clone'; +import { isClonedKey, joinCloneKeys } from '../../utils/clone'; import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../../utils/utils'; import { DashboardGridItem } from '../layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; import { RowRepeaterBehavior } from '../layout-default/RowRepeaterBehavior'; +import { TabItemRepeaterBehavior } from '../layout-tabs/TabItemRepeaterBehavior'; import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { getRowFromClipboard } from '../layouts-shared/paste'; import { generateUniqueTitle, ungroupLayout } from '../layouts-shared/utils'; @@ -81,7 +89,16 @@ export class RowsLayoutManager extends SceneObjectBase i } public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager { - throw new Error('Method not implemented.'); + return this.clone({ + rows: this.state.rows.map((row) => { + const key = joinCloneKeys(ancestorKey, row.state.key!); + + return row.clone({ + key, + layout: row.state.layout.cloneLayout(key, isSource), + }); + }), + }); } public duplicate(): DashboardLayoutManager { @@ -179,7 +196,21 @@ export class RowsLayoutManager extends SceneObjectBase i if (layout instanceof TabsLayoutManager) { for (const tab of layout.state.tabs) { - rows.push(new RowItem({ layout: tab.state.layout.clone(), title: tab.state.title })); + if (isClonedKey(tab.state.key!)) { + continue; + } + + const conditionalRendering = tab.state.conditionalRendering; + conditionalRendering?.clearParent(); + + const behavior = tab.state.$behaviors?.find((b) => b instanceof TabItemRepeaterBehavior); + const $behaviors = !behavior + ? undefined + : [new RowItemRepeaterBehavior({ variableName: behavior.state.variableName })]; + + rows.push( + new RowItem({ layout: tab.state.layout.clone(), title: tab.state.title, conditionalRendering, $behaviors }) + ); } } else if (layout instanceof DefaultGridLayoutManager) { const config: Array<{ @@ -253,7 +284,7 @@ export class RowsLayoutManager extends SceneObjectBase i const duplicateTitles = new Set(); this.state.rows.forEach((row) => { - const title = row.state.title; + const title = sceneGraph.interpolate(row, row.state.title); const count = (titleCounts.get(title) ?? 0) + 1; titleCounts.set(title, count); if (count > 1 && title) { diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx index 39e1de86928..17f033999b9 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx @@ -33,6 +33,7 @@ import { LayoutParent } from '../types/LayoutParent'; import { useEditOptions } from './TabItemEditor'; import { TabItemRenderer } from './TabItemRenderer'; +import { TabItemRepeaterBehavior } from './TabItemRepeaterBehavior'; import { TabItems } from './TabItems'; import { TabsLayoutManager } from './TabsLayoutManager'; @@ -176,6 +177,23 @@ export class TabItem this.onChangeTitle(name); } + public onChangeRepeat(repeat: string | undefined) { + let repeatBehavior = this._getRepeatBehavior(); + + if (repeat) { + // Remove repeat behavior if it exists to trigger repeat when adding new one + if (repeatBehavior) { + repeatBehavior.removeBehavior(); + } + + repeatBehavior = new TabItemRepeaterBehavior({ variableName: repeat }); + this.setState({ $behaviors: [...(this.state.$behaviors ?? []), repeatBehavior] }); + repeatBehavior.activate(); + } else { + repeatBehavior?.removeBehavior(); + } + } + public setIsDropTarget(isDropTarget: boolean) { if (!!this.state.isDropTarget !== isDropTarget) { this.setState({ isDropTarget }); @@ -199,6 +217,10 @@ export class TabItem } } + public getRepeatVariable(): string | undefined { + return this._getRepeatBehavior()?.state.variableName; + } + public getParentLayout(): TabsLayoutManager { return sceneGraph.getAncestor(this, TabsLayoutManager); } @@ -217,4 +239,8 @@ export class TabItem const duplicateTitles = parentLayout.duplicateTitles(); return !duplicateTitles.has(this.state.title); } + + private _getRepeatBehavior(): TabItemRepeaterBehavior | undefined { + return this.state.$behaviors?.find((b) => b instanceof TabItemRepeaterBehavior); + } } diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx index 9958e0c87ce..84e7260de10 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx @@ -1,11 +1,16 @@ import { useMemo } from 'react'; -import { Input, Field } from '@grafana/ui'; -import { t } from 'app/core/internationalization'; +import { selectors } from '@grafana/e2e-selectors'; +import { Alert, Input, Field, TextLink } from '@grafana/ui'; +import { t, Trans } from 'app/core/internationalization'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; +import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect'; +import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants'; +import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSource'; import { useConditionalRenderingEditor } from '../../conditional-rendering/ConditionalRenderingEditor'; +import { getQueryRunnerFor, useDashboard } from '../../utils/utils'; import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector'; import { useEditPaneInputAutoFocus } from '../layouts-shared/utils'; @@ -25,9 +30,28 @@ export function useEditOptions(model: TabItem, isNewElement: boolean): OptionsPa [model, isNewElement] ); + const repeatCategory = useMemo( + () => + new OptionsPaneCategoryDescriptor({ + title: t('dashboard.tabs-layout.tab-options.repeat.title', 'Repeat options'), + id: 'repeat-options', + isOpenDefault: false, + }).addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.tabs-layout.tab-options.repeat.variable.title', 'Repeat by variable'), + description: t( + 'dashboard.tabs-layout.tab-options.repeat.variable.description', + 'Repeat this tab for each value in the selected variable.' + ), + render: () => , + }) + ), + [model] + ); + const layoutCategory = useLayoutCategory(layout); - const editOptions = [tabCategory, ...layoutCategory]; + const editOptions = [tabCategory, ...layoutCategory, repeatCategory]; const conditionalRenderingCategory = useMemo( () => useConditionalRenderingEditor(model.state.conditionalRendering), @@ -62,3 +86,51 @@ function TabTitleInput({ tab, isNewElement }: { tab: TabItem; isNewElement: bool ); } + +function TabRepeatSelect({ tab }: { tab: TabItem }) { + const { layout } = tab.useState(); + const dashboard = useDashboard(tab); + + const isAnyPanelUsingDashboardDS = layout.getVizPanels().some((vizPanel) => { + const runner = getQueryRunnerFor(vizPanel); + return ( + runner?.state.datasource?.uid === SHARED_DASHBOARD_QUERY || + (runner?.state.datasource?.uid === MIXED_DATASOURCE_NAME && + runner?.state.queries.some((query) => query.datasource?.uid === SHARED_DASHBOARD_QUERY)) + ); + }); + + return ( + <> + tab.onChangeRepeat(repeat)} + /> + {isAnyPanelUsingDashboardDS ? ( + +

+ + Panels in this tab use the {{ SHARED_DASHBOARD_QUERY }} data source. These panels will reference the panel + in the original tab, not the ones in the repeated tabs. + +

+ + Learn more + +
+ ) : undefined} + + ); +} diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx index 2400ce19cfb..178a6870967 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx @@ -8,6 +8,7 @@ import { Tab, useElementSelection, usePointerDistance, useStyles2 } from '@grafa import { t } from 'app/core/internationalization'; import { useIsConditionallyHidden } from '../../conditional-rendering/useIsConditionallyHidden'; +import { useIsClone } from '../../utils/clone'; import { useDashboardState } from '../../utils/utils'; import { TabItem } from './TabItem'; @@ -28,6 +29,9 @@ export function TabItemRenderer({ model }: SceneComponentProps) { const styles = useStyles2(getStyles); const pointerDistance = usePointerDistance(); const [isConditionallyHidden] = useIsConditionallyHidden(model); + const isClone = useIsClone(model); + + const isDraggable = !isClone && isEditing; if (isConditionallyHidden && !isEditing && !isActive) { return null; @@ -43,7 +47,7 @@ export function TabItemRenderer({ model }: SceneComponentProps) { } return ( - + {(dragProvided, dragSnapshot) => (
dragProvided.innerRef(ref)} diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.test.tsx new file mode 100644 index 00000000000..93a0d400a71 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.test.tsx @@ -0,0 +1,272 @@ +import { VariableRefresh } from '@grafana/data'; +import { getPanelPlugin } from '@grafana/data/test'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { + SceneGridRow, + SceneTimeRange, + SceneVariableSet, + TestVariable, + VariableValueOption, + PanelBuilders, +} from '@grafana/scenes'; +import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants'; +import { TextMode } from 'app/plugins/panel/text/panelcfg.gen'; + +import { getCloneKey, isInCloneChain, joinCloneKeys } from '../../utils/clone'; +import { activateFullSceneTree } from '../../utils/test-utils'; +import { DashboardScene } from '../DashboardScene'; +import { DashboardGridItem } from '../layout-default/DashboardGridItem'; +import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; + +import { TabItem } from './TabItem'; +import { TabItemRepeaterBehavior } from './TabItemRepeaterBehavior'; +import { TabsLayoutManager } from './TabsLayoutManager'; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + setPluginExtensionGetter: jest.fn(), + getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }), +})); + +setPluginImportUtils({ + importPanelPlugin: () => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: () => undefined, +}); + +describe('TabItemRepeaterBehavior', () => { + describe('Given scene with variable with 5 values', () => { + let scene: DashboardScene, layout: TabsLayoutManager, repeatBehavior: TabItemRepeaterBehavior; + let layoutStateUpdates: unknown[]; + + beforeEach(async () => { + ({ scene, layout, repeatBehavior } = buildScene({ variableQueryTime: 0 })); + + layoutStateUpdates = []; + layout.subscribeToState((state) => layoutStateUpdates.push(state)); + + activateFullSceneTree(scene); + await new Promise((r) => setTimeout(r, 1)); + }); + + it('Should repeat tab', () => { + // Verify that first tab still has repeat behavior + const tab1 = layout.state.tabs[0]; + expect(tab1.state.key).toBe(getCloneKey('tab-1', 0)); + expect(tab1.state.$behaviors?.[0]).toBeInstanceOf(TabItemRepeaterBehavior); + expect(tab1.state.$variables!.state.variables[0].getValue()).toBe('A1'); + + const tab1Children = getTabChildren(tab1); + expect(tab1Children[0].state.key!).toBe(joinCloneKeys(tab1.state.key!, 'grid-item-0')); + expect(tab1Children[0].state.body?.state.key).toBe(joinCloneKeys(tab1Children[0].state.key!, 'panel-0')); + + const tab2 = layout.state.tabs[1]; + expect(tab2.state.key).toBe(getCloneKey('tab-1', 1)); + expect(tab2.state.$behaviors).toEqual([]); + expect(tab2.state.$variables!.state.variables[0].getValueText?.()).toBe('B'); + + const tab2Children = getTabChildren(tab2); + expect(tab2Children[0].state.key!).toBe(joinCloneKeys(tab2.state.key!, 'grid-item-0')); + expect(tab2Children[0].state.body?.state.key).toBe(joinCloneKeys(tab2Children[0].state.key!, 'panel-0')); + }); + + it('Repeated tabs should be read only', () => { + const tab1 = layout.state.tabs[0]; + expect(isInCloneChain(tab1.state.key!)).toBe(false); + + const tab2 = layout.state.tabs[1]; + expect(isInCloneChain(tab2.state.key!)).toBe(true); + }); + + it('Should push tab at the bottom down', () => { + // Should push tab at the bottom down + const tabAtTheBottom = layout.state.tabs[5]; + expect(tabAtTheBottom.state.title).toBe('Tab at the bottom'); + }); + + it('Should handle second repeat cycle and update remove old repeats', async () => { + // trigger another repeat cycle by changing the variable + const variable = scene.state.$variables!.state.variables[0] as TestVariable; + variable.changeValueTo(['B1', 'C1']); + + await new Promise((r) => setTimeout(r, 1)); + + // should now only have 2 repeated tabs (and the panel above + the tab at the bottom) + expect(layout.state.tabs.length).toBe(3); + }); + + it('Should ignore repeat process if variable values are the same', async () => { + // trigger another repeat cycle by changing the variable + repeatBehavior.performRepeat(); + + await new Promise((r) => setTimeout(r, 1)); + + expect(layoutStateUpdates.length).toBe(1); + }); + }); + + describe('Given scene with variable with 15 values', () => { + let scene: DashboardScene, layout: TabsLayoutManager; + let layoutStateUpdates: unknown[]; + + beforeEach(async () => { + ({ scene, layout } = buildScene({ variableQueryTime: 0 }, [ + { label: 'A', value: 'A1' }, + { label: 'B', value: 'B1' }, + { label: 'C', value: 'C1' }, + { label: 'D', value: 'D1' }, + { label: 'E', value: 'E1' }, + { label: 'F', value: 'F1' }, + { label: 'G', value: 'G1' }, + { label: 'H', value: 'H1' }, + { label: 'I', value: 'I1' }, + { label: 'J', value: 'J1' }, + { label: 'K', value: 'K1' }, + { label: 'L', value: 'L1' }, + { label: 'M', value: 'M1' }, + { label: 'N', value: 'N1' }, + { label: 'O', value: 'O1' }, + ])); + + layoutStateUpdates = []; + layout.subscribeToState((state) => layoutStateUpdates.push(state)); + + activateFullSceneTree(scene); + await new Promise((r) => setTimeout(r, 1)); + }); + + it('Should handle second repeat cycle and update remove old repeats', async () => { + // should have 15 repeated tabs (and the panel above) + expect(layout.state.tabs.length).toBe(16); + + // trigger another repeat cycle by changing the variable + const variable = scene.state.$variables!.state.variables[0] as TestVariable; + variable.changeValueTo(['B1', 'C1']); + + await new Promise((r) => setTimeout(r, 1)); + + // should now only have 2 repeated tabs (and the panel above) + expect(layout.state.tabs.length).toBe(3); + }); + }); + + describe('Given a scene with empty variable', () => { + it('Should preserve repeat tab', async () => { + const { scene, layout } = buildScene({ variableQueryTime: 0 }, []); + activateFullSceneTree(scene); + await new Promise((r) => setTimeout(r, 1)); + + // Should have 2 tabs, one without repeat and one with the dummy tab + expect(layout.state.tabs.length).toBe(2); + expect(layout.state.tabs[0].state.$behaviors?.[0]).toBeInstanceOf(TabItemRepeaterBehavior); + }); + }); +}); + +interface SceneOptions { + variableQueryTime: number; + variableRefresh?: VariableRefresh; +} + +function buildTextPanel(key: string, content: string) { + const panel = PanelBuilders.text().setOption('content', content).setOption('mode', TextMode.Markdown).build(); + panel.setState({ key }); + return panel; +} + +function buildScene( + options: SceneOptions, + variableOptions?: VariableValueOption[], + variableStateOverrides?: { isMulti: boolean } +) { + const repeatBehavior = new TabItemRepeaterBehavior({ variableName: 'server' }); + + const tabs = [ + new TabItem({ + key: 'tab-1', + $behaviors: [repeatBehavior], + layout: DefaultGridLayoutManager.fromGridItems([ + new DashboardGridItem({ + key: 'grid-item-1', + x: 0, + y: 11, + width: 24, + height: 5, + body: buildTextPanel('text-1', 'Panel inside repeated tab, server = $server'), + }), + ]), + }), + new TabItem({ + key: 'tab-2', + title: 'Tab at the bottom', + layout: DefaultGridLayoutManager.fromGridItems([ + new DashboardGridItem({ + key: 'grid-item-2', + x: 0, + y: 17, + body: buildTextPanel('text-2', 'Panel inside tab, server = $server'), + }), + new DashboardGridItem({ + key: 'grid-item-3', + x: 0, + y: 25, + body: buildTextPanel('text-3', 'Panel inside tab, server = $server'), + }), + ]), + }), + ]; + + const layout = new TabsLayoutManager({ tabs }); + + const scene = new DashboardScene({ + $timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }), + $variables: new SceneVariableSet({ + variables: [ + new TestVariable({ + name: 'server', + query: 'A.*', + value: ALL_VARIABLE_VALUE, + text: ALL_VARIABLE_TEXT, + isMulti: true, + includeAll: true, + delayMs: options.variableQueryTime, + refresh: options.variableRefresh, + optionsToReturn: variableOptions ?? [ + { label: 'A', value: 'A1' }, + { label: 'B', value: 'B1' }, + { label: 'C', value: 'C1' }, + { label: 'D', value: 'D1' }, + { label: 'E', value: 'E1' }, + ], + ...variableStateOverrides, + }), + ], + }), + body: layout, + }); + + const tabToRepeat = repeatBehavior.parent as SceneGridRow; + + return { scene, layout, tabs, repeatBehavior, tabToRepeat }; +} + +function getTabLayout(tab: TabItem): DefaultGridLayoutManager { + const layout = tab.getLayout(); + + if (!(layout instanceof DefaultGridLayoutManager)) { + throw new Error('Invalid layout'); + } + + return layout; +} + +function getTabChildren(tab: TabItem): DashboardGridItem[] { + const layout = getTabLayout(tab); + + const filteredChildren = layout.state.grid.state.children.filter((child) => child instanceof DashboardGridItem); + + if (filteredChildren.length !== layout.state.grid.state.children.length) { + throw new Error('Invalid layout'); + } + + return filteredChildren; +} diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.ts new file mode 100644 index 00000000000..a0e0207c7ad --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRepeaterBehavior.ts @@ -0,0 +1,161 @@ +import { isEqual } from 'lodash'; + +import { + LocalValueVariable, + MultiValueVariable, + sceneGraph, + SceneObjectBase, + SceneObjectState, + SceneVariableSet, + VariableDependencyConfig, + VariableValueSingle, +} from '@grafana/scenes'; + +import { isClonedKeyOf, getCloneKey } from '../../utils/clone'; +import { getMultiVariableValues } from '../../utils/utils'; +import { DashboardRepeatsProcessedEvent } from '../types/DashboardRepeatsProcessedEvent'; + +import { TabItem } from './TabItem'; +import { TabsLayoutManager } from './TabsLayoutManager'; + +interface TabItemRepeaterBehaviorState extends SceneObjectState { + variableName: string; +} + +export class TabItemRepeaterBehavior extends SceneObjectBase { + protected _variableDependency = new VariableDependencyConfig(this, { + variableNames: [this.state.variableName], + onVariableUpdateCompleted: () => this.performRepeat(), + }); + + private _prevRepeatValues?: VariableValueSingle[]; + private _clonedTabs?: TabItem[]; + + public constructor(state: TabItemRepeaterBehaviorState) { + super(state); + + this.addActivationHandler(() => this._activationHandler()); + } + + private _activationHandler() { + this.performRepeat(); + } + + private _getTab(): TabItem { + if (!(this.parent instanceof TabItem)) { + throw new Error('RepeatedTabItemBehavior: Parent is not a TabItem'); + } + + return this.parent; + } + + private _getLayout(): TabsLayoutManager { + const layout = this._getTab().parent; + + if (!(layout instanceof TabsLayoutManager)) { + throw new Error('RepeatedTabItemBehavior: Layout is not a TabsLayoutManager'); + } + + return layout; + } + + public performRepeat(force = false) { + if (this._variableDependency.hasDependencyInLoadingState()) { + return; + } + + const variable = sceneGraph.lookupVariable(this.state.variableName, this.parent?.parent!); + + if (!variable) { + console.error('RepeatedTabItemBehavior: Variable not found'); + return; + } + + if (!(variable instanceof MultiValueVariable)) { + console.error('RepeatedTabItemBehavior: Variable is not a MultiValueVariable'); + return; + } + + const tabToRepeat = this._getTab(); + const layout = this._getLayout(); + const { values, texts } = getMultiVariableValues(variable); + + // Do nothing if values are the same + if (isEqual(this._prevRepeatValues, values) && !force) { + return; + } + + this._prevRepeatValues = values; + + this._clonedTabs = []; + + const tabContent = tabToRepeat.getLayout(); + + // when variable has no options (due to error or similar) it will not render any panels at all + // adding a placeholder in this case so that there is at least empty panel that can display error + const emptyVariablePlaceholderOption = { + values: [''], + texts: variable.hasAllValue() ? ['All'] : ['None'], + }; + + const variableValues = values.length ? values : emptyVariablePlaceholderOption.values; + const variableTexts = texts.length ? texts : emptyVariablePlaceholderOption.texts; + + // Loop through variable values and create repeats + for (let tabIndex = 0; tabIndex < variableValues.length; tabIndex++) { + const isSourceTab = tabIndex === 0; + const tabClone = isSourceTab ? tabToRepeat : tabToRepeat.clone({ $behaviors: [] }); + + const tabCloneKey = getCloneKey(tabToRepeat.state.key!, tabIndex); + + tabClone.setState({ + key: tabCloneKey, + $variables: new SceneVariableSet({ + variables: [ + new LocalValueVariable({ + name: this.state.variableName, + value: variableValues[tabIndex], + text: String(variableTexts[tabIndex]), + isMulti: variable.state.isMulti, + includeAll: variable.state.includeAll, + }), + ], + }), + layout: tabContent.cloneLayout?.(tabCloneKey, isSourceTab), + }); + + this._clonedTabs.push(tabClone); + } + + updateLayout(layout, this._clonedTabs, tabToRepeat.state.key!); + + // Used from dashboard url sync + this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true); + } + + public removeBehavior() { + const tab = this._getTab(); + const layout = this._getLayout(); + const tabs = getTabsFilterOutRepeatClones(layout, tab.state.key!); + + layout.setState({ tabs }); + + // Remove behavior and the scoped local variable + tab.setState({ $behaviors: tab.state.$behaviors!.filter((b) => b !== this), $variables: undefined }); + } +} + +function updateLayout(layout: TabsLayoutManager, tabs: TabItem[], tabKey: string) { + const allTabs = getTabsFilterOutRepeatClones(layout, tabKey); + const index = allTabs.findIndex((tab) => tab.state.key!.includes(tabKey)); + + if (index === -1) { + throw new Error('TabItemRepeaterBehavior: Tab not found in layout'); + } + + layout.setState({ tabs: [...allTabs.slice(0, index), ...tabs, ...allTabs.slice(index + 1)] }); +} + +function getTabsFilterOutRepeatClones(layout: TabsLayoutManager, tabKey: string) { + return layout.state.tabs.filter((tab) => !isClonedKeyOf(tab.state.key!, tabKey)); +} diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx index 9be608411c1..ba0bcb661eb 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -1,4 +1,5 @@ import { + sceneGraph, SceneObjectBase, SceneObjectState, SceneObjectUrlSyncConfig, @@ -14,8 +15,10 @@ import { ObjectsReorderedOnCanvasEvent, } from '../../edit-pane/shared'; import { serializeTabsLayout } from '../../serialization/layoutSerializers/TabsLayoutSerializer'; +import { isClonedKey, joinCloneKeys } from '../../utils/clone'; import { getDashboardSceneFor } from '../../utils/utils'; import { RowItem } from '../layout-rows/RowItem'; +import { RowItemRepeaterBehavior } from '../layout-rows/RowItemRepeaterBehavior'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; import { getTabFromClipboard } from '../layouts-shared/paste'; import { generateUniqueTitle, ungroupLayout } from '../layouts-shared/utils'; @@ -23,6 +26,7 @@ import { DashboardLayoutManager } from '../types/DashboardLayoutManager'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; import { TabItem } from './TabItem'; +import { TabItemRepeaterBehavior } from './TabItemRepeaterBehavior'; import { TabsLayoutManagerRenderer } from './TabsLayoutManagerRenderer'; interface TabsLayoutManagerState extends SceneObjectState { @@ -122,7 +126,16 @@ export class TabsLayoutManager extends SceneObjectBase i } public cloneLayout(ancestorKey: string, isSource: boolean): DashboardLayoutManager { - throw new Error('Method not implemented.'); + return this.clone({ + tabs: this.state.tabs.map((tab) => { + const key = joinCloneKeys(ancestorKey, tab.state.key!); + + return tab.clone({ + key, + layout: tab.state.layout.cloneLayout(key, isSource), + }); + }), + }); } public addNewTab(tab?: TabItem) { @@ -149,7 +162,19 @@ export class TabsLayoutManager extends SceneObjectBase i } public activateRepeaters() { - this.state.tabs.forEach((tab) => tab.getLayout().activateRepeaters?.()); + this.state.tabs.forEach((tab) => { + if (!tab.isActive) { + tab.activate(); + } + + const behavior = (tab.state.$behaviors ?? []).find((b) => b instanceof TabItemRepeaterBehavior); + + if (!behavior?.isActive) { + behavior?.activate(); + } + + tab.getLayout().activateRepeaters?.(); + }); } public shouldUngroup(): boolean { @@ -210,7 +235,21 @@ export class TabsLayoutManager extends SceneObjectBase i if (layout instanceof RowsLayoutManager) { for (const row of layout.state.rows) { - tabs.push(new TabItem({ layout: row.state.layout.clone(), title: row.state.title })); + if (isClonedKey(row.state.key!)) { + continue; + } + + const conditionalRendering = row.state.conditionalRendering; + conditionalRendering?.clearParent(); + + const behavior = row.state.$behaviors?.find((b) => b instanceof RowItemRepeaterBehavior); + const $behaviors = !behavior + ? undefined + : [new TabItemRepeaterBehavior({ variableName: behavior.state.variableName })]; + + tabs.push( + new TabItem({ layout: row.state.layout.clone(), title: row.state.title, conditionalRendering, $behaviors }) + ); } } else { layout.clearParent(); @@ -245,7 +284,7 @@ export class TabsLayoutManager extends SceneObjectBase i const duplicateTitles = new Set(); this.state.tabs.forEach((tab) => { - const title = tab.state.title; + const title = sceneGraph.interpolate(tab, tab.state.title); const count = (titleCounts.get(title) ?? 0) + 1; titleCounts.set(title, count); if (count > 1) { diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts index b6fae840629..49a8cf94870 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/RowsLayoutSerializer.ts @@ -1,4 +1,3 @@ -import { SceneObject } from '@grafana/scenes'; import { Spec as DashboardV2Spec, RowsLayoutRowKind, @@ -7,6 +6,7 @@ import { import { RowItem } from '../../scene/layout-rows/RowItem'; import { RowItemRepeaterBehavior } from '../../scene/layout-rows/RowItemRepeaterBehavior'; import { RowsLayoutManager } from '../../scene/layout-rows/RowsLayoutManager'; +import { isClonedKey } from '../../utils/clone'; import { layoutDeserializerRegistry } from './layoutSerializerRegistry'; import { getConditionalRendering } from './utils'; @@ -15,7 +15,7 @@ export function serializeRowsLayout(layoutManager: RowsLayoutManager): Dashboard return { kind: 'RowsLayout', spec: { - rows: layoutManager.state.rows.map(serializeRow), + rows: layoutManager.state.rows.filter((row) => !isClonedKey(row.state.key!)).map(serializeRow), }, }; } @@ -72,17 +72,16 @@ export function deserializeRow( panelIdGenerator?: () => number ): RowItem { const layout = row.spec.layout; - const behaviors: SceneObject[] = []; - if (row.spec.repeat) { - behaviors.push(new RowItemRepeaterBehavior({ variableName: row.spec.repeat.value })); - } + const $behaviors = !row.spec.repeat + ? undefined + : [new RowItemRepeaterBehavior({ variableName: row.spec.repeat.value })]; return new RowItem({ title: row.spec.title, collapse: row.spec.collapse, hideHeader: row.spec.hideHeader, fillScreen: row.spec.fillScreen, - $behaviors: behaviors, + $behaviors, layout: layoutDeserializerRegistry.get(layout.kind).deserialize(layout, elements, preload, panelIdGenerator), conditionalRendering: getConditionalRendering(row), }); diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts index 6ab820816d1..9a8cbe50bdb 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/TabsLayoutSerializer.ts @@ -4,7 +4,9 @@ import { } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { TabItem } from '../../scene/layout-tabs/TabItem'; +import { TabItemRepeaterBehavior } from '../../scene/layout-tabs/TabItemRepeaterBehavior'; import { TabsLayoutManager } from '../../scene/layout-tabs/TabsLayoutManager'; +import { isClonedKey } from '../../utils/clone'; import { layoutDeserializerRegistry } from './layoutSerializerRegistry'; import { getConditionalRendering } from './utils'; @@ -13,7 +15,7 @@ export function serializeTabsLayout(layoutManager: TabsLayoutManager): Dashboard return { kind: 'TabsLayout', spec: { - tabs: layoutManager.state.tabs.map(serializeTab), + tabs: layoutManager.state.tabs.filter((tab) => !isClonedKey(tab.state.key!)).map(serializeTab), }, }; } @@ -34,6 +36,17 @@ export function serializeTab(tab: TabItem): TabsLayoutTabKind { tabKind.spec.conditionalRendering = conditionalRenderingRootGroup; } + if (tab.state.$behaviors) { + for (const behavior of tab.state.$behaviors) { + if (behavior instanceof TabItemRepeaterBehavior) { + if (tabKind.spec.repeat) { + throw new Error('Multiple repeaters are not supported'); + } + tabKind.spec.repeat = { value: behavior.state.variableName, mode: 'variable' }; + } + } + } + return tabKind; } @@ -61,9 +74,14 @@ export function deserializeTab( panelIdGenerator?: () => number ): TabItem { const layout = tab.spec.layout; + const $behaviors = !tab.spec.repeat + ? undefined + : [new TabItemRepeaterBehavior({ variableName: tab.spec.repeat.value })]; + return new TabItem({ title: tab.spec.title, layout: layoutDeserializerRegistry.get(layout.kind).deserialize(layout, elements, preload, panelIdGenerator), + $behaviors, conditionalRendering: getConditionalRendering(tab), }); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 823fddaec55..c0550c104e6 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -3107,9 +3107,20 @@ "description": "Organize panels into horizontal tabs", "name": "Tabs", "tab": { - "new": "New tab" + "new": "New tab", + "repeat": { + "learn-more": "Learn more", + "warning": "Panels in this tab use the {{SHARED_DASHBOARD_QUERY}} data source. These panels will reference the panel in the original tab, not the ones in the repeated tabs." + } }, "tab-options": { + "repeat": { + "title": "Repeat options", + "variable": { + "description": "Repeat this tab for each value in the selected variable.", + "title": "Repeat by variable" + } + }, "title-not-unique": "Title should be unique", "title-option": "Title" }, From 962232c31cf7f1695b598e125d212428c71994c3 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Fri, 11 Apr 2025 13:39:11 +0200 Subject: [PATCH 33/37] Prometheus: Add ability to clear metric name in PromQL query builder (#103821) is clearable true --- .../src/querybuilder/components/MetricSelect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grafana-prometheus/src/querybuilder/components/MetricSelect.tsx b/packages/grafana-prometheus/src/querybuilder/components/MetricSelect.tsx index 5e752bd89f8..0de1f240694 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/MetricSelect.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/MetricSelect.tsx @@ -249,7 +249,7 @@ export function MetricSelect({ return ( Date: Fri, 11 Apr 2025 05:47:40 -0600 Subject: [PATCH 34/37] TableNg: Fix a few bugs found in K8s land (#103829) * fix: a few bugs found in K8s land * fix: column width buggy behavior * fix: column width try again --- .../Table/TableNG/Cells/TableCellNG.tsx | 2 ++ .../src/components/Table/TableNG/TableNG.tsx | 32 +++++++++---------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx index 3e3138e55ef..1a1c3bbca5a 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx @@ -159,6 +159,7 @@ export function TableCellNG(props: TableCellNGProps) { tableCellDiv?.style.setProperty('min-height', `100%`); tableCellDiv?.style.setProperty('height', `fit-content`); tableCellDiv?.style.setProperty('background', colors.bgHoverColor || 'none'); + tableCellDiv?.style.setProperty('min-width', 'min-content'); } }; @@ -173,6 +174,7 @@ export function TableCellNG(props: TableCellNGProps) { tableCellDiv?.style.removeProperty('min-height'); tableCellDiv?.style.removeProperty('height'); tableCellDiv?.style.removeProperty('background'); + tableCellDiv?.style.removeProperty('min-width'); } }; diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx index 446c5023645..01934d0da2e 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx @@ -865,28 +865,26 @@ export function mapFrameToDataGrid({ }); }); - // INFO: This loop calculates the width for each column in less than a millisecond. + // set columns that are at minimum width let sharedWidth = availableWidth / fieldCountWithoutWidth; - - // First pass: Assign minimum widths to columns that need it - columns.forEach((column) => { - if (!column.width && column.minWidth! > sharedWidth) { - column.width = column.minWidth; - availableWidth -= column.width!; - fieldCountWithoutWidth -= 1; + for (let i = fieldCountWithoutWidth; i > 0; i--) { + for (const column of columns) { + if (!column.width && column.minWidth! > sharedWidth) { + column.width = column.minWidth; + availableWidth -= column.width!; + fieldCountWithoutWidth -= 1; + sharedWidth = availableWidth / fieldCountWithoutWidth; + } } - }); + } - // Recalculate shared width after assigning minimum widths - sharedWidth = availableWidth / fieldCountWithoutWidth; - - // Second pass: Assign shared width to remaining columns - columns.forEach((column) => { + // divide up the rest of the space + for (const column of columns) { if (!column.width) { column.width = sharedWidth; } - column.minWidth = COLUMN.MIN_WIDTH; // Ensure min-width is always set - }); + column.minWidth = COLUMN.MIN_WIDTH; + } return columns; } @@ -980,6 +978,8 @@ const getStyles = (theme: GrafanaTheme2) => ({ '--rdg-summary-border-color': theme.colors.border.medium, '.rdg-cell': { + // Prevent collisions with custom cell components + zIndex: 2, borderRight: 'none', }, }, From 50499fa749511f49f90643d77fcb891fa60ec6f9 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Fri, 11 Apr 2025 13:56:58 +0200 Subject: [PATCH 35/37] Dashboards: Add a config setting that limits the number of series that will be displayed in a panel. Users can opt in to render all series. (#103405) * Limit series in panels in dashboards * fix mangled comment --- conf/defaults.ini | 3 +++ packages/grafana-data/src/types/config.ts | 1 + packages/grafana-runtime/src/config.ts | 1 + pkg/api/dtos/frontend_settings.go | 1 + pkg/api/frontendsettings.go | 1 + pkg/setting/setting.go | 2 ++ .../dashboard-scene/serialization/layoutSerializers/utils.ts | 2 ++ .../dashboard-scene/serialization/transformSaveModelToScene.ts | 1 + public/app/features/dashboard-scene/utils/utils.ts | 1 + 9 files changed, 13 insertions(+) diff --git a/conf/defaults.ini b/conf/defaults.ini index 6997156c517..1678a620278 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -472,6 +472,9 @@ default_home_dashboard_path = # Dashboards UIDs to report performance metrics for. * can be used to report metrics for all dashboards dashboard_performance_metrics = +# Maximum number of series that will be showed in a single panel. Users can opt in to rendering all series. Default is 0 (unlimited). +panel_series_limit = + ################################### Data sources ######################### [datasources] # Upper limit of data sources that Grafana will return. This limit is a temporary configuration and it will be deprecated when pagination will be introduced on the list data sources API. diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts index dbfbc5eeeb3..3e6de71e8d3 100644 --- a/packages/grafana-data/src/types/config.ts +++ b/packages/grafana-data/src/types/config.ts @@ -227,6 +227,7 @@ export interface GrafanaConfig { rudderstackIntegrationsUrl: string | undefined; analyticsConsoleReporting: boolean; dashboardPerformanceMetrics: string[]; + panelSeriesLimit: number; sqlConnectionLimits: SqlConnectionLimits; sharedWithMeFolderUID?: string; rootFolderUID?: string; diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index 8b0beab714f..7031216db7c 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -184,6 +184,7 @@ export class GrafanaBootConfig implements GrafanaConfig { rudderstackIntegrationsUrl: undefined; analyticsConsoleReporting = false; dashboardPerformanceMetrics: string[] = []; + panelSeriesLimit = 0; sqlConnectionLimits = { maxOpenConns: 100, maxIdleConns: 100, diff --git a/pkg/api/dtos/frontend_settings.go b/pkg/api/dtos/frontend_settings.go index b9c66d8de32..16ee4d2ac3d 100644 --- a/pkg/api/dtos/frontend_settings.go +++ b/pkg/api/dtos/frontend_settings.go @@ -194,6 +194,7 @@ type FrontendSettingsDTO struct { AnalyticsConsoleReporting bool `json:"analyticsConsoleReporting"` DashboardPerformanceMetrics []string `json:"dashboardPerformanceMetrics"` + PanelSeriesLimit int `json:"panelSeriesLimit"` FeedbackLinksEnabled bool `json:"feedbackLinksEnabled"` ApplicationInsightsConnectionString string `json:"applicationInsightsConnectionString"` diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index c069cf8cf9a..935455a09c4 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -216,6 +216,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro RudderstackIntegrationsUrl: hs.Cfg.RudderstackIntegrationsURL, AnalyticsConsoleReporting: hs.Cfg.FrontendAnalyticsConsoleReporting, DashboardPerformanceMetrics: hs.Cfg.DashboardPerformanceMetrics, + PanelSeriesLimit: hs.Cfg.PanelSeriesLimit, FeedbackLinksEnabled: hs.Cfg.FeedbackLinksEnabled, ApplicationInsightsConnectionString: hs.Cfg.ApplicationInsightsConnectionString, ApplicationInsightsEndpointUrl: hs.Cfg.ApplicationInsightsEndpointUrl, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 84df40fd67a..c20b0468699 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -226,6 +226,7 @@ type Cfg struct { MinRefreshInterval string DefaultHomeDashboardPath string DashboardPerformanceMetrics []string + PanelSeriesLimit int // Auth LoginCookieName string @@ -1149,6 +1150,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.MinRefreshInterval = valueAsString(dashboards, "min_refresh_interval", "5s") cfg.DefaultHomeDashboardPath = dashboards.Key("default_home_dashboard_path").MustString("") cfg.DashboardPerformanceMetrics = util.SplitString(dashboards.Key("dashboard_performance_metrics").MustString("")) + cfg.PanelSeriesLimit = dashboards.Key("panel_series_limit").MustInt(0) if err := readUserSettings(iniFile, cfg); err != nil { return err diff --git a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts index 81c3985f238..48da1b2aa8f 100644 --- a/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts +++ b/public/app/features/dashboard-scene/serialization/layoutSerializers/utils.ts @@ -67,6 +67,7 @@ export function buildVizPanel(panel: PanelKind, id?: number): VizPanel { displayMode: panel.spec.transparent ? 'transparent' : 'default', hoverHeader: !panel.spec.title && !timeOverrideShown, hoverHeaderOffset: 0, + seriesLimit: config.panelSeriesLimit, $data: createPanelDataProvider(panel), titleItems, $behaviors: [], @@ -106,6 +107,7 @@ export function buildLibraryPanel(panel: LibraryPanelKind, id?: number): VizPane const vizPanelState: VizPanelState = { key: getVizPanelKeyForPanelId(id ?? panel.spec.id), titleItems, + seriesLimit: config.panelSeriesLimit, $behaviors: [ new LibraryPanelBehavior({ uid: panel.spec.libraryPanel.uid, diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 0b7435c1fe9..d0418ba94d0 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -322,6 +322,7 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem { options: panel.options ?? {}, fieldConfig: panel.fieldConfig, pluginVersion: panel.pluginVersion, + seriesLimit: config.panelSeriesLimit, displayMode: panel.transparent ? 'transparent' : undefined, // To be replaced with it's own option persited option instead derived hoverHeader: !panel.title && !timeOverrideShown, diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index fff901adb67..e4e302dd652 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -328,6 +328,7 @@ export function getDefaultVizPanel(): VizPanel { return new VizPanel({ title: newPanelTitle, pluginId: defaultPluginId, + seriesLimit: config.panelSeriesLimit, titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })], hoverHeaderOffset: 0, $behaviors: [], From f5ad1ef69b7094cd8b986c08c66c95587d30a349 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Fri, 11 Apr 2025 06:09:52 -0600 Subject: [PATCH 36/37] K8s: Folders: Add v1 api (#103842) --- apps/dashboard/kinds/dashboard.cue | 12 +- .../pkg/migration/conversion/conversion.go | 90 +- .../migration/conversion/conversion_test.go | 14 +- go.mod | 2 +- pkg/api/folder_test.go | 4 +- pkg/apis/folder/{v0alpha1 => v1}/doc.go | 2 +- pkg/apis/folder/{v0alpha1 => v1}/register.go | 4 +- pkg/apis/folder/{v0alpha1 => v1}/types.go | 2 +- .../{v0alpha1 => v1}/zz_generated.deepcopy.go | 2 +- .../{v0alpha1 => v1}/zz_generated.defaults.go | 2 +- .../{v0alpha1 => v1}/zz_generated.openapi.go | 50 +- ...enerated.openapi_violation_exceptions.list | 4 +- .../datamigrations/to_unified_storage.go | 2 +- pkg/registry/apis/dashboard/large_test.go | 8 +- pkg/registry/apis/dashboard/legacy/migrate.go | 4 +- .../dashboard/legacy/sql_dashboards_test.go | 4 +- .../dashboard/legacysearcher/search_client.go | 12 +- pkg/registry/apis/dashboard/mutation_test.go | 16 +- pkg/registry/apis/dashboard/register.go | 64 +- pkg/registry/apis/dashboard/register_test.go | 40 +- pkg/registry/apis/dashboard/search.go | 6 +- pkg/registry/apis/folders/authorizer_test.go | 4 +- pkg/registry/apis/folders/conversions.go | 12 +- pkg/registry/apis/folders/folder_storage.go | 4 +- .../apis/folders/folder_storage_test.go | 4 +- pkg/registry/apis/folders/legacy_storage.go | 12 +- .../apis/folders/legacy_storage_test.go | 12 +- pkg/registry/apis/folders/register.go | 49 +- pkg/registry/apis/folders/register_test.go | 134 +- pkg/registry/apis/folders/sub_access.go | 8 +- pkg/registry/apis/folders/sub_count.go | 12 +- pkg/registry/apis/folders/sub_parent_test.go | 22 +- pkg/registry/apis/folders/sub_parents.go | 22 +- .../provisioning/controller/finalizers.go | 2 +- pkg/registry/apis/provisioning/register.go | 2 +- .../apis/provisioning/resources/client.go | 2 +- .../apis/provisioning/resources/folders.go | 6 +- .../apis/provisioning/resources/object.go | 2 +- .../provisioning/resources/parser_test.go | 2 +- .../apis/provisioning/resources/tree.go | 2 +- .../apiserver/standalone/runtime_test.go | 3 +- pkg/services/authn/authnimpl/service_test.go | 8 +- pkg/services/authz/rbac/store/folder_store.go | 4 +- pkg/services/authz/zanzana/common/info.go | 6 +- pkg/services/authz/zanzana/translations.go | 12 +- .../dashboards/service/client/client.go | 8 +- .../dashboards/service/client/client_test.go | 4 +- .../dashboards/service/dashboard_service.go | 34 +- .../service/dashboard_service_test.go | 26 +- .../dashboardversion/dashverimpl/dashver.go | 4 +- .../folder/folderimpl/conversions_test.go | 16 +- pkg/services/folder/folderimpl/folder.go | 8 +- .../folderimpl/folder_unifiedstorage.go | 14 +- .../folderimpl/folder_unifiedstorage_test.go | 46 +- .../folder/folderimpl/unifiedstore.go | 4 +- pkg/storage/legacysql/dualwrite/utils.go | 2 +- pkg/storage/legacysql/dualwrite/utils_test.go | 2 +- pkg/storage/unified/README.md | 2 +- pkg/storage/unified/apistore/fake_large.go | 4 +- pkg/storage/unified/apistore/go.mod | 1 - pkg/storage/unified/apistore/go.sum | 1 - pkg/storage/unified/apistore/prepare_test.go | 56 +- pkg/storage/unified/resource/go.mod | 1 - pkg/storage/unified/resource/go.sum | 2 - pkg/storage/unified/resource/search.go | 12 +- .../resource/testdata/folder-resource.json | 2 +- .../resource/testdata/folder-resource2.json | 2 +- pkg/storage/unified/search/dashboard.go | 4 +- .../search/testdata/doc/folder-aaa.json | 2 +- .../search/testdata/doc/folder-bbb.json | 2 +- .../integration/api_validation_test.go | 28 +- pkg/tests/apis/folder/folders_test.go | 30 +- .../apis/folder/testdata/folder-generate.yaml | 2 +- .../folder/testdata/folder-test-create.yaml | 2 +- .../folder/testdata/folder-test-replace.yaml | 2 +- .../folder.grafana.app-v1.json | 1822 +++++++++++++++++ pkg/tests/apis/openapi_test.go | 2 +- pkg/tests/apis/provisioning/helper_test.go | 2 +- 78 files changed, 2325 insertions(+), 514 deletions(-) rename pkg/apis/folder/{v0alpha1 => v1}/doc.go (59%) rename pkg/apis/folder/{v0alpha1 => v1}/register.go (96%) rename pkg/apis/folder/{v0alpha1 => v1}/types.go (99%) rename pkg/apis/folder/{v0alpha1 => v1}/zz_generated.deepcopy.go (99%) rename pkg/apis/folder/{v0alpha1 => v1}/zz_generated.defaults.go (96%) rename pkg/apis/folder/{v0alpha1 => v1}/zz_generated.openapi.go (83%) rename pkg/apis/folder/{v0alpha1 => v1}/zz_generated.openapi_violation_exceptions.list (56%) create mode 100644 pkg/tests/apis/openapi_snapshots/folder.grafana.app-v1.json diff --git a/apps/dashboard/kinds/dashboard.cue b/apps/dashboard/kinds/dashboard.cue index 2e56b8e2e22..5be84dcccf3 100644 --- a/apps/dashboard/kinds/dashboard.cue +++ b/apps/dashboard/kinds/dashboard.cue @@ -1,9 +1,9 @@ package kinds import ( - "github.com/grafana/grafana/sdkkinds/dashboard/v0alpha1" - "github.com/grafana/grafana/sdkkinds/dashboard/v1alpha1" - "github.com/grafana/grafana/sdkkinds/dashboard/v2alpha1" + v0 "github.com/grafana/grafana/sdkkinds/dashboard/v0alpha1" + v1 "github.com/grafana/grafana/sdkkinds/dashboard/v1alpha1" + v2 "github.com/grafana/grafana/sdkkinds/dashboard/v2alpha1" ) // Status is the shared status of all dashboard versions. @@ -51,19 +51,19 @@ dashboard: { versions: { "v0alpha1": { schema: { - spec: v0alpha1.DashboardSpec + spec: v0.DashboardSpec status: DashboardStatus } } "v1alpha1": { schema: { - spec: v1alpha1.DashboardSpec + spec: v1.DashboardSpec status: DashboardStatus } } "v2alpha1": { schema: { - spec: v2alpha1.DashboardSpec + spec: v2.DashboardSpec status: DashboardStatus } } diff --git a/apps/dashboard/pkg/migration/conversion/conversion.go b/apps/dashboard/pkg/migration/conversion/conversion.go index 7733659d606..cb11e024f85 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion.go +++ b/apps/dashboard/pkg/migration/conversion/conversion.go @@ -4,55 +4,55 @@ import ( "k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/runtime" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashv2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" "github.com/grafana/grafana/apps/dashboard/pkg/migration" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" ) func RegisterConversions(s *runtime.Scheme) error { - if err := s.AddConversionFunc((*v0alpha1.Dashboard)(nil), (*v1alpha1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V0_to_V1(a.(*v0alpha1.Dashboard), b.(*v1alpha1.Dashboard), scope) + if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V0_to_V1(a.(*dashv0.Dashboard), b.(*dashv1.Dashboard), scope) }); err != nil { return err } - if err := s.AddConversionFunc((*v0alpha1.Dashboard)(nil), (*v2alpha1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V0_to_V2(a.(*v0alpha1.Dashboard), b.(*v2alpha1.Dashboard), scope) + if err := s.AddConversionFunc((*dashv0.Dashboard)(nil), (*dashv2.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V0_to_V2(a.(*dashv0.Dashboard), b.(*dashv2.Dashboard), scope) }); err != nil { return err } - if err := s.AddConversionFunc((*v1alpha1.Dashboard)(nil), (*v0alpha1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V1_to_V0(a.(*v1alpha1.Dashboard), b.(*v0alpha1.Dashboard), scope) + if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv0.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V1_to_V0(a.(*dashv1.Dashboard), b.(*dashv0.Dashboard), scope) }); err != nil { return err } - if err := s.AddConversionFunc((*v1alpha1.Dashboard)(nil), (*v2alpha1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V1_to_V2(a.(*v1alpha1.Dashboard), b.(*v2alpha1.Dashboard), scope) + if err := s.AddConversionFunc((*dashv1.Dashboard)(nil), (*dashv2.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V1_to_V2(a.(*dashv1.Dashboard), b.(*dashv2.Dashboard), scope) }); err != nil { return err } - if err := s.AddConversionFunc((*v2alpha1.Dashboard)(nil), (*v0alpha1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2_to_V0(a.(*v2alpha1.Dashboard), b.(*v0alpha1.Dashboard), scope) + if err := s.AddConversionFunc((*dashv2.Dashboard)(nil), (*dashv0.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V2_to_V0(a.(*dashv2.Dashboard), b.(*dashv0.Dashboard), scope) }); err != nil { return err } - if err := s.AddConversionFunc((*v2alpha1.Dashboard)(nil), (*v1alpha1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_V2_to_V1(a.(*v2alpha1.Dashboard), b.(*v1alpha1.Dashboard), scope) + if err := s.AddConversionFunc((*dashv2.Dashboard)(nil), (*dashv1.Dashboard)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_V2_to_V1(a.(*dashv2.Dashboard), b.(*dashv1.Dashboard), scope) }); err != nil { return err } return nil } -func Convert_V0_to_V1(in *v0alpha1.Dashboard, out *v1alpha1.Dashboard, scope conversion.Scope) error { +func Convert_V0_to_V1(in *dashv0.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta out.Spec.Object = in.Spec.Object - out.Status = v1alpha1.DashboardStatus{ - Conversion: &v1alpha1.DashboardConversionStatus{ - StoredVersion: v0alpha1.VERSION, + out.Status = dashv1.DashboardStatus{ + Conversion: &dashv1.DashboardConversionStatus{ + StoredVersion: dashv0.VERSION, }, } @@ -64,7 +64,7 @@ func Convert_V0_to_V1(in *v0alpha1.Dashboard, out *v1alpha1.Dashboard, scope con return nil } -func Convert_V0_to_V2(in *v0alpha1.Dashboard, out *v2alpha1.Dashboard, scope conversion.Scope) error { +func Convert_V0_to_V2(in *dashv0.Dashboard, out *dashv2.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta // TODO (@radiohead): implement V0 to V2 conversion @@ -77,16 +77,16 @@ func Convert_V0_to_V2(in *v0alpha1.Dashboard, out *v2alpha1.Dashboard, scope con } // We need to make sure the layout is set to some value, otherwise the JSON marshaling will fail. - out.Spec.Layout = v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ - GridLayoutKind: &v2alpha1.DashboardGridLayoutKind{ + out.Spec.Layout = dashv2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2.DashboardGridLayoutKind{ Kind: "GridLayout", - Spec: v2alpha1.DashboardGridLayoutSpec{}, + Spec: dashv2.DashboardGridLayoutSpec{}, }, } - out.Status = v2alpha1.DashboardStatus{ - Conversion: &v2alpha1.DashboardConversionStatus{ - StoredVersion: v0alpha1.VERSION, + out.Status = dashv2.DashboardStatus{ + Conversion: &dashv2.DashboardConversionStatus{ + StoredVersion: dashv0.VERSION, Failed: true, Error: "backend conversion not yet implemented", }, @@ -95,21 +95,21 @@ func Convert_V0_to_V2(in *v0alpha1.Dashboard, out *v2alpha1.Dashboard, scope con return nil } -func Convert_V1_to_V0(in *v1alpha1.Dashboard, out *v0alpha1.Dashboard, scope conversion.Scope) error { +func Convert_V1_to_V0(in *dashv1.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta out.Spec.Object = in.Spec.Object - out.Status = v0alpha1.DashboardStatus{ - Conversion: &v0alpha1.DashboardConversionStatus{ - StoredVersion: v1alpha1.VERSION, + out.Status = dashv0.DashboardStatus{ + Conversion: &dashv0.DashboardConversionStatus{ + StoredVersion: dashv1.VERSION, }, } return nil } -func Convert_V1_to_V2(in *v1alpha1.Dashboard, out *v2alpha1.Dashboard, scope conversion.Scope) error { +func Convert_V1_to_V2(in *dashv1.Dashboard, out *dashv2.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta // TODO (@radiohead): implement V1 to V2 conversion @@ -122,16 +122,16 @@ func Convert_V1_to_V2(in *v1alpha1.Dashboard, out *v2alpha1.Dashboard, scope con } // We need to make sure the layout is set to some value, otherwise the JSON marshaling will fail. - out.Spec.Layout = v2alpha1.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ - GridLayoutKind: &v2alpha1.DashboardGridLayoutKind{ + out.Spec.Layout = dashv2.DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTabsLayoutKind{ + GridLayoutKind: &dashv2.DashboardGridLayoutKind{ Kind: "GridLayout", - Spec: v2alpha1.DashboardGridLayoutSpec{}, + Spec: dashv2.DashboardGridLayoutSpec{}, }, } - out.Status = v2alpha1.DashboardStatus{ - Conversion: &v2alpha1.DashboardConversionStatus{ - StoredVersion: v1alpha1.VERSION, + out.Status = dashv2.DashboardStatus{ + Conversion: &dashv2.DashboardConversionStatus{ + StoredVersion: dashv1.VERSION, Failed: true, Error: "backend conversion not yet implemented", }, @@ -140,14 +140,14 @@ func Convert_V1_to_V2(in *v1alpha1.Dashboard, out *v2alpha1.Dashboard, scope con return nil } -func Convert_V2_to_V0(in *v2alpha1.Dashboard, out *v0alpha1.Dashboard, scope conversion.Scope) error { +func Convert_V2_to_V0(in *dashv2.Dashboard, out *dashv0.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta // TODO: implement V2 to V0 conversion - out.Status = v0alpha1.DashboardStatus{ - Conversion: &v0alpha1.DashboardConversionStatus{ - StoredVersion: v2alpha1.VERSION, + out.Status = dashv0.DashboardStatus{ + Conversion: &dashv0.DashboardConversionStatus{ + StoredVersion: dashv2.VERSION, Failed: true, Error: "backend conversion not yet implemented", }, @@ -156,14 +156,14 @@ func Convert_V2_to_V0(in *v2alpha1.Dashboard, out *v0alpha1.Dashboard, scope con return nil } -func Convert_V2_to_V1(in *v2alpha1.Dashboard, out *v1alpha1.Dashboard, scope conversion.Scope) error { +func Convert_V2_to_V1(in *dashv2.Dashboard, out *dashv1.Dashboard, scope conversion.Scope) error { out.ObjectMeta = in.ObjectMeta // TODO: implement V2 to V1 conversion - out.Status = v1alpha1.DashboardStatus{ - Conversion: &v1alpha1.DashboardConversionStatus{ - StoredVersion: v2alpha1.VERSION, + out.Status = dashv1.DashboardStatus{ + Conversion: &dashv1.DashboardConversionStatus{ + StoredVersion: dashv2.VERSION, Failed: true, Error: "backend conversion not yet implemented", }, diff --git a/apps/dashboard/pkg/migration/conversion/conversion_test.go b/apps/dashboard/pkg/migration/conversion/conversion_test.go index c77afd78e1f..243745ce0ab 100644 --- a/apps/dashboard/pkg/migration/conversion/conversion_test.go +++ b/apps/dashboard/pkg/migration/conversion/conversion_test.go @@ -9,18 +9,18 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashv2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" ) func TestConversionMatrixExist(t *testing.T) { versions := []v1.Object{ - &v0alpha1.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV0"}}}, - &v1alpha1.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV1"}}}, - &v2alpha1.Dashboard{Spec: v2alpha1.DashboardSpec{Title: "dashboardV2"}}, + &dashv0.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV0"}}}, + &dashv1.Dashboard{Spec: common.Unstructured{Object: map[string]any{"title": "dashboardV1"}}}, + &dashv2.Dashboard{Spec: dashv2.DashboardSpec{Title: "dashboardV2"}}, } scheme := runtime.NewScheme() @@ -47,7 +47,7 @@ func TestConversionMatrixExist(t *testing.T) { } func TestDeepCopyValid(t *testing.T) { - dash1 := &v0alpha1.Dashboard{} + dash1 := &dashv0.Dashboard{} meta1, err := utils.MetaAccessor(dash1) require.NoError(t, err) meta1.SetFolder("f1") diff --git a/go.mod b/go.mod index 1ee5e09a7ea..c8bca87d601 100644 --- a/go.mod +++ b/go.mod @@ -215,7 +215,7 @@ require ( github.com/grafana/grafana/apps/playlist v0.0.0-20250220164708-c8d4ff28a450 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 // @grafana/grafana-app-platform-squad github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250401081501-6af5fbf3fff0 // @grafana/grafana-app-platform-squad - github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250402082028-6781612335d9 // @grafana/grafana-search-and-storage + github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250402082028-6781612335d9 // indirect; @grafana/grafana-search-and-storage github.com/grafana/grafana/pkg/apis/secret v0.0.0-20250319110241-5a004939da2a // @grafana/grafana-operator-experience-squad github.com/grafana/grafana/pkg/apiserver v0.0.0-20250325075903-77fa2271be7a // @grafana/grafana-app-platform-squad diff --git a/pkg/api/folder_test.go b/pkg/api/folder_test.go index 96e8b75a0ec..a8405044fcf 100644 --- a/pkg/api/folder_test.go +++ b/pkg/api/folder_test.go @@ -15,7 +15,7 @@ import ( clientrest "k8s.io/client-go/rest" "github.com/grafana/grafana/pkg/api/dtos" - folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" @@ -618,7 +618,7 @@ func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) { cfg := setting.NewCfg() cfg.UnifiedStorage = map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { + folders.RESOURCEGROUP: { DualWriterMode: tc.unifiedStorageMode, }, } diff --git a/pkg/apis/folder/v0alpha1/doc.go b/pkg/apis/folder/v1/doc.go similarity index 59% rename from pkg/apis/folder/v0alpha1/doc.go rename to pkg/apis/folder/v1/doc.go index fc85f689f0e..7a4e5ab7643 100644 --- a/pkg/apis/folder/v0alpha1/doc.go +++ b/pkg/apis/folder/v1/doc.go @@ -3,4 +3,4 @@ // +k8s:defaulter-gen=TypeMeta // +groupName=folder.grafana.app -package v0alpha1 // import "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" +package v1 // import "github.com/grafana/grafana/pkg/apis/folder/v1" diff --git a/pkg/apis/folder/v0alpha1/register.go b/pkg/apis/folder/v1/register.go similarity index 96% rename from pkg/apis/folder/v0alpha1/register.go rename to pkg/apis/folder/v1/register.go index 27f1e0ed670..ebfb2b6e122 100644 --- a/pkg/apis/folder/v0alpha1/register.go +++ b/pkg/apis/folder/v1/register.go @@ -1,4 +1,4 @@ -package v0alpha1 +package v1 import ( "fmt" @@ -12,7 +12,7 @@ import ( const ( GROUP = "folder.grafana.app" - VERSION = "v0alpha1" + VERSION = "v1" RESOURCE = "folders" APIVERSION = GROUP + "/" + VERSION RESOURCEGROUP = RESOURCE + "." + GROUP diff --git a/pkg/apis/folder/v0alpha1/types.go b/pkg/apis/folder/v1/types.go similarity index 99% rename from pkg/apis/folder/v0alpha1/types.go rename to pkg/apis/folder/v1/types.go index ceaaaf1233b..4a301680b80 100644 --- a/pkg/apis/folder/v0alpha1/types.go +++ b/pkg/apis/folder/v1/types.go @@ -1,4 +1,4 @@ -package v0alpha1 +package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/apis/folder/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/folder/v1/zz_generated.deepcopy.go similarity index 99% rename from pkg/apis/folder/v0alpha1/zz_generated.deepcopy.go rename to pkg/apis/folder/v1/zz_generated.deepcopy.go index 7a70c772be0..4e65160d2d3 100644 --- a/pkg/apis/folder/v0alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/folder/v1/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ // Code generated by deepcopy-gen. DO NOT EDIT. -package v0alpha1 +package v1 import ( runtime "k8s.io/apimachinery/pkg/runtime" diff --git a/pkg/apis/folder/v0alpha1/zz_generated.defaults.go b/pkg/apis/folder/v1/zz_generated.defaults.go similarity index 96% rename from pkg/apis/folder/v0alpha1/zz_generated.defaults.go rename to pkg/apis/folder/v1/zz_generated.defaults.go index 238fc2f4edc..e889bc10780 100644 --- a/pkg/apis/folder/v0alpha1/zz_generated.defaults.go +++ b/pkg/apis/folder/v1/zz_generated.defaults.go @@ -5,7 +5,7 @@ // Code generated by defaulter-gen. DO NOT EDIT. -package v0alpha1 +package v1 import ( runtime "k8s.io/apimachinery/pkg/runtime" diff --git a/pkg/apis/folder/v0alpha1/zz_generated.openapi.go b/pkg/apis/folder/v1/zz_generated.openapi.go similarity index 83% rename from pkg/apis/folder/v0alpha1/zz_generated.openapi.go rename to pkg/apis/folder/v1/zz_generated.openapi.go index c3c9f74bfc4..244b064854d 100644 --- a/pkg/apis/folder/v0alpha1/zz_generated.openapi.go +++ b/pkg/apis/folder/v1/zz_generated.openapi.go @@ -5,7 +5,7 @@ // Code generated by openapi-gen. DO NOT EDIT. -package v0alpha1 +package v1 import ( common "k8s.io/kube-openapi/pkg/common" @@ -14,18 +14,18 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1.DescendantCounts": schema_pkg_apis_folder_v0alpha1_DescendantCounts(ref), - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1.Folder": schema_pkg_apis_folder_v0alpha1_Folder(ref), - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1.FolderAccessInfo": schema_pkg_apis_folder_v0alpha1_FolderAccessInfo(ref), - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1.FolderInfo": schema_pkg_apis_folder_v0alpha1_FolderInfo(ref), - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1.FolderInfoList": schema_pkg_apis_folder_v0alpha1_FolderInfoList(ref), - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1.FolderList": schema_pkg_apis_folder_v0alpha1_FolderList(ref), - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1.ResourceStats": schema_pkg_apis_folder_v0alpha1_ResourceStats(ref), - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1.Spec": schema_pkg_apis_folder_v0alpha1_Spec(ref), + "github.com/grafana/grafana/pkg/apis/folder/v1.DescendantCounts": schema_pkg_apis_folder_v1_DescendantCounts(ref), + "github.com/grafana/grafana/pkg/apis/folder/v1.Folder": schema_pkg_apis_folder_v1_Folder(ref), + "github.com/grafana/grafana/pkg/apis/folder/v1.FolderAccessInfo": schema_pkg_apis_folder_v1_FolderAccessInfo(ref), + "github.com/grafana/grafana/pkg/apis/folder/v1.FolderInfo": schema_pkg_apis_folder_v1_FolderInfo(ref), + "github.com/grafana/grafana/pkg/apis/folder/v1.FolderInfoList": schema_pkg_apis_folder_v1_FolderInfoList(ref), + "github.com/grafana/grafana/pkg/apis/folder/v1.FolderList": schema_pkg_apis_folder_v1_FolderList(ref), + "github.com/grafana/grafana/pkg/apis/folder/v1.ResourceStats": schema_pkg_apis_folder_v1_ResourceStats(ref), + "github.com/grafana/grafana/pkg/apis/folder/v1.Spec": schema_pkg_apis_folder_v1_Spec(ref), } } -func schema_pkg_apis_folder_v0alpha1_DescendantCounts(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_folder_v1_DescendantCounts(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -52,7 +52,7 @@ func schema_pkg_apis_folder_v0alpha1_DescendantCounts(ref common.ReferenceCallba Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/folder/v0alpha1.ResourceStats"), + Ref: ref("github.com/grafana/grafana/pkg/apis/folder/v1.ResourceStats"), }, }, }, @@ -63,11 +63,11 @@ func schema_pkg_apis_folder_v0alpha1_DescendantCounts(ref common.ReferenceCallba }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1.ResourceStats"}, + "github.com/grafana/grafana/pkg/apis/folder/v1.ResourceStats"}, } } -func schema_pkg_apis_folder_v0alpha1_Folder(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_folder_v1_Folder(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -96,18 +96,18 @@ func schema_pkg_apis_folder_v0alpha1_Folder(ref common.ReferenceCallback) common "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/folder/v0alpha1.Spec"), + Ref: ref("github.com/grafana/grafana/pkg/apis/folder/v1.Spec"), }, }, }, }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1.Spec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + "github.com/grafana/grafana/pkg/apis/folder/v1.Spec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, } } -func schema_pkg_apis_folder_v0alpha1_FolderAccessInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_folder_v1_FolderAccessInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -163,7 +163,7 @@ func schema_pkg_apis_folder_v0alpha1_FolderAccessInfo(ref common.ReferenceCallba } } -func schema_pkg_apis_folder_v0alpha1_FolderInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_folder_v1_FolderInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -214,7 +214,7 @@ func schema_pkg_apis_folder_v0alpha1_FolderInfo(ref common.ReferenceCallback) co } } -func schema_pkg_apis_folder_v0alpha1_FolderInfoList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_folder_v1_FolderInfoList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -256,7 +256,7 @@ func schema_pkg_apis_folder_v0alpha1_FolderInfoList(ref common.ReferenceCallback Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/folder/v0alpha1.FolderInfo"), + Ref: ref("github.com/grafana/grafana/pkg/apis/folder/v1.FolderInfo"), }, }, }, @@ -266,11 +266,11 @@ func schema_pkg_apis_folder_v0alpha1_FolderInfoList(ref common.ReferenceCallback }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1.FolderInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/grafana/grafana/pkg/apis/folder/v1.FolderInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } -func schema_pkg_apis_folder_v0alpha1_FolderList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_folder_v1_FolderList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -303,7 +303,7 @@ func schema_pkg_apis_folder_v0alpha1_FolderList(ref common.ReferenceCallback) co Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/folder/v0alpha1.Folder"), + Ref: ref("github.com/grafana/grafana/pkg/apis/folder/v1.Folder"), }, }, }, @@ -313,11 +313,11 @@ func schema_pkg_apis_folder_v0alpha1_FolderList(ref common.ReferenceCallback) co }, }, Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1.Folder", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + "github.com/grafana/grafana/pkg/apis/folder/v1.Folder", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, } } -func schema_pkg_apis_folder_v0alpha1_ResourceStats(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_folder_v1_ResourceStats(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -351,7 +351,7 @@ func schema_pkg_apis_folder_v0alpha1_ResourceStats(ref common.ReferenceCallback) } } -func schema_pkg_apis_folder_v0alpha1_Spec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_pkg_apis_folder_v1_Spec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ diff --git a/pkg/apis/folder/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/folder/v1/zz_generated.openapi_violation_exceptions.list similarity index 56% rename from pkg/apis/folder/v0alpha1/zz_generated.openapi_violation_exceptions.list rename to pkg/apis/folder/v1/zz_generated.openapi_violation_exceptions.list index f738ebaa1eb..17876d3b1c3 100644 --- a/pkg/apis/folder/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/pkg/apis/folder/v1/zz_generated.openapi_violation_exceptions.list @@ -1,2 +1,2 @@ -API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/folder/v0alpha1,DescendantCounts,Counts -API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/folder/v0alpha1,FolderInfoList,Items +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/folder/v1,DescendantCounts,Counts +API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/folder/v1,FolderInfoList,Items diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go index 913e6173b68..0d0abdf95bf 100644 --- a/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go +++ b/pkg/cmd/grafana-cli/commands/datamigrations/to_unified_storage.go @@ -12,7 +12,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" authlib "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" diff --git a/pkg/registry/apis/dashboard/large_test.go b/pkg/registry/apis/dashboard/large_test.go index 92ca25ae144..58876d72f96 100644 --- a/pkg/registry/apis/dashboard/large_test.go +++ b/pkg/registry/apis/dashboard/large_test.go @@ -10,7 +10,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - dashboardv1alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" ) func TestLargeDashboardSupport(t *testing.T) { @@ -21,7 +21,7 @@ func TestLargeDashboardSupport(t *testing.T) { f, err := os.ReadFile(devdash) require.NoError(t, err) - dash := &dashboardv1alpha1.Dashboard{ + dash := &dashv1.Dashboard{ ObjectMeta: v1.ObjectMeta{ Name: "test", Namespace: "test", @@ -38,7 +38,7 @@ func TestLargeDashboardSupport(t *testing.T) { scheme := runtime.NewScheme() - err = dashboardv1alpha1.AddToScheme(scheme) + err = dashv1.AddToScheme(scheme) require.NoError(t, err) largeObject := NewDashboardLargeObjectSupport(scheme, 0) @@ -56,7 +56,7 @@ func TestLargeDashboardSupport(t *testing.T) { }`, string(small)) // Now make it big again - rehydratedDash := &dashboardv1alpha1.Dashboard{ + rehydratedDash := &dashv1.Dashboard{ ObjectMeta: v1.ObjectMeta{ Name: "test", Namespace: "test", diff --git a/pkg/registry/apis/dashboard/legacy/migrate.go b/pkg/registry/apis/dashboard/legacy/migrate.go index 26da721dada..67b7336900c 100644 --- a/pkg/registry/apis/dashboard/legacy/migrate.go +++ b/pkg/registry/apis/dashboard/legacy/migrate.go @@ -13,7 +13,7 @@ import ( authlib "github.com/grafana/authlib/types" dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" - folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/search/sort" @@ -326,7 +326,7 @@ func (a *dashboardSqlAccess) migrateFolders(ctx context.Context, orgId int64, op // Now send each dashboard for i := 1; rows.Next(); i++ { dash := rows.row.Dash - dash.APIVersion = "folder.grafana.app/v0alpha1" + dash.APIVersion = "folder.grafana.app/v1" dash.Kind = "Folder" dash.SetNamespace(opts.Namespace) dash.SetResourceVersion("") // it will be filled in by the backend diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go index fbb6d8f6b09..a3accabadad 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards_test.go @@ -132,7 +132,7 @@ func TestBuildSaveDashboardCommand(t *testing.T) { } dash := &dashboard.Dashboard{ TypeMeta: metav1.TypeMeta{ - APIVersion: "dashboard.grafana.app/v1alpha1", + APIVersion: dashboard.APIVERSION, }, ObjectMeta: metav1.ObjectMeta{ Name: "test-dash", @@ -170,7 +170,7 @@ func TestBuildSaveDashboardCommand(t *testing.T) { &dashboards.Dashboard{ ID: 1234, Version: 2, - APIVersion: "dashboard.grafana.app/v1alpha1", + APIVersion: dashboard.APIVERSION, }, nil).Once() cmd, created, err = access.buildSaveDashboardCommand(ctx, 1, dash) require.NoError(t, err) diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go index a347774097b..55d37301d7c 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -14,7 +14,7 @@ import ( dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/search/sort" @@ -92,7 +92,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour switch req.Options.Key.Resource { case dashboard.DASHBOARD_RESOURCE: queryType = searchstore.TypeDashboard - case folderv0alpha1.RESOURCE: + case folders.RESOURCE: queryType = searchstore.TypeFolder default: return nil, fmt.Errorf("bad type request") @@ -104,7 +104,7 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour if len(req.Federated) == 1 && ((req.Federated[0].Resource == dashboard.DASHBOARD_RESOURCE && queryType == searchstore.TypeFolder) || - (req.Federated[0].Resource == folderv0alpha1.RESOURCE && queryType == searchstore.TypeDashboard)) { + (req.Federated[0].Resource == folders.RESOURCE && queryType == searchstore.TypeDashboard)) { queryType = "" // makes the legacy store search across both } @@ -336,8 +336,8 @@ func getResourceKey(item *dashboards.DashboardSearchProjection, namespace string if item.IsFolder { return &resource.ResourceKey{ Namespace: namespace, - Group: folderv0alpha1.GROUP, - Resource: folderv0alpha1.RESOURCE, + Group: folders.GROUP, + Resource: folders.RESOURCE, Name: item.UID, } } @@ -401,7 +401,7 @@ func (c *DashboardSearchClient) GetStats(ctx context.Context, req *resource.Reso switch parts[0] { case dashboard.GROUP: count, err = c.dashboardStore.CountInOrg(ctx, info.OrgID, false) - case folderv0alpha1.GROUP: + case folders.GROUP: count, err = c.dashboardStore.CountInOrg(ctx, info.OrgID, true) default: return nil, fmt.Errorf("invalid group") diff --git a/pkg/registry/apis/dashboard/mutation_test.go b/pkg/registry/apis/dashboard/mutation_test.go index c37b495ba99..6e702edde48 100644 --- a/pkg/registry/apis/dashboard/mutation_test.go +++ b/pkg/registry/apis/dashboard/mutation_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -25,7 +25,7 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { }{ { name: "should skip non-create/update operations", - inputObj: &v1alpha1.Dashboard{ + inputObj: &dashv1.Dashboard{ Spec: common.Unstructured{ Object: map[string]interface{}{ "id": float64(123), @@ -37,7 +37,7 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { }, { name: "v0 should extract id and set as label", - inputObj: &v0alpha1.Dashboard{ + inputObj: &dashv0.Dashboard{ Spec: common.Unstructured{ Object: map[string]interface{}{ "id": float64(123), @@ -49,7 +49,7 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { }, { name: "v1 should migrate dashboard to the latest version, if possible, and set as label", - inputObj: &v1alpha1.Dashboard{ + inputObj: &dashv1.Dashboard{ Spec: common.Unstructured{ Object: map[string]interface{}{ "id": float64(456), @@ -63,7 +63,7 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { }, { name: "v1 should not error mutation hook if migration fails", - inputObj: &v1alpha1.Dashboard{ + inputObj: &dashv1.Dashboard{ Spec: common.Unstructured{ Object: map[string]interface{}{ "id": float64(456), @@ -100,10 +100,10 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) { require.Equal(t, tt.expectedID, meta.GetDeprecatedInternalID()) //nolint:staticcheck switch v := tt.inputObj.(type) { - case *v0alpha1.Dashboard: + case *dashv0.Dashboard: _, exists := v.Spec.Object["id"] require.False(t, exists, "id should be removed from spec") - case *v1alpha1.Dashboard: + case *dashv1.Dashboard: _, exists := v.Spec.Object["id"] require.False(t, exists, "id should be removed from spec") schemaVersion, ok := v.Spec.Object["schemaVersion"].(int) diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index f78b0b50b82..82a5ed934f1 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -20,9 +20,9 @@ import ( claims "github.com/grafana/authlib/types" internal "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashv2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" "github.com/grafana/grafana/apps/dashboard/pkg/migration/conversion" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" @@ -48,7 +48,7 @@ import ( "github.com/grafana/grafana/pkg/storage/unified/apistore" "github.com/grafana/grafana/pkg/storage/unified/resource" - folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/client" ) @@ -113,7 +113,7 @@ func RegisterAPIService( dbp := legacysql.NewDatabaseProvider(sql) namespacer := request.GetNamespaceMapper(cfg) legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter) - folderClient := client.NewK8sHandler(dual, request.GetNamespaceMapper(cfg), folderv0alpha1.FolderResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashStore, userService, unified, sorter) + folderClient := client.NewK8sHandler(dual, request.GetNamespaceMapper(cfg), folders.FolderResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashStore, userService, unified, sorter) builder := &DashboardsAPIBuilder{ log: log.New("grafana-apiserver.dashboards"), @@ -146,28 +146,28 @@ func (b *DashboardsAPIBuilder) GetGroupVersions() []schema.GroupVersion { if featuremgmt.AnyEnabled(b.features, featuremgmt.FlagDashboardNewLayouts) { // If dashboards v2 is enabled, we want to use v2alpha1 as the default API version. return []schema.GroupVersion{ - v2alpha1.DashboardResourceInfo.GroupVersion(), - v0alpha1.DashboardResourceInfo.GroupVersion(), - v1alpha1.DashboardResourceInfo.GroupVersion(), + dashv2.DashboardResourceInfo.GroupVersion(), + dashv0.DashboardResourceInfo.GroupVersion(), + dashv1.DashboardResourceInfo.GroupVersion(), } } return []schema.GroupVersion{ - v1alpha1.DashboardResourceInfo.GroupVersion(), - v0alpha1.DashboardResourceInfo.GroupVersion(), - v2alpha1.DashboardResourceInfo.GroupVersion(), + dashv1.DashboardResourceInfo.GroupVersion(), + dashv0.DashboardResourceInfo.GroupVersion(), + dashv2.DashboardResourceInfo.GroupVersion(), } } func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { b.scheme = scheme - if err := v0alpha1.AddToScheme(scheme); err != nil { + if err := dashv0.AddToScheme(scheme); err != nil { return err } - if err := v1alpha1.AddToScheme(scheme); err != nil { + if err := dashv1.AddToScheme(scheme); err != nil { return err } - if err := v2alpha1.AddToScheme(scheme); err != nil { + if err := dashv2.AddToScheme(scheme); err != nil { return err } @@ -370,13 +370,13 @@ func getDashboardProperties(obj runtime.Object) (string, string, error) { // Extract properties based on the object's type switch d := obj.(type) { - case *v0alpha1.Dashboard: + case *dashv0.Dashboard: title = d.Spec.GetNestedString(dashboardSpecTitle) refresh = d.Spec.GetNestedString(dashboardSpecRefreshInterval) - case *v1alpha1.Dashboard: + case *dashv1.Dashboard: title = d.Spec.GetNestedString(dashboardSpecTitle) refresh = d.Spec.GetNestedString(dashboardSpecRefreshInterval) - case *v2alpha1.Dashboard: + case *dashv2.Dashboard: title = d.Spec.Title refresh = d.Spec.TimeSettings.AutoRefresh default: @@ -401,15 +401,15 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver largeObjects = NewDashboardLargeObjectSupport(opts.Scheme, opts.StorageOpts.BlobThresholdBytes) storageOpts.LargeObjectSupport = largeObjects } - opts.StorageOptsRegister(v0alpha1.DashboardResourceInfo.GroupResource(), storageOpts) + opts.StorageOptsRegister(dashv0.DashboardResourceInfo.GroupResource(), storageOpts) // v0alpha1 if err := b.storageForVersion(apiGroupInfo, opts, largeObjects, - v0alpha1.DashboardResourceInfo, - &v0alpha1.LibraryPanelResourceInfo, + dashv0.DashboardResourceInfo, + &dashv0.LibraryPanelResourceInfo, func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) { - dto := &v0alpha1.DashboardWithAccessInfo{} - dash, ok := obj.(*v0alpha1.Dashboard) + dto := &dashv0.DashboardWithAccessInfo{} + dash, ok := obj.(*dashv0.Dashboard) if ok { dto.Dashboard = *dash } @@ -423,11 +423,11 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver // v1alpha1 if err := b.storageForVersion(apiGroupInfo, opts, largeObjects, - v1alpha1.DashboardResourceInfo, + dashv1.DashboardResourceInfo, nil, // do not register library panel func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) { - dto := &v1alpha1.DashboardWithAccessInfo{} - dash, ok := obj.(*v1alpha1.Dashboard) + dto := &dashv1.DashboardWithAccessInfo{} + dash, ok := obj.(*dashv1.Dashboard) if ok { dto.Dashboard = *dash } @@ -441,11 +441,11 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver // v2alpha1 if err := b.storageForVersion(apiGroupInfo, opts, largeObjects, - v2alpha1.DashboardResourceInfo, + dashv2.DashboardResourceInfo, nil, // do not register library panel func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) { - dto := &v2alpha1.DashboardWithAccessInfo{} - dash, ok := obj.(*v2alpha1.Dashboard) + dto := &dashv2.DashboardWithAccessInfo{} + dash, ok := obj.(*dashv2.Dashboard) if ok { dto.Dashboard = *dash } @@ -515,9 +515,9 @@ func (b *DashboardsAPIBuilder) storageForVersion( func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { - defs := v0alpha1.GetOpenAPIDefinitions(ref) - maps.Copy(defs, v1alpha1.GetOpenAPIDefinitions(ref)) - maps.Copy(defs, v2alpha1.GetOpenAPIDefinitions(ref)) + defs := dashv0.GetOpenAPIDefinitions(ref) + maps.Copy(defs, dashv1.GetOpenAPIDefinitions(ref)) + maps.Copy(defs, dashv2.GetOpenAPIDefinitions(ref)) return defs } } @@ -528,7 +528,7 @@ func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op } func (b *DashboardsAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes { - if gv.Version != v0alpha1.VERSION { + if gv.Version != dashv0.VERSION { return nil // Only show the custom routes for v0 } diff --git a/pkg/registry/apis/dashboard/register_test.go b/pkg/registry/apis/dashboard/register_test.go index 2bea09b35f9..b149515e0ad 100644 --- a/pkg/registry/apis/dashboard/register_test.go +++ b/pkg/registry/apis/dashboard/register_test.go @@ -5,9 +5,9 @@ import ( "fmt" "testing" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashv2 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -24,7 +24,7 @@ func TestDashboardAPIBuilder_Validate(t *testing.T) { zeroInt64 := int64(0) tests := []struct { name string - inputObj *v1alpha1.Dashboard + inputObj *dashv1.Dashboard deletionOptions metav1.DeleteOptions dashboardResponse *dashboards.DashboardProvisioning dashboardErrorResponse error @@ -33,7 +33,7 @@ func TestDashboardAPIBuilder_Validate(t *testing.T) { }{ { name: "should return an error if data is found", - inputObj: &v1alpha1.Dashboard{ + inputObj: &dashv1.Dashboard{ Spec: common.Unstructured{}, TypeMeta: metav1.TypeMeta{ Kind: "Dashboard", @@ -52,7 +52,7 @@ func TestDashboardAPIBuilder_Validate(t *testing.T) { }, { name: "should return an error if unable to check", - inputObj: &v1alpha1.Dashboard{ + inputObj: &dashv1.Dashboard{ Spec: common.Unstructured{}, TypeMeta: metav1.TypeMeta{ Kind: "Dashboard", @@ -71,7 +71,7 @@ func TestDashboardAPIBuilder_Validate(t *testing.T) { }, { name: "should be okay if error is provisioned dashboard not found", - inputObj: &v1alpha1.Dashboard{ + inputObj: &dashv1.Dashboard{ Spec: common.Unstructured{}, TypeMeta: metav1.TypeMeta{ Kind: "Dashboard", @@ -90,7 +90,7 @@ func TestDashboardAPIBuilder_Validate(t *testing.T) { }, { name: "Should still run the check for delete if grace period is not 0", - inputObj: &v1alpha1.Dashboard{ + inputObj: &dashv1.Dashboard{ Spec: common.Unstructured{}, TypeMeta: metav1.TypeMeta{ Kind: "Dashboard", @@ -109,7 +109,7 @@ func TestDashboardAPIBuilder_Validate(t *testing.T) { }, { name: "should not run the check for delete if grace period is set to 0", - inputObj: &v1alpha1.Dashboard{ + inputObj: &dashv1.Dashboard{ Spec: common.Unstructured{}, TypeMeta: metav1.TypeMeta{ Kind: "Dashboard", @@ -137,10 +137,10 @@ func TestDashboardAPIBuilder_Validate(t *testing.T) { err := b.Validate(context.Background(), admission.NewAttributesRecord( tt.inputObj, nil, - v1alpha1.DashboardResourceInfo.GroupVersionKind(), + dashv1.DashboardResourceInfo.GroupVersionKind(), "stacks-123", tt.inputObj.Name, - v1alpha1.DashboardResourceInfo.GroupVersionResource(), + dashv1.DashboardResourceInfo.GroupVersionResource(), "", admission.Operation("DELETE"), &tt.deletionOptions, @@ -173,9 +173,9 @@ func TestDashboardAPIBuilder_GetGroupVersions(t *testing.T) { name: "should return v1alpha1 by default", enabledFeatures: []string{}, expected: []schema.GroupVersion{ - v1alpha1.DashboardResourceInfo.GroupVersion(), - v0alpha1.DashboardResourceInfo.GroupVersion(), - v2alpha1.DashboardResourceInfo.GroupVersion(), + dashv1.DashboardResourceInfo.GroupVersion(), + dashv0.DashboardResourceInfo.GroupVersion(), + dashv2.DashboardResourceInfo.GroupVersion(), }, }, { @@ -184,9 +184,9 @@ func TestDashboardAPIBuilder_GetGroupVersions(t *testing.T) { featuremgmt.FlagKubernetesDashboards, }, expected: []schema.GroupVersion{ - v1alpha1.DashboardResourceInfo.GroupVersion(), - v0alpha1.DashboardResourceInfo.GroupVersion(), - v2alpha1.DashboardResourceInfo.GroupVersion(), + dashv1.DashboardResourceInfo.GroupVersion(), + dashv0.DashboardResourceInfo.GroupVersion(), + dashv2.DashboardResourceInfo.GroupVersion(), }, }, { @@ -195,9 +195,9 @@ func TestDashboardAPIBuilder_GetGroupVersions(t *testing.T) { featuremgmt.FlagDashboardNewLayouts, }, expected: []schema.GroupVersion{ - v2alpha1.DashboardResourceInfo.GroupVersion(), - v0alpha1.DashboardResourceInfo.GroupVersion(), - v1alpha1.DashboardResourceInfo.GroupVersion(), + dashv2.DashboardResourceInfo.GroupVersion(), + dashv0.DashboardResourceInfo.GroupVersion(), + dashv1.DashboardResourceInfo.GroupVersion(), }, }, } diff --git a/pkg/registry/apis/dashboard/search.go b/pkg/registry/apis/dashboard/search.go index 86967a9f5d3..4e286491ce1 100644 --- a/pkg/registry/apis/dashboard/search.go +++ b/pkg/registry/apis/dashboard/search.go @@ -23,7 +23,7 @@ import ( dashboardv0alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" - folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/dashboards" @@ -265,7 +265,7 @@ func (s *SearchHandler) DoSearch(w http.ResponseWriter, r *http.Request) { searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), dashboardv0alpha1.DASHBOARD_RESOURCE) // Currently a search query is across folders and dashboards if err == nil { - federate, err = asResourceKey(user.GetNamespace(), folderv0alpha1.RESOURCE) + federate, err = asResourceKey(user.GetNamespace(), folders.RESOURCE) } case 1: searchRequest.Options.Key, err = asResourceKey(user.GetNamespace(), types[0]) @@ -473,7 +473,7 @@ func (s *SearchHandler) getDashboardsUIDsSharedWithUser(ctx context.Context, use } // only folders the user has access to will be returned here - folderKey, err := asResourceKey(user.GetNamespace(), folderv0alpha1.RESOURCE) + folderKey, err := asResourceKey(user.GetNamespace(), folders.RESOURCE) if err != nil { return sharedDashboards, err } diff --git a/pkg/registry/apis/folders/authorizer_test.go b/pkg/registry/apis/folders/authorizer_test.go index eced4071112..100afc9da85 100644 --- a/pkg/registry/apis/folders/authorizer_test.go +++ b/pkg/registry/apis/folders/authorizer_test.go @@ -13,7 +13,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - folderv0aplha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -261,7 +261,7 @@ func TestMultiTenantAuthorizer(t *testing.T) { authz := newMultiTenantAuthorizer(tt.input.client) authorized, _, err := authz.Authorize( types.WithAuthInfo(context.Background(), tt.input.info), - authorizer.AttributesRecord{User: tt.input.info, Verb: tt.input.verb, APIGroup: folderv0aplha1.GROUP, Resource: "folders", ResourceRequest: true, Name: "123", Namespace: "stacks-1"}, + authorizer.AttributesRecord{User: tt.input.info, Verb: tt.input.verb, APIGroup: folders.GROUP, Resource: "folders", ResourceRequest: true, Name: "123", Namespace: "stacks-1"}, ) if tt.expeted.err { diff --git a/pkg/registry/apis/folders/conversions.go b/pkg/registry/apis/folders/conversions.go index 3159e9acf68..174683b3b60 100644 --- a/pkg/registry/apis/folders/conversions.go +++ b/pkg/registry/apis/folders/conversions.go @@ -9,7 +9,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/folder" @@ -40,20 +40,20 @@ func LegacyCreateCommandToUnstructured(cmd *folder.CreateFolderCommand) (*unstru return obj, nil } -func LegacyFolderToUnstructured(v *folder.Folder, namespacer request.NamespaceMapper) (*v0alpha1.Folder, error) { +func LegacyFolderToUnstructured(v *folder.Folder, namespacer request.NamespaceMapper) (*folders.Folder, error) { return convertToK8sResource(v, namespacer) } -func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) (*v0alpha1.Folder, error) { - f := &v0alpha1.Folder{ - TypeMeta: v0alpha1.FolderResourceInfo.TypeMeta(), +func convertToK8sResource(v *folder.Folder, namespacer request.NamespaceMapper) (*folders.Folder, error) { + f := &folders.Folder{ + TypeMeta: folders.FolderResourceInfo.TypeMeta(), ObjectMeta: metav1.ObjectMeta{ Name: v.UID, ResourceVersion: fmt.Sprintf("%d", v.Updated.UnixMilli()), CreationTimestamp: metav1.NewTime(v.Created), Namespace: namespacer(v.OrgID), }, - Spec: v0alpha1.Spec{ + Spec: folders.Spec{ Title: v.Title, Description: v.Description, }, diff --git a/pkg/registry/apis/folders/folder_storage.go b/pkg/registry/apis/folders/folder_storage.go index 4bc271c67b0..9b1f1c43da5 100644 --- a/pkg/registry/apis/folders/folder_storage.go +++ b/pkg/registry/apis/folders/folder_storage.go @@ -12,7 +12,7 @@ import ( claims "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -91,7 +91,7 @@ func (s *folderStorage) Create(ctx context.Context, return nil, err } - p, ok := obj.(*v0alpha1.Folder) + p, ok := obj.(*folders.Folder) if !ok { return nil, fmt.Errorf("expected folder?") } diff --git a/pkg/registry/apis/folders/folder_storage_test.go b/pkg/registry/apis/folders/folder_storage_test.go index 31cc755b6f3..68c2a808902 100644 --- a/pkg/registry/apis/folders/folder_storage_test.go +++ b/pkg/registry/apis/folders/folder_storage_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/services/accesscontrol" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/user" @@ -50,7 +50,7 @@ func TestSetDefaultPermissionsWhenCreatingFolder(t *testing.T) { store: &fakeStorage{}, cfg: cfg, } - obj := &v0alpha1.Folder{} + obj := &folders.Folder{} ctx := request.WithNamespace(context.Background(), "org-2") ctx = identity.WithRequester(ctx, &user.SignedInUser{ diff --git a/pkg/registry/apis/folders/legacy_storage.go b/pkg/registry/apis/folders/legacy_storage.go index 8c350cacc67..155e2d2d5a7 100644 --- a/pkg/registry/apis/folders/legacy_storage.go +++ b/pkg/registry/apis/folders/legacy_storage.go @@ -14,7 +14,7 @@ import ( "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -116,7 +116,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO return nil, err } - list := &v0alpha1.FolderList{} + list := &folders.FolderList{} for _, v := range hits { r, err := convertToK8sResource(v, s.namespacer) if err != nil { @@ -178,7 +178,7 @@ func (s *legacyStorage) Create(ctx context.Context, return nil, err } - p, ok := obj.(*v0alpha1.Folder) + p, ok := obj.(*folders.Folder) if !ok { return nil, fmt.Errorf("expected folder?") } @@ -248,11 +248,11 @@ func (s *legacyStorage) Update(ctx context.Context, if err != nil { return oldObj, created, err } - f, ok := obj.(*v0alpha1.Folder) + f, ok := obj.(*folders.Folder) if !ok { return nil, created, fmt.Errorf("expected folder after update") } - old, ok := oldObj.(*v0alpha1.Folder) + old, ok := oldObj.(*folders.Folder) if !ok { return nil, created, fmt.Errorf("expected old object to be a folder also") } @@ -313,7 +313,7 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio if err != nil { return nil, false, err } - p, ok := v.(*v0alpha1.Folder) + p, ok := v.(*folders.Folder) if !ok { return v, false, fmt.Errorf("expected a folder response from Get") } diff --git a/pkg/registry/apis/folders/legacy_storage_test.go b/pkg/registry/apis/folders/legacy_storage_test.go index ad877424e36..ee77264ca9c 100644 --- a/pkg/registry/apis/folders/legacy_storage_test.go +++ b/pkg/registry/apis/folders/legacy_storage_test.go @@ -14,7 +14,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folderv1 "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" "github.com/grafana/grafana/pkg/services/org" @@ -57,7 +57,7 @@ func TestLegacyStorageList(t *testing.T) { uidsReturnedByList := []string{} for _, obj := range list { - f, ok := obj.(*v0alpha1.Folder) + f, ok := obj.(*folderv1.Folder) require.Equal(t, true, ok) uidsReturnedByList = append(uidsReturnedByList, f.Name) } @@ -89,7 +89,7 @@ func TestLegacyStorage_List_Pagination(t *testing.T) { result, err := storage.List(ctx, options) require.NoError(t, err) - list, ok := result.(*v0alpha1.FolderList) + list, ok := result.(*folderv1.FolderList) require.True(t, ok) token, err := base64.StdEncoding.DecodeString(list.Continue) require.NoError(t, err) @@ -113,7 +113,7 @@ func TestLegacyStorage_List_Pagination(t *testing.T) { result, err := storage.List(ctx, options) require.NoError(t, err) - list, ok := result.(*v0alpha1.FolderList) + list, ok := result.(*folderv1.FolderList) require.True(t, ok) token, err := base64.StdEncoding.DecodeString(list.Continue) require.NoError(t, err) @@ -152,7 +152,7 @@ func TestLegacyStorage_List_LabelSelector(t *testing.T) { require.False(t, folderService.LastQuery.WithFullpath) require.False(t, folderService.LastQuery.WithFullpathUIDs) - list, ok := result.(*v0alpha1.FolderList) + list, ok := result.(*folderv1.FolderList) require.True(t, ok) require.Len(t, list.Items, 1) }) @@ -181,7 +181,7 @@ func TestLegacyStorage_List_LabelSelector(t *testing.T) { require.True(t, folderService.LastQuery.WithFullpath) require.True(t, folderService.LastQuery.WithFullpathUIDs) - list, ok := result.(*v0alpha1.FolderList) + list, ok := result.(*folderv1.FolderList) require.True(t, ok) require.Len(t, list.Items, 1) diff --git a/pkg/registry/apis/folders/register.go b/pkg/registry/apis/folders/register.go index 9692dfcbd25..fd1e717b03d 100644 --- a/pkg/registry/apis/folders/register.go +++ b/pkg/registry/apis/folders/register.go @@ -20,7 +20,7 @@ import ( authtypes "github.com/grafana/authlib/types" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -37,7 +37,7 @@ import ( var _ builder.APIGroupBuilder = (*FolderAPIBuilder)(nil) var _ builder.APIGroupValidation = (*FolderAPIBuilder)(nil) -var resourceInfo = v0alpha1.FolderResourceInfo +var resourceInfo = folders.FolderResourceInfo var errNoUser = errors.New("valid user is required") var errNoResource = errors.New("resource name is required") @@ -67,13 +67,6 @@ func RegisterAPIService(cfg *setting.Cfg, registerer prometheus.Registerer, unified resource.ResourceClient, ) *FolderAPIBuilder { - if !featuremgmt.AnyEnabled(features, - featuremgmt.FlagKubernetesClientDashboardsFolders, - featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, - featuremgmt.FlagProvisioning) { - return nil // skip registration unless opting into Kubernetes folders or unless we want to customize registration when testing - } - builder := &FolderAPIBuilder{ gv: resourceInfo.GroupVersion(), features: features, @@ -103,11 +96,11 @@ func (b *FolderAPIBuilder) GetGroupVersion() schema.GroupVersion { func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { scheme.AddKnownTypes(gv, - &v0alpha1.Folder{}, - &v0alpha1.FolderList{}, - &v0alpha1.FolderInfoList{}, - &v0alpha1.DescendantCounts{}, - &v0alpha1.FolderAccessInfo{}, + &folders.Folder{}, + &folders.FolderList{}, + &folders.FolderInfoList{}, + &folders.DescendantCounts{}, + &folders.FolderAccessInfo{}, ) } @@ -142,7 +135,7 @@ func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.API return err } storage[resourceInfo.StoragePath()] = store - apiGroupInfo.VersionedResourcesStorageMap[v0alpha1.VERSION] = storage + apiGroupInfo.VersionedResourcesStorageMap[folders.VERSION] = storage b.storage = storage[resourceInfo.StoragePath()].(grafanarest.Storage) return nil } @@ -187,13 +180,13 @@ func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.API storage[resourceInfo.StoragePath("counts")] = &subCountREST{searcher: b.searcher} storage[resourceInfo.StoragePath("access")] = &subAccessREST{b.folderSvc} - apiGroupInfo.VersionedResourcesStorageMap[v0alpha1.VERSION] = storage + apiGroupInfo.VersionedResourcesStorageMap[folders.VERSION] = storage b.storage = storage[resourceInfo.StoragePath()].(grafanarest.Storage) return nil } func (b *FolderAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - return v0alpha1.GetOpenAPIDefinitions + return folders.GetOpenAPIDefinitions } func (b *FolderAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { @@ -222,9 +215,9 @@ func (b *FolderAPIBuilder) Mutate(ctx context.Context, a admission.Attributes, _ verb := a.GetOperation() if verb == admission.Create || verb == admission.Update { obj := a.GetObject() - f, ok := obj.(*v0alpha1.Folder) + f, ok := obj.(*folders.Folder) if !ok { - return fmt.Errorf("obj is not v0alpha1.Folder") + return fmt.Errorf("obj is not folders.Folder") } f.Spec.Title = strings.Trim(f.Spec.Title, "") return nil @@ -239,9 +232,9 @@ func (b *FolderAPIBuilder) Validate(ctx context.Context, a admission.Attributes, return nil // This is normal for sub-resource } - f, ok := obj.(*v0alpha1.Folder) + f, ok := obj.(*folders.Folder) if !ok { - return fmt.Errorf("obj is not v0alpha1.Folder") + return fmt.Errorf("obj is not folders.Folder") } verb := a.GetOperation() @@ -262,7 +255,7 @@ func (b *FolderAPIBuilder) Validate(ctx context.Context, a admission.Attributes, return nil } -func (b *FolderAPIBuilder) validateOnDelete(ctx context.Context, f *v0alpha1.Folder) error { +func (b *FolderAPIBuilder) validateOnDelete(ctx context.Context, f *folders.Folder) error { resp, err := b.searcher.GetStats(ctx, &resource.ResourceStatsRequest{Namespace: f.Namespace, Folder: f.Name}) if err != nil { return err @@ -292,9 +285,9 @@ func (b *FolderAPIBuilder) validateOnCreate(ctx context.Context, id string, obj } } - f, ok := obj.(*v0alpha1.Folder) + f, ok := obj.(*folders.Folder) if !ok { - return fmt.Errorf("obj is not v0alpha1.Folder") + return fmt.Errorf("obj is not folders.Folder") } if f.Spec.Title == "" { return dashboards.ErrFolderTitleEmpty @@ -342,14 +335,14 @@ func (b *FolderAPIBuilder) checkFolderMaxDepth(ctx context.Context, obj runtime. } func (b *FolderAPIBuilder) validateOnUpdate(ctx context.Context, obj, old runtime.Object) error { - f, ok := obj.(*v0alpha1.Folder) + f, ok := obj.(*folders.Folder) if !ok { - return fmt.Errorf("obj is not v0alpha1.Folder") + return fmt.Errorf("obj is not folders.Folder") } - fOld, ok := old.(*v0alpha1.Folder) + fOld, ok := old.(*folders.Folder) if !ok { - return fmt.Errorf("obj is not v0alpha1.Folder") + return fmt.Errorf("obj is not folders.Folder") } var newParent = getParent(obj) if newParent != getParent(fOld) { diff --git a/pkg/registry/apis/folders/register_test.go b/pkg/registry/apis/folders/register_test.go index 17ba1f542e1..5c8f90e7e1a 100644 --- a/pkg/registry/apis/folders/register_test.go +++ b/pkg/registry/apis/folders/register_test.go @@ -10,7 +10,7 @@ import ( "k8s.io/apiserver/pkg/admission" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" @@ -22,7 +22,7 @@ import ( func TestFolderAPIBuilder_Validate_Create(t *testing.T) { type input struct { - obj *v0alpha1.Folder + obj *folders.Folder annotations map[string]string name string } @@ -30,15 +30,15 @@ func TestFolderAPIBuilder_Validate_Create(t *testing.T) { initialMaxDepth := folderValidationRules.maxDepth folderValidationRules.maxDepth = 2 defer func() { folderValidationRules.maxDepth = initialMaxDepth }() - deepFolder := &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + deepFolder := &folders.Folder{ + Spec: folders.Spec{ Title: "foo", }, } deepFolder.Name = "valid-parent" deepFolder.Annotations = map[string]string{"grafana.app/folder": "valid-grandparent"} - parentFolder := &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + parentFolder := &folders.Folder{ + Spec: folders.Spec{ Title: "foo-grandparent", }, } @@ -53,8 +53,8 @@ func TestFolderAPIBuilder_Validate_Create(t *testing.T) { { name: "should return error when name is invalid", input: input{ - obj: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + obj: &folders.Folder{ + Spec: folders.Spec{ Title: "foo", }, }, @@ -65,8 +65,8 @@ func TestFolderAPIBuilder_Validate_Create(t *testing.T) { { name: "should return no error if every validation passes", input: input{ - obj: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + obj: &folders.Folder{ + Spec: folders.Spec{ Title: "foo", }, }, @@ -76,8 +76,8 @@ func TestFolderAPIBuilder_Validate_Create(t *testing.T) { { name: "should not allow creating a folder in a tree that is too deep", input: input{ - obj: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + obj: &folders.Folder{ + Spec: folders.Spec{ Title: "foo", }, }, @@ -97,8 +97,8 @@ func TestFolderAPIBuilder_Validate_Create(t *testing.T) { { name: "should return error when title is empty", input: input{ - obj: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + obj: &folders.Folder{ + Spec: folders.Spec{ Title: "", }, }, @@ -110,8 +110,8 @@ func TestFolderAPIBuilder_Validate_Create(t *testing.T) { name: "should return error if folder is a parent of itself", input: input{ annotations: map[string]string{utils.AnnoKeyFolder: "myself"}, - obj: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + obj: &folders.Folder{ + Spec: folders.Spec{ Title: "title", }, }, @@ -145,10 +145,10 @@ func TestFolderAPIBuilder_Validate_Create(t *testing.T) { err := b.Validate(context.Background(), admission.NewAttributesRecord( tt.input.obj, nil, - v0alpha1.SchemeGroupVersion.WithKind("folder"), + folders.SchemeGroupVersion.WithKind("folder"), "stacks-123", tt.input.name, - v0alpha1.SchemeGroupVersion.WithResource("folders"), + folders.SchemeGroupVersion.WithResource("folders"), "", "CREATE", nil, @@ -188,8 +188,8 @@ func TestFolderAPIBuilder_Validate_Delete(t *testing.T) { us := storageMock{m, s} sm := searcherMock{Mock: m} - obj := &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + obj := &folders.Folder{ + Spec: folders.Spec{ Title: "foo", }, ObjectMeta: metav1.ObjectMeta{ @@ -221,10 +221,10 @@ func TestFolderAPIBuilder_Validate_Delete(t *testing.T) { err := b.Validate(context.Background(), admission.NewAttributesRecord( obj, nil, - v0alpha1.SchemeGroupVersion.WithKind("folder"), + folders.SchemeGroupVersion.WithKind("folder"), obj.Namespace, obj.Name, - v0alpha1.SchemeGroupVersion.WithResource("folders"), + folders.SchemeGroupVersion.WithResource("folders"), "", "DELETE", nil, @@ -243,7 +243,7 @@ func TestFolderAPIBuilder_Validate_Delete(t *testing.T) { } func TestFolderAPIBuilder_Validate_Update(t *testing.T) { - var circularObj = &v0alpha1.Folder{ + var circularObj = &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Namespace: "stacks-123", Name: "new-parent", @@ -253,15 +253,15 @@ func TestFolderAPIBuilder_Validate_Update(t *testing.T) { tests := []struct { name string - updatedObj *v0alpha1.Folder - expected *v0alpha1.Folder + updatedObj *folders.Folder + expected *folders.Folder setupFn func(*mock.Mock) wantErr bool }{ { name: "should allow updating a folder spec", - updatedObj: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + updatedObj: &folders.Folder{ + Spec: folders.Spec{ Title: "different title", }, ObjectMeta: metav1.ObjectMeta{ @@ -270,8 +270,8 @@ func TestFolderAPIBuilder_Validate_Update(t *testing.T) { Annotations: map[string]string{"grafana.app/folder": "valid-parent"}, }, }, - expected: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + expected: &folders.Folder{ + Spec: folders.Spec{ Title: "different title", }, ObjectMeta: metav1.ObjectMeta{ @@ -283,8 +283,8 @@ func TestFolderAPIBuilder_Validate_Update(t *testing.T) { }, { name: "updated title should not be empty", - updatedObj: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + updatedObj: &folders.Folder{ + Spec: folders.Spec{ Title: "", }, ObjectMeta: metav1.ObjectMeta{ @@ -297,8 +297,8 @@ func TestFolderAPIBuilder_Validate_Update(t *testing.T) { }, { name: "should allow moving to a valid parent", - updatedObj: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + updatedObj: &folders.Folder{ + Spec: folders.Spec{ Title: "foo", }, ObjectMeta: metav1.ObjectMeta{ @@ -309,14 +309,14 @@ func TestFolderAPIBuilder_Validate_Update(t *testing.T) { }, setupFn: func(m *mock.Mock) { m.On("Get", mock.Anything, "new-parent", mock.Anything).Return( - &v0alpha1.Folder{}, + &folders.Folder{}, nil).Once() }, }, { name: "should not allow moving to a k6 folder", - updatedObj: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + updatedObj: &folders.Folder{ + Spec: folders.Spec{ Title: "foo", }, ObjectMeta: metav1.ObjectMeta{ @@ -327,15 +327,15 @@ func TestFolderAPIBuilder_Validate_Update(t *testing.T) { }, setupFn: func(m *mock.Mock) { m.On("Get", mock.Anything, accesscontrol.K6FolderUID, mock.Anything).Return( - &v0alpha1.Folder{}, + &folders.Folder{}, nil).Once() }, wantErr: true, }, { name: "should not allow moving to a folder that is too deep", - updatedObj: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + updatedObj: &folders.Folder{ + Spec: folders.Spec{ Title: "foo", }, ObjectMeta: metav1.ObjectMeta{ @@ -358,8 +358,8 @@ func TestFolderAPIBuilder_Validate_Update(t *testing.T) { us := storageMock{m, s} sm := searcherMock{Mock: m} - obj := &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + obj := &folders.Folder{ + Spec: folders.Spec{ Title: "foo", }, ObjectMeta: metav1.ObjectMeta{ @@ -386,10 +386,10 @@ func TestFolderAPIBuilder_Validate_Update(t *testing.T) { err := b.Validate(context.Background(), admission.NewAttributesRecord( tt.updatedObj, obj, - v0alpha1.SchemeGroupVersion.WithKind("folder"), + folders.SchemeGroupVersion.WithKind("folder"), tt.updatedObj.Namespace, tt.updatedObj.Name, - v0alpha1.SchemeGroupVersion.WithResource("folders"), + folders.SchemeGroupVersion.WithResource("folders"), "", "UPDATE", nil, @@ -410,14 +410,14 @@ func TestFolderAPIBuilder_Validate_Update(t *testing.T) { func TestFolderAPIBuilder_Mutate_Create(t *testing.T) { tests := []struct { name string - input *v0alpha1.Folder - expected *v0alpha1.Folder + input *folders.Folder + expected *folders.Folder wantErr bool }{ { name: "should trim a title", - input: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + input: &folders.Folder{ + Spec: folders.Spec{ Title: " foo ", }, TypeMeta: metav1.TypeMeta{ @@ -427,8 +427,8 @@ func TestFolderAPIBuilder_Mutate_Create(t *testing.T) { Name: "valid-name", }, }, - expected: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + expected: &folders.Folder{ + Spec: folders.Spec{ Title: "foo", }, TypeMeta: metav1.TypeMeta{ @@ -441,8 +441,8 @@ func TestFolderAPIBuilder_Mutate_Create(t *testing.T) { }, { name: "should return error if title doesnt exist", - input: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{}, + input: &folders.Folder{ + Spec: folders.Spec{}, TypeMeta: metav1.TypeMeta{ Kind: "Folder", }, @@ -454,7 +454,7 @@ func TestFolderAPIBuilder_Mutate_Create(t *testing.T) { }, { name: "should return error if spec doesnt exist", - input: &v0alpha1.Folder{ + input: &folders.Folder{ TypeMeta: metav1.TypeMeta{ Kind: "Folder", }, @@ -482,10 +482,10 @@ func TestFolderAPIBuilder_Mutate_Create(t *testing.T) { err := b.Validate(context.Background(), admission.NewAttributesRecord( tt.input, nil, - v0alpha1.SchemeGroupVersion.WithKind("folder"), + folders.SchemeGroupVersion.WithKind("folder"), "stacks-123", tt.input.Name, - v0alpha1.SchemeGroupVersion.WithResource("folders"), + folders.SchemeGroupVersion.WithResource("folders"), "", "CREATE", nil, @@ -503,8 +503,8 @@ func TestFolderAPIBuilder_Mutate_Create(t *testing.T) { } func TestFolderAPIBuilder_Mutate_Update(t *testing.T) { - existingObj := &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + existingObj := &folders.Folder{ + Spec: folders.Spec{ Title: "some title", }, TypeMeta: metav1.TypeMeta{ @@ -516,14 +516,14 @@ func TestFolderAPIBuilder_Mutate_Update(t *testing.T) { } tests := []struct { name string - input *v0alpha1.Folder - expected *v0alpha1.Folder + input *folders.Folder + expected *folders.Folder wantErr bool }{ { name: "should trim a title", - input: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + input: &folders.Folder{ + Spec: folders.Spec{ Title: " foo ", }, TypeMeta: metav1.TypeMeta{ @@ -533,8 +533,8 @@ func TestFolderAPIBuilder_Mutate_Update(t *testing.T) { Name: "valid-name", }, }, - expected: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{ + expected: &folders.Folder{ + Spec: folders.Spec{ Title: "foo", }, TypeMeta: metav1.TypeMeta{ @@ -547,8 +547,8 @@ func TestFolderAPIBuilder_Mutate_Update(t *testing.T) { }, { name: "should return error if title doesnt exist", - input: &v0alpha1.Folder{ - Spec: v0alpha1.Spec{}, + input: &folders.Folder{ + Spec: folders.Spec{}, TypeMeta: metav1.TypeMeta{ Kind: "Folder", }, @@ -560,7 +560,7 @@ func TestFolderAPIBuilder_Mutate_Update(t *testing.T) { }, { name: "should return error if spec doesnt exist", - input: &v0alpha1.Folder{ + input: &folders.Folder{ TypeMeta: metav1.TypeMeta{ Kind: "Folder", }, @@ -588,10 +588,10 @@ func TestFolderAPIBuilder_Mutate_Update(t *testing.T) { err := b.Validate(context.Background(), admission.NewAttributesRecord( tt.input, existingObj, - v0alpha1.SchemeGroupVersion.WithKind("folder"), + folders.SchemeGroupVersion.WithKind("folder"), "stacks-123", tt.input.Name, - v0alpha1.SchemeGroupVersion.WithResource("folders"), + folders.SchemeGroupVersion.WithResource("folders"), "", "UPDATE", nil, diff --git a/pkg/registry/apis/folders/sub_access.go b/pkg/registry/apis/folders/sub_access.go index b0b2cf7f546..ccdc5afa039 100644 --- a/pkg/registry/apis/folders/sub_access.go +++ b/pkg/registry/apis/folders/sub_access.go @@ -8,7 +8,7 @@ import ( "k8s.io/apiserver/pkg/registry/rest" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/guardian" @@ -22,7 +22,7 @@ var _ = rest.Connecter(&subAccessREST{}) var _ = rest.StorageMetadata(&subAccessREST{}) func (r *subAccessREST) New() runtime.Object { - return &v0alpha1.FolderAccessInfo{} + return &folders.FolderAccessInfo{} } func (r *subAccessREST) Destroy() { @@ -37,7 +37,7 @@ func (r *subAccessREST) ProducesMIMETypes(verb string) []string { } func (r *subAccessREST) ProducesObject(verb string) interface{} { - return &v0alpha1.FolderAccessInfo{} + return &folders.FolderAccessInfo{} } func (r *subAccessREST) NewConnectOptions() (runtime.Object, bool, string) { @@ -69,7 +69,7 @@ func (r *subAccessREST) Connect(ctx context.Context, name string, opts runtime.O } return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - access := &v0alpha1.FolderAccessInfo{} + access := &folders.FolderAccessInfo{} access.CanEdit, _ = guardian.CanEdit() access.CanSave, _ = guardian.CanSave() access.CanAdmin, _ = guardian.CanAdmin() diff --git a/pkg/registry/apis/folders/sub_count.go b/pkg/registry/apis/folders/sub_count.go index 2316527c004..062a6fde7b2 100644 --- a/pkg/registry/apis/folders/sub_count.go +++ b/pkg/registry/apis/folders/sub_count.go @@ -7,7 +7,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/storage/unified/resource" ) @@ -22,7 +22,7 @@ var ( ) func (r *subCountREST) New() runtime.Object { - return &v0alpha1.DescendantCounts{} + return &folders.DescendantCounts{} } func (r *subCountREST) Destroy() { @@ -37,7 +37,7 @@ func (r *subCountREST) ProducesMIMETypes(verb string) []string { } func (r *subCountREST) ProducesObject(verb string) interface{} { - return &v0alpha1.DescendantCounts{} + return &folders.DescendantCounts{} } func (r *subCountREST) NewConnectOptions() (runtime.Object, bool, string) { @@ -60,11 +60,11 @@ func (r *subCountREST) Connect(ctx context.Context, name string, opts runtime.Ob responder.Error(err) return } - rsp := &v0alpha1.DescendantCounts{ - Counts: make([]v0alpha1.ResourceStats, len(stats.Stats)), + rsp := &folders.DescendantCounts{ + Counts: make([]folders.ResourceStats, len(stats.Stats)), } for i, v := range stats.Stats { - rsp.Counts[i] = v0alpha1.ResourceStats{ + rsp.Counts[i] = folders.ResourceStats{ Group: v.Group, Resource: v.Resource, Count: v.Count, diff --git a/pkg/registry/apis/folders/sub_parent_test.go b/pkg/registry/apis/folders/sub_parent_test.go index 0c29800dbf4..c383d5b7245 100644 --- a/pkg/registry/apis/folders/sub_parent_test.go +++ b/pkg/registry/apis/folders/sub_parent_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -14,46 +14,46 @@ import ( func TestSubParent(t *testing.T) { tests := []struct { name string - input *v0alpha1.Folder - expected *v0alpha1.FolderInfoList + input *folders.Folder + expected *folders.FolderInfoList setuFn func(*mock.Mock) }{ { name: "no parents", - input: &v0alpha1.Folder{ + input: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Annotations: map[string]string{}, }, - Spec: v0alpha1.Spec{ + Spec: folders.Spec{ Title: "some tittle", }, }, - expected: &v0alpha1.FolderInfoList{Items: []v0alpha1.FolderInfo{{Name: "test", Title: "some tittle"}}}, + expected: &folders.FolderInfoList{Items: []folders.FolderInfo{{Name: "test", Title: "some tittle"}}}, }, { name: "has a parent", - input: &v0alpha1.Folder{ + input: &folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "test", Annotations: map[string]string{"grafana.app/folder": "parent-test"}, }, - Spec: v0alpha1.Spec{ + Spec: folders.Spec{ Title: "some tittle", }, }, setuFn: func(m *mock.Mock) { - m.On("Get", context.TODO(), "parent-test", &metav1.GetOptions{}).Return(&v0alpha1.Folder{ + m.On("Get", context.TODO(), "parent-test", &metav1.GetOptions{}).Return(&folders.Folder{ ObjectMeta: metav1.ObjectMeta{ Name: "parent-test", Annotations: map[string]string{}, }, - Spec: v0alpha1.Spec{ + Spec: folders.Spec{ Title: "some other tittle", }, }, nil).Once() }, - expected: &v0alpha1.FolderInfoList{Items: []v0alpha1.FolderInfo{ + expected: &folders.FolderInfoList{Items: []folders.FolderInfo{ {Name: "test", Title: "some tittle", Parent: "parent-test"}, {Name: "parent-test", Title: "some other tittle"}}, }}, diff --git a/pkg/registry/apis/folders/sub_parents.go b/pkg/registry/apis/folders/sub_parents.go index 87f9a999c1f..598b5ae4d0e 100644 --- a/pkg/registry/apis/folders/sub_parents.go +++ b/pkg/registry/apis/folders/sub_parents.go @@ -10,7 +10,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" ) type subParentsREST struct { @@ -21,7 +21,7 @@ var _ = rest.Connecter(&subParentsREST{}) var _ = rest.StorageMetadata(&subParentsREST{}) func (r *subParentsREST) New() runtime.Object { - return &v0alpha1.FolderInfoList{} + return &folders.FolderInfoList{} } func (r *subParentsREST) Destroy() { @@ -36,7 +36,7 @@ func (r *subParentsREST) ProducesMIMETypes(verb string) []string { } func (r *subParentsREST) ProducesObject(verb string) interface{} { - return &v0alpha1.FolderInfoList{} + return &folders.FolderInfoList{} } func (r *subParentsREST) NewConnectOptions() (runtime.Object, bool, string) { @@ -48,7 +48,7 @@ func (r *subParentsREST) Connect(ctx context.Context, name string, opts runtime. if err != nil { return nil, err } - folder, ok := obj.(*v0alpha1.Folder) + folder, ok := obj.(*folders.Folder) if !ok { return nil, fmt.Errorf("expecting folder, found: %T", folder) } @@ -61,13 +61,13 @@ func (r *subParentsREST) Connect(ctx context.Context, name string, opts runtime. }), nil } -func (r *subParentsREST) parents(ctx context.Context, folder *v0alpha1.Folder) *v0alpha1.FolderInfoList { - info := &v0alpha1.FolderInfoList{ - Items: []v0alpha1.FolderInfo{}, +func (r *subParentsREST) parents(ctx context.Context, folder *folders.Folder) *folders.FolderInfoList { + info := &folders.FolderInfoList{ + Items: []folders.FolderInfo{}, } for folder != nil { parent := getParent(folder) - info.Items = append(info.Items, v0alpha1.FolderInfo{ + info.Items = append(info.Items, folders.FolderInfo{ Name: folder.Name, Title: folder.Spec.Title, Description: folder.Spec.Description, @@ -79,7 +79,7 @@ func (r *subParentsREST) parents(ctx context.Context, folder *v0alpha1.Folder) * obj, err := r.getter.Get(ctx, parent, &metav1.GetOptions{}) if err != nil { - info.Items = append(info.Items, v0alpha1.FolderInfo{ + info.Items = append(info.Items, folders.FolderInfo{ Name: parent, Detached: true, Description: err.Error(), @@ -87,9 +87,9 @@ func (r *subParentsREST) parents(ctx context.Context, folder *v0alpha1.Folder) * break } - parentFolder, ok := obj.(*v0alpha1.Folder) + parentFolder, ok := obj.(*folders.Folder) if !ok { - info.Items = append(info.Items, v0alpha1.FolderInfo{ + info.Items = append(info.Items, folders.FolderInfo{ Name: parent, Detached: true, Description: fmt.Sprintf("expected folder, found: %T", obj), diff --git a/pkg/registry/apis/provisioning/controller/finalizers.go b/pkg/registry/apis/provisioning/controller/finalizers.go index 137bd9e7055..1420f9ab958 100644 --- a/pkg/registry/apis/provisioning/controller/finalizers.go +++ b/pkg/registry/apis/provisioning/controller/finalizers.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana/pkg/apimachinery/utils" - folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/resources" diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index 9807476cbb4..7f8c70d497a 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -29,7 +29,7 @@ import ( dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" apiutils "github.com/grafana/grafana/pkg/apimachinery/utils" - folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/apiserver/readonly" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" diff --git a/pkg/registry/apis/provisioning/resources/client.go b/pkg/registry/apis/provisioning/resources/client.go index a112aa3965e..6151d6064ab 100644 --- a/pkg/registry/apis/provisioning/resources/client.go +++ b/pkg/registry/apis/provisioning/resources/client.go @@ -11,7 +11,7 @@ import ( "k8s.io/client-go/dynamic" dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" iam "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver" "github.com/grafana/grafana/pkg/services/apiserver/client" diff --git a/pkg/registry/apis/provisioning/resources/folders.go b/pkg/registry/apis/provisioning/resources/folders.go index 6947ba44b95..0fbd8fa36ed 100644 --- a/pkg/registry/apis/provisioning/resources/folders.go +++ b/pkg/registry/apis/provisioning/resources/folders.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository" "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" ) @@ -119,8 +119,8 @@ func (fm *FolderManager) EnsureFolderExists(ctx context.Context, folder Folder, }, }, } - obj.SetAPIVersion(v0alpha1.APIVERSION) - obj.SetKind(v0alpha1.FolderResourceInfo.GroupVersionKind().Kind) + obj.SetAPIVersion(folders.APIVERSION) + obj.SetKind(folders.FolderResourceInfo.GroupVersionKind().Kind) obj.SetNamespace(cfg.GetNamespace()) obj.SetName(folder.ID) diff --git a/pkg/registry/apis/provisioning/resources/object.go b/pkg/registry/apis/provisioning/resources/object.go index 27df4be5cb6..eb62fa7e92d 100644 --- a/pkg/registry/apis/provisioning/resources/object.go +++ b/pkg/registry/apis/provisioning/resources/object.go @@ -8,7 +8,7 @@ import ( dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" - folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/storage/legacysql/dualwrite" diff --git a/pkg/registry/apis/provisioning/resources/parser_test.go b/pkg/registry/apis/provisioning/resources/parser_test.go index 85b509eae3d..1a02854ae2b 100644 --- a/pkg/registry/apis/provisioning/resources/parser_test.go +++ b/pkg/registry/apis/provisioning/resources/parser_test.go @@ -56,7 +56,7 @@ spec: // Now try again without a name _, err = parser.Parse(context.Background(), &repository.FileInfo{ - Data: []byte(`apiVersion: dashboard.grafana.app/v1alpha1 + Data: []byte(`apiVersion: ` + dashboardV1.APIVERSION + ` kind: Dashboard spec: title: Test dashboard diff --git a/pkg/registry/apis/provisioning/resources/tree.go b/pkg/registry/apis/provisioning/resources/tree.go index 85a091459ae..4efd0e82d57 100644 --- a/pkg/registry/apis/provisioning/resources/tree.go +++ b/pkg/registry/apis/provisioning/resources/tree.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/grafana/grafana/pkg/apimachinery/utils" - folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" "github.com/grafana/grafana/pkg/registry/apis/provisioning/safepath" ) diff --git a/pkg/services/apiserver/standalone/runtime_test.go b/pkg/services/apiserver/standalone/runtime_test.go index d5c756031d3..dbee0a54e2b 100644 --- a/pkg/services/apiserver/standalone/runtime_test.go +++ b/pkg/services/apiserver/standalone/runtime_test.go @@ -4,11 +4,12 @@ import ( "fmt" "testing" + dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/stretchr/testify/require" ) func TestReadRuntimeCOnfig(t *testing.T) { - out, err := ReadRuntimeConfig("all/all=true,dashboard.grafana.app/v1alpha1=false") + out, err := ReadRuntimeConfig("all/all=true," + dashboardv1.APIVERSION + "=false") require.NoError(t, err) require.Equal(t, []RuntimeConfig{ {Group: "all", Version: "all", Enabled: true}, diff --git a/pkg/services/authn/authnimpl/service_test.go b/pkg/services/authn/authnimpl/service_test.go index 3decba02de4..bd01e07456a 100644 --- a/pkg/services/authn/authnimpl/service_test.go +++ b/pkg/services/authn/authnimpl/service_test.go @@ -307,7 +307,7 @@ func TestService_OrgID(t *testing.T) { desc: "should set org id from default namespace", req: &authn.Request{HTTPRequest: &http.Request{ Header: map[string][]string{}, - URL: mustParseURL("http://localhost/apis/folder.grafana.app/v0alpha1/namespaces/default/folders"), + URL: mustParseURL("http://localhost/apis/folder.grafana.app/v1/namespaces/default/folders"), }}, expectedOrgID: 1, }, @@ -315,7 +315,7 @@ func TestService_OrgID(t *testing.T) { desc: "should set org id from namespace", req: &authn.Request{HTTPRequest: &http.Request{ Header: map[string][]string{}, - URL: mustParseURL("http://localhost/apis/folder.grafana.app/v0alpha1/namespaces/org-2/folders"), + URL: mustParseURL("http://localhost/apis/folder.grafana.app/v1/namespaces/org-2/folders"), }}, expectedOrgID: 2, }, @@ -323,7 +323,7 @@ func TestService_OrgID(t *testing.T) { desc: "should set set org 1 for stack namespace", req: &authn.Request{HTTPRequest: &http.Request{ Header: map[string][]string{}, - URL: mustParseURL("http://localhost/apis/folder.grafana.app/v0alpha1/namespaces/stacks-100/folders"), + URL: mustParseURL("http://localhost/apis/folder.grafana.app/v1/namespaces/stacks-100/folders"), }}, stackID: 100, expectedOrgID: 1, @@ -332,7 +332,7 @@ func TestService_OrgID(t *testing.T) { desc: "should error for wrong stack namespace", req: &authn.Request{HTTPRequest: &http.Request{ Header: map[string][]string{}, - URL: mustParseURL("http://localhost/apis/folder.grafana.app/v0alpha1/namespaces/stacks-100/folders"), + URL: mustParseURL("http://localhost/apis/folder.grafana.app/v1/namespaces/stacks-100/folders"), }}, stackID: 101, expectedOrgID: 0, diff --git a/pkg/services/authz/rbac/store/folder_store.go b/pkg/services/authz/rbac/store/folder_store.go index a8ed3d2a1f0..f61456c7261 100644 --- a/pkg/services/authz/rbac/store/folder_store.go +++ b/pkg/services/authz/rbac/store/folder_store.go @@ -12,7 +12,7 @@ import ( "k8s.io/client-go/tools/pager" "github.com/grafana/grafana/pkg/apimachinery/utils" - folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folderv1 "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/storage/legacysql" "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" @@ -158,5 +158,5 @@ func (s *APIFolderStore) client(ctx context.Context, namespace string) (dynamic. if err != nil { return nil, err } - return client.Resource(folderv0alpha1.FolderResourceInfo.GroupVersionResource()).Namespace(namespace), nil + return client.Resource(folderv1.FolderResourceInfo.GroupVersionResource()).Namespace(namespace), nil } diff --git a/pkg/services/authz/zanzana/common/info.go b/pkg/services/authz/zanzana/common/info.go index 9506a8ac8d9..dbf228883cc 100644 --- a/pkg/services/authz/zanzana/common/info.go +++ b/pkg/services/authz/zanzana/common/info.go @@ -4,7 +4,7 @@ import ( authzv1 "github.com/grafana/authlib/authz/proto/v1" "google.golang.org/protobuf/types/known/structpb" - folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" iamalpha1 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" ) @@ -16,8 +16,8 @@ type typeInfo struct { var typedResources = map[string]typeInfo{ FormatGroupResource( - folderalpha1.FolderResourceInfo.GroupResource().Group, - folderalpha1.FolderResourceInfo.GroupResource().Resource, + folders.FolderResourceInfo.GroupResource().Group, + folders.FolderResourceInfo.GroupResource().Resource, "", ): {Type: "folder", Relations: RelationsTyped}, FormatGroupResource( diff --git a/pkg/services/authz/zanzana/translations.go b/pkg/services/authz/zanzana/translations.go index 457c75bdd82..f7874b50308 100644 --- a/pkg/services/authz/zanzana/translations.go +++ b/pkg/services/authz/zanzana/translations.go @@ -1,8 +1,8 @@ package zanzana import ( - dashboardalpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - folderalpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + dashboards "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" ) const ( @@ -44,11 +44,11 @@ func newScopedMapping(relation, group, resource, subresource string) actionMappi } var ( - folderGroup = folderalpha1.FolderResourceInfo.GroupResource().Group - folderResource = folderalpha1.FolderResourceInfo.GroupResource().Resource + folderGroup = folders.FolderResourceInfo.GroupResource().Group + folderResource = folders.FolderResourceInfo.GroupResource().Resource - dashboardGroup = dashboardalpha1.DashboardResourceInfo.GroupResource().Group - dashboardResource = dashboardalpha1.DashboardResourceInfo.GroupResource().Resource + dashboardGroup = dashboards.DashboardResourceInfo.GroupResource().Group + dashboardResource = dashboards.DashboardResourceInfo.GroupResource().Resource ) var resourceTranslations = map[string]resourceTranslation{ diff --git a/pkg/services/dashboards/service/client/client.go b/pkg/services/dashboards/service/client/client.go index bea26198844..7a3404b50f8 100644 --- a/pkg/services/dashboards/service/client/client.go +++ b/pkg/services/dashboards/service/client/client.go @@ -10,7 +10,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - dashboardv1alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/apiserver" @@ -47,7 +47,7 @@ func NewK8sClientWithFallback( ) *K8sClientWithFallback { newClientFunc := newK8sClientFactory(cfg, restConfigProvider, dashboardStore, userService, resourceClient, sorter, dual) return &K8sClientWithFallback{ - K8sHandler: newClientFunc(context.Background(), dashboardv1alpha1.VERSION), + K8sHandler: newClientFunc(context.Background(), dashboardv1.VERSION), newClientFunc: newClientFunc, metrics: newK8sClientMetrics(reg), log: log.New("dashboards-k8s-client"), @@ -117,7 +117,7 @@ func newK8sClientFactory( cacheMutex := &sync.RWMutex{} return func(ctx context.Context, version string) client.K8sHandler { _, span := tracing.Start(ctx, "k8sClientFactory.GetClient", - attribute.String("group", dashboardv1alpha1.GROUP), + attribute.String("group", dashboardv1.GROUP), attribute.String("version", version), attribute.String("resource", "dashboards"), ) @@ -143,7 +143,7 @@ func newK8sClientFactory( } gvr := schema.GroupVersionResource{ - Group: dashboardv1alpha1.GROUP, + Group: dashboardv1.GROUP, Version: version, Resource: "dashboards", } diff --git a/pkg/services/dashboards/service/client/client_test.go b/pkg/services/dashboards/service/client/client_test.go index 561f9da150a..767f89ad1f5 100644 --- a/pkg/services/dashboards/service/client/client_test.go +++ b/pkg/services/dashboards/service/client/client_test.go @@ -11,7 +11,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - dashboardv1alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/client" ) @@ -38,7 +38,7 @@ func setupTest(t *testing.T) *testSetup { if version == "v2alpha1" { return mockClientV2Alpha1 } - if version == dashboardv1alpha1.VERSION { + if version == dashboardv1.VERSION { return mockClientV1Alpha1 } t.Fatalf("Unexpected call to newClientFunc with version %s", version) diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index aa718cafbfd..eed74193348 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -26,11 +26,11 @@ import ( "github.com/grafana/grafana-app-sdk/logging" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard" - dashboardv0alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" - dashboardv1alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashboardv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folderv1 "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/kvstore" "github.com/grafana/grafana/pkg/infra/log" @@ -1553,7 +1553,7 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb result.SortMeta = hit.Field.GetNestedInt64(fieldName) } - if hit.Resource == folderv0alpha1.RESOURCE { + if hit.Resource == folderv1.RESOURCE { result.IsFolder = true } @@ -1566,7 +1566,7 @@ func (dr *DashboardServiceImpl) FindDashboards(ctx context.Context, query *dashb return dr.dashboardStore.FindDashboards(ctx, query) } -func (dr *DashboardServiceImpl) fetchFolderNames(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery, hits []dashboardv0alpha1.DashboardHit) (map[string]string, error) { +func (dr *DashboardServiceImpl) fetchFolderNames(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery, hits []dashboardv0.DashboardHit) (map[string]string, error) { // call this with elevated permissions so we can get folder names where user does not have access // some dashboards are shared directly with user, but the folder is not accessible via the folder permissions serviceCtx, serviceIdent := identity.WithServiceIdentity(ctx, query.OrgId) @@ -1923,7 +1923,7 @@ func (dr *DashboardServiceImpl) listDashboardsThroughK8s(ctx context.Context, or return dashboards, nil } -func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (dashboardv0alpha1.SearchResults, error) { +func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery) (dashboardv0.SearchResults, error) { request := &resource.ResourceSearchRequest{ Options: &resource.ListOptions{ Fields: []*resource.Requirement{}, @@ -2053,21 +2053,21 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex switch query.Type { case "": // When no type specified, search for dashboards - request.Options.Key, err = resource.AsResourceKey(namespace, dashboardv1alpha1.DASHBOARD_RESOURCE) + request.Options.Key, err = resource.AsResourceKey(namespace, dashboardv1.DASHBOARD_RESOURCE) // Currently a search query is across folders and dashboards if err == nil { - federate, err = resource.AsResourceKey(namespace, folderv0alpha1.RESOURCE) + federate, err = resource.AsResourceKey(namespace, folderv1.RESOURCE) } case searchstore.TypeDashboard, searchstore.TypeAnnotation: - request.Options.Key, err = resource.AsResourceKey(namespace, dashboardv1alpha1.DASHBOARD_RESOURCE) + request.Options.Key, err = resource.AsResourceKey(namespace, dashboardv1.DASHBOARD_RESOURCE) case searchstore.TypeFolder, searchstore.TypeAlertFolder: - request.Options.Key, err = resource.AsResourceKey(namespace, folderv0alpha1.RESOURCE) + request.Options.Key, err = resource.AsResourceKey(namespace, folderv1.RESOURCE) default: err = fmt.Errorf("bad type request") } if err != nil { - return dashboardv0alpha1.SearchResults{}, err + return dashboardv0.SearchResults{}, err } if federate != nil { @@ -2077,14 +2077,14 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex if query.Sort.Name != "" { sortName, isDesc, err := legacysearcher.ParseSortName(query.Sort.Name) if err != nil { - return dashboardv0alpha1.SearchResults{}, err + return dashboardv0.SearchResults{}, err } request.SortBy = append(request.SortBy, &resource.ResourceSearchRequest_Sort{Field: sortName, Desc: isDesc}) } res, err := dr.k8sclient.Search(ctx, query.OrgId, request) if err != nil { - return dashboardv0alpha1.SearchResults{}, err + return dashboardv0.SearchResults{}, err } return dashboardsearch.ParseResults(res, 0) @@ -2114,7 +2114,7 @@ func (dr *DashboardServiceImpl) searchProvisionedDashboardsThroughK8s(ctx contex var mu sync.Mutex g, ctx := errgroup.WithContext(ctx) for _, h := range searchResults.Hits { - func(hit dashboardv0alpha1.DashboardHit) { + func(hit dashboardv0.DashboardHit) { g.Go(func() error { out, err := dr.k8sclient.Get(ctx, hit.Name, query.OrgId, v1.GetOptions{}) if err != nil { @@ -2246,7 +2246,7 @@ func (dr *DashboardServiceImpl) unstructuredToLegacyDashboardWithUsers(item *uns FolderUID: obj.GetFolder(), Version: int(dashVersion), Data: simplejson.NewFromAny(spec), - APIVersion: strings.TrimPrefix(item.GetAPIVersion(), dashboardv1alpha1.GROUP+"/"), + APIVersion: strings.TrimPrefix(item.GetAPIVersion(), dashboardv1.GROUP+"/"), } out.Created = obj.GetCreationTimestamp().Time @@ -2335,7 +2335,7 @@ func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, names finalObj.Object["spec"] = obj finalObj.SetName(uid) finalObj.SetNamespace(namespace) - finalObj.SetGroupVersionKind(dashboardv1alpha1.DashboardResourceInfo.GroupVersionKind()) + finalObj.SetGroupVersionKind(dashboardv1.DashboardResourceInfo.GroupVersionKind()) meta, err := utils.MetaAccessor(finalObj) if err != nil { @@ -2353,7 +2353,7 @@ func LegacySaveCommandToUnstructured(cmd *dashboards.SaveDashboardCommand, names return finalObj, nil } -func getFolderUIDs(hits []dashboardv0alpha1.DashboardHit) []string { +func getFolderUIDs(hits []dashboardv0.DashboardHit) []string { folderSet := map[string]bool{} for _, hit := range hits { if hit.Folder != "" && !folderSet[hit.Folder] { diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 42d04c82db7..313edeba510 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -16,7 +16,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apiserver/pkg/endpoints/request" - dashboardv1alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/components/simplejson" @@ -542,8 +542,8 @@ func TestGetProvisionedDashboardData(t *testing.T) { k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": dashboardv1alpha1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1alpha1.DashboardResourceInfo.GroupVersionKind().Kind, + "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "name": "uid", "labels": map[string]interface{}{ @@ -648,8 +648,8 @@ func TestGetProvisionedDashboardDataByDashboardID(t *testing.T) { provisioningTimestamp := int64(1234567) k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]interface{}{ - "apiVersion": dashboardv1alpha1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1alpha1.DashboardResourceInfo.GroupVersionKind().Kind, + "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "name": "uid", "labels": map[string]interface{}{ @@ -742,8 +742,8 @@ func TestGetProvisionedDashboardDataByDashboardUID(t *testing.T) { provisioningTimestamp := int64(1234567) k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{Object: map[string]interface{}{ - "apiVersion": dashboardv1alpha1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1alpha1.DashboardResourceInfo.GroupVersionKind().Kind, + "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "name": "uid", "labels": map[string]interface{}{ @@ -975,8 +975,8 @@ func TestDeleteOrphanedProvisionedDashboards(t *testing.T) { k8sCliMock.On("GetNamespace", mock.Anything, mock.Anything).Return("default") k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": dashboardv1alpha1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1alpha1.DashboardResourceInfo.GroupVersionKind().Kind, + "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "name": "uid", "labels": map[string]interface{}{ @@ -1120,7 +1120,7 @@ func TestUnprovisionDashboard(t *testing.T) { }} k8sCliMock.On("Get", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(dash, nil) dashWithoutAnnotations := &unstructured.Unstructured{Object: map[string]any{ - "apiVersion": "dashboard.grafana.app/v1alpha1", + "apiVersion": dashboardv1.APIVERSION, "kind": "Dashboard", "metadata": map[string]any{ "name": "uid", @@ -2491,7 +2491,7 @@ func TestSetDefaultPermissionsAfterCreate(t *testing.T) { // Create test object key := &resource.ResourceKey{Group: "dashboard.grafana.app", Resource: "dashboards", Name: "test", Namespace: "default"} - obj := &dashboardv1alpha1.Dashboard{ + obj := &dashboardv1.Dashboard{ TypeMeta: metav1.TypeMeta{ APIVersion: "dashboard.grafana.app/v0alpha1", }, @@ -2834,8 +2834,8 @@ func TestK8sDashboardCleanupJob(t *testing.T) { func createTestUnstructuredDashboard(uid, title string, resourceVersion string) unstructured.Unstructured { return unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": dashboardv1alpha1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1alpha1.DashboardResourceInfo.GroupVersionKind().Kind, + "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "name": uid, "deletionTimestamp": "2023-01-01T00:00:00Z", diff --git a/pkg/services/dashboardversion/dashverimpl/dashver.go b/pkg/services/dashboardversion/dashverimpl/dashver.go index bb314f9eea8..49be4af1883 100644 --- a/pkg/services/dashboardversion/dashverimpl/dashver.go +++ b/pkg/services/dashboardversion/dashverimpl/dashver.go @@ -11,7 +11,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" @@ -55,7 +55,7 @@ func ProvideService(cfg *setting.Cfg, db db.DB, dashboardService dashboards.Dash k8sclient: client.NewK8sHandler( dual, request.GetNamespaceMapper(cfg), - v1alpha1.DashboardResourceInfo.GroupVersionResource(), + dashv1.DashboardResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashboardStore, userService, diff --git a/pkg/services/folder/folderimpl/conversions_test.go b/pkg/services/folder/folderimpl/conversions_test.go index 9e18c6e0050..e777b2f8220 100644 --- a/pkg/services/folder/folderimpl/conversions_test.go +++ b/pkg/services/folder/folderimpl/conversions_test.go @@ -18,7 +18,7 @@ func TestFolderConversions(t *testing.T) { input := &unstructured.Unstructured{} err := input.UnmarshalJSON([]byte(`{ "kind": "Folder", - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "metadata": { "name": "be79sztagf20wd", "namespace": "default", @@ -86,10 +86,10 @@ func TestFolderConversions(t *testing.T) { func TestFolderListConversions(t *testing.T) { input := &unstructured.UnstructuredList{} err := input.UnmarshalJSON([]byte(`{ - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "items": [ { - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "kind": "Folder", "metadata": { "annotations": { @@ -113,7 +113,7 @@ func TestFolderListConversions(t *testing.T) { } }, { - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "kind": "Folder", "metadata": { "annotations": { @@ -135,7 +135,7 @@ func TestFolderListConversions(t *testing.T) { } }, { - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "kind": "Folder", "metadata": { "annotations": { @@ -158,7 +158,7 @@ func TestFolderListConversions(t *testing.T) { } }, { - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "kind": "Folder", "metadata": { "annotations": { @@ -180,7 +180,7 @@ func TestFolderListConversions(t *testing.T) { } }, { - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "kind": "Folder", "metadata": { "annotations": { @@ -203,7 +203,7 @@ func TestFolderListConversions(t *testing.T) { } }, { - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "kind": "Folder", "metadata": { "annotations": {}, diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index b40b54aece5..adb66dd2820 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -18,9 +18,9 @@ import ( "github.com/grafana/dskit/concurrency" - dashboardalpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folderv1 "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/db" @@ -118,7 +118,7 @@ func ProvideService( k8sHandler := client.NewK8sHandler( dual, request.GetNamespaceMapper(cfg), - v0alpha1.FolderResourceInfo.GroupVersionResource(), + folderv1.FolderResourceInfo.GroupVersionResource(), restConfig.GetRestConfig, dashboardStore, userService, @@ -136,7 +136,7 @@ func ProvideService( dashHandler := client.NewK8sHandler( dual, request.GetNamespaceMapper(cfg), - dashboardalpha1.DashboardResourceInfo.GroupVersionResource(), + dashboardv1.DashboardResourceInfo.GroupVersionResource(), restConfig.GetRestConfig, dashboardStore, userService, diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage.go b/pkg/services/folder/folderimpl/folder_unifiedstorage.go index 6842724fdd3..9e67cfac626 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage.go @@ -16,7 +16,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folderv1 "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/slugify" @@ -176,8 +176,8 @@ func (s *Service) searchFoldersFromApiServer(ctx context.Context, query folder.S Options: &resource.ListOptions{ Key: &resource.ResourceKey{ Namespace: s.k8sclient.GetNamespace(query.OrgID), - Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, - Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, + Group: folderv1.FolderResourceInfo.GroupVersionResource().Group, + Resource: folderv1.FolderResourceInfo.GroupVersionResource().Resource, }, Fields: []*resource.Requirement{}, Labels: []*resource.Requirement{}, @@ -252,8 +252,8 @@ func (s *Service) getFolderByIDFromApiServer(ctx context.Context, id int64, orgI folderkey := &resource.ResourceKey{ Namespace: s.k8sclient.GetNamespace(orgID), - Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, - Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, + Group: folderv1.FolderResourceInfo.GroupVersionResource().Group, + Resource: folderv1.FolderResourceInfo.GroupVersionResource().Resource, } request := &resource.ResourceSearchRequest{ @@ -308,8 +308,8 @@ func (s *Service) getFolderByTitleFromApiServer(ctx context.Context, orgID int64 folderkey := &resource.ResourceKey{ Namespace: s.k8sclient.GetNamespace(orgID), - Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, - Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, + Group: folderv1.FolderResourceInfo.GroupVersionResource().Group, + Resource: folderv1.FolderResourceInfo.GroupVersionResource().Resource, } request := &resource.ResourceSearchRequest{ diff --git a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go index 472fdf35858..08f8854d3ca 100644 --- a/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go +++ b/pkg/services/folder/folderimpl/folder_unifiedstorage_test.go @@ -17,7 +17,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + foldersv1 "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log/logtest" @@ -59,9 +59,9 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { t.Skip("skipping integration test") } - m := map[string]v0alpha1.Folder{} + m := map[string]foldersv1.Folder{} - unifiedStorageFolder := &v0alpha1.Folder{} + unifiedStorageFolder := &foldersv1.Folder{} unifiedStorageFolder.Kind = "folder" fooFolder := &folder.Folder{ @@ -82,19 +82,19 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { mux := http.NewServeMux() - mux.HandleFunc("DELETE /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/deletefolder", func(w http.ResponseWriter, req *http.Request) { + mux.HandleFunc("DELETE /apis/folder.grafana.app/v1/namespaces/default/folders/deletefolder", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") }) - mux.HandleFunc("GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders", func(w http.ResponseWriter, req *http.Request) { + mux.HandleFunc("GET /apis/folder.grafana.app/v1/namespaces/default/folders", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") - l := &v0alpha1.FolderList{} + l := &foldersv1.FolderList{} l.Kind = "Folder" err := json.NewEncoder(w).Encode(l) require.NoError(t, err) }) - mux.HandleFunc("GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/foo", func(w http.ResponseWriter, req *http.Request) { + mux.HandleFunc("GET /apis/folder.grafana.app/v1/namespaces/default/folders/foo", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") namespacer := func(_ int64) string { return "1" } result, err := internalfolders.LegacyFolderToUnstructured(fooFolder, namespacer) @@ -104,7 +104,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { require.NoError(t, err) }) - mux.HandleFunc("GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/updatefolder", func(w http.ResponseWriter, req *http.Request) { + mux.HandleFunc("GET /apis/folder.grafana.app/v1/namespaces/default/folders/updatefolder", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") namespacer := func(_ int64) string { return "1" } result, err := internalfolders.LegacyFolderToUnstructured(updateFolder, namespacer) @@ -114,12 +114,12 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { require.NoError(t, err) }) - mux.HandleFunc("PUT /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/updatefolder", func(w http.ResponseWriter, req *http.Request) { + mux.HandleFunc("PUT /apis/folder.grafana.app/v1/namespaces/default/folders/updatefolder", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") buf, err := io.ReadAll(req.Body) require.NoError(t, err) - var foldr v0alpha1.Folder + var foldr foldersv1.Folder err = json.Unmarshal(buf, &foldr) require.NoError(t, err) @@ -133,22 +133,22 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { require.NoError(t, err) }) - mux.HandleFunc("GET /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/ady4yobv315a8e", func(w http.ResponseWriter, req *http.Request) { + mux.HandleFunc("GET /apis/folder.grafana.app/v1/namespaces/default/folders/ady4yobv315a8e", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(unifiedStorageFolder) require.NoError(t, err) }) - mux.HandleFunc("PUT /apis/folder.grafana.app/v0alpha1/namespaces/default/folders/ady4yobv315a8e", func(w http.ResponseWriter, req *http.Request) { + mux.HandleFunc("PUT /apis/folder.grafana.app/v1/namespaces/default/folders/ady4yobv315a8e", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(unifiedStorageFolder) require.NoError(t, err) }) - mux.HandleFunc("POST /apis/folder.grafana.app/v0alpha1/namespaces/default/folders", func(w http.ResponseWriter, req *http.Request) { + mux.HandleFunc("POST /apis/folder.grafana.app/v1/namespaces/default/folders", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/json") buf, err := io.ReadAll(req.Body) require.NoError(t, err) - var folder v0alpha1.Folder + var folder foldersv1.Folder err = json.Unmarshal(buf, &folder) require.NoError(t, err) @@ -184,7 +184,7 @@ func TestIntegrationFolderServiceViaUnifiedStorage(t *testing.T) { features := featuremgmt.WithFeatures(featuresArr...) dashboardStore := dashboards.NewFakeDashboardStore(t) - k8sCli := client.NewK8sHandler(dualwrite.ProvideTestService(), request.GetNamespaceMapper(cfg), v0alpha1.FolderResourceInfo.GroupVersionResource(), restCfgProvider.GetRestConfig, dashboardStore, userService, nil, sort.ProvideService()) + k8sCli := client.NewK8sHandler(dualwrite.ProvideTestService(), request.GetNamespaceMapper(cfg), foldersv1.FolderResourceInfo.GroupVersionResource(), restCfgProvider.GetRestConfig, dashboardStore, userService, nil, sort.ProvideService()) unifiedStore := ProvideUnifiedStore(k8sCli, userService) ctx := context.Background() @@ -541,8 +541,8 @@ func TestSearchFoldersFromApiServer(t *testing.T) { Options: &resource.ListOptions{ Key: &resource.ResourceKey{ Namespace: "default", - Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, - Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, + Group: foldersv1.FolderResourceInfo.GroupVersionResource().Group, + Resource: foldersv1.FolderResourceInfo.GroupVersionResource().Resource, }, Fields: []*resource.Requirement{ { @@ -631,8 +631,8 @@ func TestSearchFoldersFromApiServer(t *testing.T) { Options: &resource.ListOptions{ Key: &resource.ResourceKey{ Namespace: "default", - Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, - Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, + Group: foldersv1.FolderResourceInfo.GroupVersionResource().Group, + Resource: foldersv1.FolderResourceInfo.GroupVersionResource().Resource, }, Fields: []*resource.Requirement{}, Labels: []*resource.Requirement{ @@ -700,8 +700,8 @@ func TestSearchFoldersFromApiServer(t *testing.T) { Options: &resource.ListOptions{ Key: &resource.ResourceKey{ Namespace: "default", - Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, - Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, + Group: foldersv1.FolderResourceInfo.GroupVersionResource().Group, + Resource: foldersv1.FolderResourceInfo.GroupVersionResource().Resource, }, Fields: []*resource.Requirement{}, Labels: []*resource.Requirement{}, @@ -797,8 +797,8 @@ func TestGetFoldersFromApiServer(t *testing.T) { Options: &resource.ListOptions{ Key: &resource.ResourceKey{ Namespace: "default", - Group: v0alpha1.FolderResourceInfo.GroupVersionResource().Group, - Resource: v0alpha1.FolderResourceInfo.GroupVersionResource().Resource, + Group: foldersv1.FolderResourceInfo.GroupVersionResource().Group, + Resource: foldersv1.FolderResourceInfo.GroupVersionResource().Resource, }, Fields: []*resource.Requirement{}, Labels: []*resource.Requirement{}, diff --git a/pkg/services/folder/folderimpl/unifiedstore.go b/pkg/services/folder/folderimpl/unifiedstore.go index 41e1981a665..665ba45dd81 100644 --- a/pkg/services/folder/folderimpl/unifiedstore.go +++ b/pkg/services/folder/folderimpl/unifiedstore.go @@ -16,7 +16,7 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resource" - "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folderv1 "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/infra/log" internalfolders "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -452,7 +452,7 @@ func (ss *FolderUnifiedStoreImpl) CountInOrg(ctx context.Context, orgID int64) ( } func toFolderLegacyCounts(u *unstructured.Unstructured) (*folder.DescendantCounts, error) { - ds, err := v0alpha1.UnstructuredToDescendantCounts(u) + ds, err := folderv1.UnstructuredToDescendantCounts(u) if err != nil { return nil, err } diff --git a/pkg/storage/legacysql/dualwrite/utils.go b/pkg/storage/legacysql/dualwrite/utils.go index 0d91860070b..db748679678 100644 --- a/pkg/storage/legacysql/dualwrite/utils.go +++ b/pkg/storage/legacysql/dualwrite/utils.go @@ -5,7 +5,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" ) func IsReadingLegacyDashboardsAndFolders(ctx context.Context, svc Service) bool { diff --git a/pkg/storage/legacysql/dualwrite/utils_test.go b/pkg/storage/legacysql/dualwrite/utils_test.go index f13851da484..35440a738e2 100644 --- a/pkg/storage/legacysql/dualwrite/utils_test.go +++ b/pkg/storage/legacysql/dualwrite/utils_test.go @@ -9,7 +9,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - folders "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" ) func TestIsReadingLegacyDashboardsAndFolders(t *testing.T) { diff --git a/pkg/storage/unified/README.md b/pkg/storage/unified/README.md index 8efde1c2c91..f2fcb6e8e7c 100644 --- a/pkg/storage/unified/README.md +++ b/pkg/storage/unified/README.md @@ -191,7 +191,7 @@ No resources found in default namespace. To create a folder, create a file `folder-generate.yaml`: ```yaml -apiVersion: folder.grafana.app/v0alpha1 +apiVersion: folder.grafana.app/v1 kind: Folder metadata: generateName: x # anything is ok here... except yes or true -- they become boolean! diff --git a/pkg/storage/unified/apistore/fake_large.go b/pkg/storage/unified/apistore/fake_large.go index 2fb87f5af24..f222e8470d2 100644 --- a/pkg/storage/unified/apistore/fake_large.go +++ b/pkg/storage/unified/apistore/fake_large.go @@ -3,7 +3,7 @@ package apistore import ( "context" - dashboardv1alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/storage/unified/resource" "k8s.io/apimachinery/pkg/runtime/schema" @@ -16,7 +16,7 @@ type LargeObjectSupportFake struct { } func (s *LargeObjectSupportFake) GroupResource() schema.GroupResource { - return dashboardv1alpha1.DashboardResourceInfo.GroupResource() + return dashboardv1.DashboardResourceInfo.GroupResource() } func (s *LargeObjectSupportFake) Threshold() int { diff --git a/pkg/storage/unified/apistore/go.mod b/pkg/storage/unified/apistore/go.mod index 314af27cfab..b3cbb1b88c8 100644 --- a/pkg/storage/unified/apistore/go.mod +++ b/pkg/storage/unified/apistore/go.mod @@ -212,7 +212,6 @@ require ( github.com/grafana/grafana-azure-sdk-go/v2 v2.1.6 // indirect github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 // indirect github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 // indirect - github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250402082028-6781612335d9 // indirect github.com/grafana/grafana/pkg/promlib v0.0.8 // indirect github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 // indirect github.com/grafana/otel-profiling-go v0.5.1 // indirect diff --git a/pkg/storage/unified/apistore/go.sum b/pkg/storage/unified/apistore/go.sum index 0285eb79361..c7bcf3afc45 100644 --- a/pkg/storage/unified/apistore/go.sum +++ b/pkg/storage/unified/apistore/go.sum @@ -1276,7 +1276,6 @@ github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043/go. github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8 h1:9qOLpC21AmXZqZ6rUhrBWl2mVqS3CzV53pzw0BCuHt0= github.com/grafana/grafana/pkg/aggregator v0.0.0-20250220163425-b4c4b9abbdc8/go.mod h1:deLQ/ywLvpVGbncRGUA4UDGt8a5Ei9sivOP+x6AQ2ko= github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250402082028-6781612335d9 h1:kJWonBYechx35NbOUVf1ulufKyjH1UlDJDXlk8bdGn0= -github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250402082028-6781612335d9/go.mod h1:QdqsZpdCtg+3TEzXX3mUakjq79LFblZ8xliHmqZj3oA= github.com/grafana/grafana/pkg/promlib v0.0.8 h1:VUWsqttdf0wMI4j9OX9oNrykguQpZcruudDAFpJJVw0= github.com/grafana/grafana/pkg/promlib v0.0.8/go.mod h1:U1ezG/MGaEPoThqsr3lymMPN5yIPdVTJnDZ+wcXT+ao= github.com/grafana/grafana/pkg/semconv v0.0.0-20250220164708-c8d4ff28a450 h1:wSqgLKFwI7fyeqf3djRXGClBLb/UPjZ4XPm/UsKFDB0= diff --git a/pkg/storage/unified/apistore/prepare_test.go b/pkg/storage/unified/apistore/prepare_test.go index cea6060fe57..09cf01972ae 100644 --- a/pkg/storage/unified/apistore/prepare_test.go +++ b/pkg/storage/unified/apistore/prepare_test.go @@ -16,7 +16,7 @@ import ( "k8s.io/apiserver/pkg/storage" authtypes "github.com/grafana/authlib/types" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/utils" ) @@ -25,11 +25,11 @@ var scheme = runtime.NewScheme() var codecs = serializer.NewCodecFactory(scheme) func TestPrepareObjectForStorage(t *testing.T) { - _ = v1alpha1.AddToScheme(scheme) + _ = dashv1.AddToScheme(scheme) node, err := snowflake.NewNode(rand.Int63n(1024)) require.NoError(t, err) s := &Storage{ - codec: apitesting.TestCodec(codecs, v1alpha1.DashboardResourceInfo.GroupVersion()), + codec: apitesting.TestCodec(codecs, dashv1.DashboardResourceInfo.GroupVersion()), snowflake: node, opts: StorageOptions{ EnableFolderSupport: true, @@ -48,14 +48,14 @@ func TestPrepareObjectForStorage(t *testing.T) { }) t.Run("Error on missing name", func(t *testing.T) { - dashboard := v1alpha1.Dashboard{} + dashboard := dashv1.Dashboard{} _, _, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject()) require.Error(t, err) require.Contains(t, err.Error(), "missing name") }) t.Run("Error on non-empty resource version", func(t *testing.T) { - dashboard := v1alpha1.Dashboard{} + dashboard := dashv1.Dashboard{} dashboard.Name = "test-name" dashboard.ResourceVersion = "123" _, _, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject()) @@ -64,13 +64,13 @@ func TestPrepareObjectForStorage(t *testing.T) { }) t.Run("Generate UID and leave deprecated ID empty, if not required", func(t *testing.T) { - dashboard := v1alpha1.Dashboard{} + dashboard := dashv1.Dashboard{} dashboard.Name = "test-name" encodedData, _, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject()) require.NoError(t, err) - newObject, _, err := s.codec.Decode(encodedData, nil, &v1alpha1.Dashboard{}) + newObject, _, err := s.codec.Decode(encodedData, nil, &dashv1.Dashboard{}) require.NoError(t, err) obj, err := utils.MetaAccessor(newObject) require.NoError(t, err) @@ -90,7 +90,7 @@ func TestPrepareObjectForStorage(t *testing.T) { ctx, _, err := identity.WithProvisioningIdentity(ctx, "default") require.NoError(t, err) - dashboard := v1alpha1.Dashboard{} + dashboard := dashv1.Dashboard{} dashboard.Name = "test-name" obj := dashboard.DeepCopyObject() meta, err := utils.MetaAccessor(obj) @@ -109,7 +109,7 @@ func TestPrepareObjectForStorage(t *testing.T) { encodedData, _, err := s.prepareObjectForStorage(ctx, obj) require.NoError(t, err) - newObject, _, err := s.codec.Decode(encodedData, nil, &v1alpha1.Dashboard{}) + newObject, _, err := s.codec.Decode(encodedData, nil, &dashv1.Dashboard{}) require.NoError(t, err) meta, err = utils.MetaAccessor(newObject) require.NoError(t, err) @@ -126,7 +126,7 @@ func TestPrepareObjectForStorage(t *testing.T) { }) t.Run("Update should manage incrementing generation and metadata", func(t *testing.T) { - dashboard := v1alpha1.Dashboard{} + dashboard := dashv1.Dashboard{} dashboard.Name = "test-name" obj := dashboard.DeepCopyObject() meta, err := utils.MetaAccessor(obj) @@ -136,7 +136,7 @@ func TestPrepareObjectForStorage(t *testing.T) { encodedData, _, err := s.prepareObjectForStorage(ctx, obj) require.NoError(t, err) - insertedObject, _, err := s.codec.Decode(encodedData, nil, &v1alpha1.Dashboard{}) + insertedObject, _, err := s.codec.Decode(encodedData, nil, &dashv1.Dashboard{}) require.NoError(t, err) meta, err = utils.MetaAccessor(insertedObject) require.NoError(t, err) @@ -156,8 +156,8 @@ func TestPrepareObjectForStorage(t *testing.T) { updatedObject := insertedObject.DeepCopyObject() meta, err = utils.MetaAccessor(updatedObject) require.NoError(t, err) - err = meta.SetStatus(v1alpha1.DashboardStatus{ - Conversion: &v1alpha1.DashboardConversionStatus{ + err = meta.SetStatus(dashv1.DashboardStatus{ + Conversion: &dashv1.DashboardConversionStatus{ Failed: true, Error: "test", }, @@ -172,7 +172,7 @@ func TestPrepareObjectForStorage(t *testing.T) { require.Equal(t, int64(1), meta.GetGeneration()) // Change the folder -- the generation should increase and the updatedBy metadata - dashboard2 := &v1alpha1.Dashboard{ObjectMeta: v1.ObjectMeta{ + dashboard2 := &dashv1.Dashboard{ObjectMeta: v1.ObjectMeta{ Name: dashboard.Name, }} // TODO... deep copy, See: https://github.com/grafana/grafana/pull/102258 meta2, err := utils.MetaAccessor(dashboard2) @@ -186,12 +186,12 @@ func TestPrepareObjectForStorage(t *testing.T) { s.opts.RequireDeprecatedInternalID = true t.Run("Should generate internal id", func(t *testing.T) { - dashboard := v1alpha1.Dashboard{} + dashboard := dashv1.Dashboard{} dashboard.Name = "test-name" encodedData, _, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject()) require.NoError(t, err) - newObject, _, err := s.codec.Decode(encodedData, nil, &v1alpha1.Dashboard{}) + newObject, _, err := s.codec.Decode(encodedData, nil, &dashv1.Dashboard{}) require.NoError(t, err) obj, err := utils.MetaAccessor(newObject) require.NoError(t, err) @@ -201,7 +201,7 @@ func TestPrepareObjectForStorage(t *testing.T) { }) t.Run("Should use deprecated ID if given it", func(t *testing.T) { - dashboard := v1alpha1.Dashboard{} + dashboard := dashv1.Dashboard{} dashboard.Name = "test-name" obj := dashboard.DeepCopyObject() meta, err := utils.MetaAccessor(obj) @@ -210,7 +210,7 @@ func TestPrepareObjectForStorage(t *testing.T) { encodedData, _, err := s.prepareObjectForStorage(ctx, obj) require.NoError(t, err) - newObject, _, err := s.codec.Decode(encodedData, nil, &v1alpha1.Dashboard{}) + newObject, _, err := s.codec.Decode(encodedData, nil, &dashv1.Dashboard{}) require.NoError(t, err) meta, err = utils.MetaAccessor(newObject) require.NoError(t, err) @@ -218,7 +218,7 @@ func TestPrepareObjectForStorage(t *testing.T) { }) t.Run("Should remove grant permissions annotation", func(t *testing.T) { - dashboard := v1alpha1.Dashboard{} + dashboard := dashv1.Dashboard{} dashboard.Name = "test-name" obj := dashboard.DeepCopyObject() meta, err := utils.MetaAccessor(obj) @@ -227,7 +227,7 @@ func TestPrepareObjectForStorage(t *testing.T) { encodedData, p, err := s.prepareObjectForStorage(ctx, obj) require.NoError(t, err) - newObject, _, err := s.codec.Decode(encodedData, nil, &v1alpha1.Dashboard{}) + newObject, _, err := s.codec.Decode(encodedData, nil, &dashv1.Dashboard{}) require.NoError(t, err) meta, err = utils.MetaAccessor(newObject) require.NoError(t, err) @@ -236,11 +236,11 @@ func TestPrepareObjectForStorage(t *testing.T) { }) t.Run("calculate generation", func(t *testing.T) { - dash := &v1alpha1.Dashboard{ + dash := &dashv1.Dashboard{ ObjectMeta: v1.ObjectMeta{ Name: "test", }, - Spec: v1alpha1.DashboardSpec{ + Spec: dashv1.DashboardSpec{ Object: map[string]interface{}{ "hello": "world", }, @@ -286,8 +286,8 @@ func TestPrepareObjectForStorage(t *testing.T) { b.Labels = map[string]string{ "a": "b", } - b.Status = v1alpha1.DashboardStatus{ - Conversion: &v1alpha1.DashboardConversionStatus{ + b.Status = dashv1.DashboardStatus{ + Conversion: &dashv1.DashboardConversionStatus{ Failed: true, }, } @@ -320,13 +320,13 @@ func getPreparedObject(t *testing.T, ctx context.Context, s *Storage, obj runtim } func TestPrepareLargeObjectForStorage(t *testing.T) { - _ = v1alpha1.AddToScheme(scheme) + _ = dashv1.AddToScheme(scheme) node, err := snowflake.NewNode(rand.Int63n(1024)) require.NoError(t, err) ctx := authtypes.WithAuthInfo(context.Background(), &identity.StaticRequester{UserID: 1, UserUID: "user-uid", Type: authtypes.TypeUser}) - dashboard := v1alpha1.Dashboard{} + dashboard := dashv1.Dashboard{} dashboard.Name = "test-name" t.Run("Should deconstruct object if size is over threshold", func(t *testing.T) { los := LargeObjectSupportFake{ @@ -334,7 +334,7 @@ func TestPrepareLargeObjectForStorage(t *testing.T) { } f := &Storage{ - codec: apitesting.TestCodec(codecs, v1alpha1.DashboardResourceInfo.GroupVersion()), + codec: apitesting.TestCodec(codecs, dashv1.DashboardResourceInfo.GroupVersion()), snowflake: node, opts: StorageOptions{ LargeObjectSupport: &los, @@ -352,7 +352,7 @@ func TestPrepareLargeObjectForStorage(t *testing.T) { } f := &Storage{ - codec: apitesting.TestCodec(codecs, v1alpha1.DashboardResourceInfo.GroupVersion()), + codec: apitesting.TestCodec(codecs, dashv1.DashboardResourceInfo.GroupVersion()), snowflake: node, opts: StorageOptions{ LargeObjectSupport: &los, diff --git a/pkg/storage/unified/resource/go.mod b/pkg/storage/unified/resource/go.mod index 5e9bf8bba61..c447e0277ad 100644 --- a/pkg/storage/unified/resource/go.mod +++ b/pkg/storage/unified/resource/go.mod @@ -17,7 +17,6 @@ require ( github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043 github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250401081501-6af5fbf3fff0 - github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250402082028-6781612335d9 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 github.com/hashicorp/golang-lru/v2 v2.0.7 diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum index c7635ad122f..946f97fab8e 100644 --- a/pkg/storage/unified/resource/go.sum +++ b/pkg/storage/unified/resource/go.sum @@ -273,8 +273,6 @@ github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0 github.com/grafana/grafana-plugin-sdk-go v0.274.1-0.20250318081012-21a7f15619b0/go.mod h1:jV+CTjXqXYuaz8FgSG7ALOib3sgiDo/00dfsQFVTSpM= github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043 h1:wdJy5x6M7auWDjUIubqhfZuZvphUMyjD7hxB3RqV4aE= github.com/grafana/grafana/apps/dashboard v0.0.0-20250317130411-3f270d1de043/go.mod h1:jwYig4wlnLLq4HQKDpS95nDeZi4+DmcD17KYYS1gMJg= -github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250402082028-6781612335d9 h1:kJWonBYechx35NbOUVf1ulufKyjH1UlDJDXlk8bdGn0= -github.com/grafana/grafana/pkg/apis/folder v0.0.0-20250402082028-6781612335d9/go.mod h1:QdqsZpdCtg+3TEzXX3mUakjq79LFblZ8xliHmqZj3oA= github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg= diff --git a/pkg/storage/unified/resource/search.go b/pkg/storage/unified/resource/search.go index 921b09047c4..a98d5d0cfc1 100644 --- a/pkg/storage/unified/resource/search.go +++ b/pkg/storage/unified/resource/search.go @@ -16,8 +16,8 @@ import ( "golang.org/x/sync/errgroup" "k8s.io/apimachinery/pkg/runtime/schema" - dashboardv1alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/authlib/types" ) @@ -683,14 +683,14 @@ func AsResourceKey(ns string, t string) (*ResourceKey, error) { case "folders", "folder": return &ResourceKey{ Namespace: ns, - Group: folderv0alpha1.GROUP, - Resource: folderv0alpha1.RESOURCE, + Group: folders.GROUP, + Resource: folders.RESOURCE, }, nil case "dashboards", "dashboard": return &ResourceKey{ Namespace: ns, - Group: dashboardv1alpha1.GROUP, - Resource: dashboardv1alpha1.DASHBOARD_RESOURCE, + Group: dashboardv1.GROUP, + Resource: dashboardv1.DASHBOARD_RESOURCE, }, nil // NOT really supported in the dashboard search UI, but useful for manual testing diff --git a/pkg/storage/unified/resource/testdata/folder-resource.json b/pkg/storage/unified/resource/testdata/folder-resource.json index 7344d8229cb..24835df9a17 100644 --- a/pkg/storage/unified/resource/testdata/folder-resource.json +++ b/pkg/storage/unified/resource/testdata/folder-resource.json @@ -1,6 +1,6 @@ { "kind": "Folder", - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "metadata": { "name": "ae2ntrqxefvnke", "namespace": "default", diff --git a/pkg/storage/unified/resource/testdata/folder-resource2.json b/pkg/storage/unified/resource/testdata/folder-resource2.json index f0b719ef16e..588954c1e1b 100644 --- a/pkg/storage/unified/resource/testdata/folder-resource2.json +++ b/pkg/storage/unified/resource/testdata/folder-resource2.json @@ -1,6 +1,6 @@ { "kind": "Folder", - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "metadata": { "name": "ae2ntrqxefvnke", "namespace": "default", diff --git a/pkg/storage/unified/search/dashboard.go b/pkg/storage/unified/search/dashboard.go index 888c2f0c4ab..a4b2275ad32 100644 --- a/pkg/storage/unified/search/dashboard.go +++ b/pkg/storage/unified/search/dashboard.go @@ -8,7 +8,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + dashV1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/services/store/kind/dashboard" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -202,7 +202,7 @@ func DashboardBuilder(namespaced resource.NamespacedDocumentSupplier) (resource. } } return resource.DocumentBuilderInfo{ - GroupResource: v1alpha1.DashboardResourceInfo.GroupResource(), + GroupResource: dashV1.DashboardResourceInfo.GroupResource(), Fields: fields, Namespaced: namespaced, }, err diff --git a/pkg/storage/unified/search/testdata/doc/folder-aaa.json b/pkg/storage/unified/search/testdata/doc/folder-aaa.json index 27ef4d28ffb..171e43eb982 100644 --- a/pkg/storage/unified/search/testdata/doc/folder-aaa.json +++ b/pkg/storage/unified/search/testdata/doc/folder-aaa.json @@ -1,6 +1,6 @@ { "kind": "Folder", - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "metadata": { "name": "aaa", "namespace": "default", diff --git a/pkg/storage/unified/search/testdata/doc/folder-bbb.json b/pkg/storage/unified/search/testdata/doc/folder-bbb.json index 81f97cc332a..1ebb4d00a43 100644 --- a/pkg/storage/unified/search/testdata/doc/folder-bbb.json +++ b/pkg/storage/unified/search/testdata/doc/folder-bbb.json @@ -1,6 +1,6 @@ { "kind": "Folder", - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "metadata": { "name": "bbb", "namespace": "default", diff --git a/pkg/tests/apis/dashboard/integration/api_validation_test.go b/pkg/tests/apis/dashboard/integration/api_validation_test.go index 1a3fe0229fb..3551f7548f1 100644 --- a/pkg/tests/apis/dashboard/integration/api_validation_test.go +++ b/pkg/tests/apis/dashboard/integration/api_validation_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - dashboardv1alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + dashboardv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" @@ -221,8 +221,8 @@ func runDashboardValidationTests(t *testing.T, ctx TestContext) { t.Run("reject dashboard with invalid schema", func(t *testing.T) { dashObj := &unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": dashboardv1alpha1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1alpha1.DashboardResourceInfo.GroupVersionKind().Kind, + "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "generateName": "test-", }, @@ -684,18 +684,18 @@ func createTestContext(t *testing.T, helper *apis.K8sTestHelper, orgUsers apis.O // getDashboardGVR returns the dashboard GroupVersionResource func getDashboardGVR() schema.GroupVersionResource { return schema.GroupVersionResource{ - Group: dashboardv1alpha1.DashboardResourceInfo.GroupVersion().Group, - Version: dashboardv1alpha1.DashboardResourceInfo.GroupVersion().Version, - Resource: dashboardv1alpha1.DashboardResourceInfo.GetName(), + Group: dashboardv1.DashboardResourceInfo.GroupVersion().Group, + Version: dashboardv1.DashboardResourceInfo.GroupVersion().Version, + Resource: dashboardv1.DashboardResourceInfo.GetName(), } } // getFolderGVR returns the folder GroupVersionResource func getFolderGVR() schema.GroupVersionResource { return schema.GroupVersionResource{ - Group: folderv0alpha1.FolderResourceInfo.GroupVersion().Group, - Version: folderv0alpha1.FolderResourceInfo.GroupVersion().Version, - Resource: folderv0alpha1.FolderResourceInfo.GetName(), + Group: folders.FolderResourceInfo.GroupVersion().Group, + Version: folders.FolderResourceInfo.GroupVersion().Version, + Resource: folders.FolderResourceInfo.GetName(), } } @@ -728,8 +728,8 @@ func createFolderObject(t *testing.T, title string, namespace string, parentFold folderObj := &unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": folderv0alpha1.FolderResourceInfo.GroupVersion().String(), - "kind": folderv0alpha1.FolderResourceInfo.GroupVersionKind().Kind, + "apiVersion": folders.FolderResourceInfo.GroupVersion().String(), + "kind": folders.FolderResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "generateName": "test-folder-", "namespace": namespace, @@ -784,8 +784,8 @@ func createDashboardObject(t *testing.T, title string, folderUID string, generat dashObj := &unstructured.Unstructured{ Object: map[string]interface{}{ - "apiVersion": dashboardv1alpha1.DashboardResourceInfo.GroupVersion().String(), - "kind": dashboardv1alpha1.DashboardResourceInfo.GroupVersionKind().Kind, + "apiVersion": dashboardv1.DashboardResourceInfo.GroupVersion().String(), + "kind": dashboardv1.DashboardResourceInfo.GroupVersionKind().Kind, "metadata": map[string]interface{}{ "generateName": "test-", "annotations": map[string]interface{}{ diff --git a/pkg/tests/apis/folder/folders_test.go b/pkg/tests/apis/folder/folders_test.go index 2b317103876..3ad3c82cdbb 100644 --- a/pkg/tests/apis/folder/folders_test.go +++ b/pkg/tests/apis/folder/folders_test.go @@ -18,7 +18,7 @@ import ( "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/dtos" - folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folders "github.com/grafana/grafana/pkg/apis/folder/v1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" "github.com/grafana/grafana/pkg/services/dashboards" @@ -38,7 +38,7 @@ func TestMain(m *testing.M) { var gvr = schema.GroupVersionResource{ Group: "folder.grafana.app", - Version: "v0alpha1", + Version: "v1", Resource: "folders", } @@ -55,7 +55,7 @@ func TestIntegrationFoldersApp(t *testing.T) { t.Run("Check discovery client", func(t *testing.T) { disco := helper.NewDiscoveryClient() - resources, err := disco.ServerResourcesForGroupVersion("folder.grafana.app/v0alpha1") + resources, err := disco.ServerResourcesForGroupVersion("folder.grafana.app/v1") require.NoError(t, err) v1Disco, err := json.MarshalIndent(resources, "", " ") @@ -64,7 +64,7 @@ func TestIntegrationFoldersApp(t *testing.T) { require.JSONEq(t, `{ "kind": "APIResourceList", "apiVersion": "v1", - "groupVersion": "folder.grafana.app/v0alpha1", + "groupVersion": "folder.grafana.app/v1", "resources": [ { "name": "folders", @@ -118,7 +118,7 @@ func TestIntegrationFoldersApp(t *testing.T) { DisableAnonymous: true, APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { + folders.RESOURCEGROUP: { DualWriterMode: grafanarest.Mode0, }, }, @@ -134,7 +134,7 @@ func TestIntegrationFoldersApp(t *testing.T) { DisableAnonymous: true, APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { + folders.RESOURCEGROUP: { DualWriterMode: grafanarest.Mode1, }, }, @@ -150,7 +150,7 @@ func TestIntegrationFoldersApp(t *testing.T) { DisableAnonymous: true, APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { + folders.RESOURCEGROUP: { DualWriterMode: grafanarest.Mode1, }, }, @@ -167,7 +167,7 @@ func TestIntegrationFoldersApp(t *testing.T) { DisableAnonymous: true, APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { + folders.RESOURCEGROUP: { DualWriterMode: grafanarest.Mode1, }, }, @@ -184,7 +184,7 @@ func TestIntegrationFoldersApp(t *testing.T) { DisableAnonymous: true, APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { + folders.RESOURCEGROUP: { DualWriterMode: grafanarest.Mode1, }, }, @@ -201,7 +201,7 @@ func TestIntegrationFoldersApp(t *testing.T) { DisableAnonymous: true, APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { + folders.RESOURCEGROUP: { DualWriterMode: grafanarest.Mode1, }, }, @@ -238,7 +238,7 @@ func doFolderTests(t *testing.T, helper *apis.K8sTestHelper) *apis.K8sTestHelper require.NotEmpty(t, uid) expectedResult := `{ - "apiVersion": "folder.grafana.app/v0alpha1", + "apiVersion": "folder.grafana.app/v1", "kind": "Folder", "metadata": { "creationTimestamp": "${creationTimestamp}", @@ -571,7 +571,7 @@ func TestIntegrationFolderCreatePermissions(t *testing.T) { DisableAnonymous: true, APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { + folders.RESOURCEGROUP: { DualWriterMode: grafanarest.Mode1, }, }, @@ -673,7 +673,7 @@ func TestIntegrationFolderGetPermissions(t *testing.T) { DisableAnonymous: true, APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { + folders.RESOURCEGROUP: { DualWriterMode: grafanarest.Mode1, }, }, @@ -850,7 +850,7 @@ func TestFoldersCreateAPIEndpointK8S(t *testing.T) { DisableAnonymous: true, APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { + folders.RESOURCEGROUP: { DualWriterMode: grafanarest.Mode1, }, }, @@ -1020,7 +1020,7 @@ func TestFoldersGetAPIEndpointK8S(t *testing.T) { DisableAnonymous: true, APIServerStorageType: "unified", UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{ - folderv0alpha1.RESOURCEGROUP: { + folders.RESOURCEGROUP: { DualWriterMode: modeDw, }, }, diff --git a/pkg/tests/apis/folder/testdata/folder-generate.yaml b/pkg/tests/apis/folder/testdata/folder-generate.yaml index a4c55dbaf5e..81e5185e0ad 100644 --- a/pkg/tests/apis/folder/testdata/folder-generate.yaml +++ b/pkg/tests/apis/folder/testdata/folder-generate.yaml @@ -1,4 +1,4 @@ -apiVersion: folder.grafana.app/v0alpha1 +apiVersion: folder.grafana.app/v1 kind: Folder metadata: generateName: x # anything is ok here... except yes or true -- they become boolean! diff --git a/pkg/tests/apis/folder/testdata/folder-test-create.yaml b/pkg/tests/apis/folder/testdata/folder-test-create.yaml index defcee9ac1e..f2483080dad 100644 --- a/pkg/tests/apis/folder/testdata/folder-test-create.yaml +++ b/pkg/tests/apis/folder/testdata/folder-test-create.yaml @@ -1,4 +1,4 @@ -apiVersion: folder.grafana.app/v0alpha1 +apiVersion: folder.grafana.app/v1 kind: Folder metadata: name: test diff --git a/pkg/tests/apis/folder/testdata/folder-test-replace.yaml b/pkg/tests/apis/folder/testdata/folder-test-replace.yaml index 493cd0db4c3..2eaf1e95403 100644 --- a/pkg/tests/apis/folder/testdata/folder-test-replace.yaml +++ b/pkg/tests/apis/folder/testdata/folder-test-replace.yaml @@ -1,4 +1,4 @@ -apiVersion: folder.grafana.app/v0alpha1 +apiVersion: folder.grafana.app/v1 kind: Folder metadata: name: test diff --git a/pkg/tests/apis/openapi_snapshots/folder.grafana.app-v1.json b/pkg/tests/apis/openapi_snapshots/folder.grafana.app-v1.json new file mode 100644 index 00000000000..cca328392f3 --- /dev/null +++ b/pkg/tests/apis/openapi_snapshots/folder.grafana.app-v1.json @@ -0,0 +1,1822 @@ +{ + "openapi": "3.0.0", + "info": { + "description": "Grafana folders", + "title": "folder.grafana.app/v1" + }, + "paths": { + "/apis/folder.grafana.app/v1/": { + "get": { + "tags": [ + "API Discovery" + ], + "description": "Describe the available kubernetes resources", + "operationId": "getAPIResources", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList" + } + } + } + } + } + } + }, + "/apis/folder.grafana.app/v1/namespaces/{namespace}/folders": { + "get": { + "tags": [ + "Folder" + ], + "description": "list objects of kind Folder", + "operationId": "listFolder", + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.FolderList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.FolderList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.FolderList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.FolderList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.FolderList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v1", + "kind": "Folder" + } + }, + "post": { + "tags": [ + "Folder" + ], + "description": "create a Folder", + "operationId": "createFolder", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + } + } + } + }, + "x-kubernetes-action": "post", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v1", + "kind": "Folder" + } + }, + "delete": { + "tags": [ + "Folder" + ], + "description": "delete collection of Folder", + "operationId": "deletecollectionFolder", + "parameters": [ + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "deletecollection", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v1", + "kind": "Folder" + } + }, + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/folder.grafana.app/v1/namespaces/{namespace}/folders/{name}": { + "get": { + "tags": [ + "Folder" + ], + "description": "read the specified Folder", + "operationId": "getFolder", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v1", + "kind": "Folder" + } + }, + "put": { + "tags": [ + "Folder" + ], + "description": "replace the specified Folder", + "operationId": "replaceFolder", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + } + } + } + }, + "x-kubernetes-action": "put", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v1", + "kind": "Folder" + } + }, + "delete": { + "tags": [ + "Folder" + ], + "description": "delete a Folder", + "operationId": "deleteFolder", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v1", + "kind": "Folder" + } + }, + "patch": { + "tags": [ + "Folder" + ], + "description": "partially update the specified Folder", + "operationId": "updateFolder", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldManager", + "in": "query", + "description": "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldValidation", + "in": "query", + "description": "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "force", + "in": "query", + "description": "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ], + "requestBody": { + "content": { + "application/apply-patch+yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/json-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + }, + "application/strategic-merge-patch+json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Patch" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + } + } + }, + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + } + } + } + }, + "x-kubernetes-action": "patch", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v1", + "kind": "Folder" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Folder", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/folder.grafana.app/v1/namespaces/{namespace}/folders/{name}/access": { + "get": { + "tags": [ + "Folder" + ], + "description": "connect GET requests to access of Folder", + "operationId": "getFolderAccess", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.FolderAccessInfo" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v1", + "kind": "FolderAccessInfo" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the FolderAccessInfo", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/folder.grafana.app/v1/namespaces/{namespace}/folders/{name}/counts": { + "get": { + "tags": [ + "Folder" + ], + "description": "connect GET requests to counts of Folder", + "operationId": "getFolderCounts", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.DescendantCounts" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v1", + "kind": "DescendantCounts" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the DescendantCounts", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/folder.grafana.app/v1/namespaces/{namespace}/folders/{name}/parents": { + "get": { + "tags": [ + "Folder" + ], + "description": "connect GET requests to parents of Folder", + "operationId": "getFolderParents", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.FolderInfoList" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "folder.grafana.app", + "version": "v1", + "kind": "FolderInfoList" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the FolderInfoList", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + } + }, + "components": { + "schemas": { + "com.github.grafana.grafana.pkg.apis.folder.v1.DescendantCounts": { + "type": "object", + "required": [ + "counts" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "counts": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.ResourceStats" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "DescendantCounts", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "DescendantCounts", + "version": "v1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v1.Folder": { + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Spec" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "Folder", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "Folder", + "version": "v1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v1.FolderAccessInfo": { + "description": "Access control information for the current user", + "type": "object", + "required": [ + "canSave", + "canEdit", + "canAdmin", + "canDelete" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "canAdmin": { + "type": "boolean", + "default": false + }, + "canDelete": { + "type": "boolean", + "default": false + }, + "canEdit": { + "type": "boolean", + "default": false + }, + "canSave": { + "type": "boolean", + "default": false + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "FolderAccessInfo", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "FolderAccessInfo", + "version": "v1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v1.FolderInfo": { + "description": "FolderInfo briefly describes a folder -- unlike a folder resource, this is a partial record of the folder metadata used for navigating parents and children", + "type": "object", + "required": [ + "name", + "title" + ], + "properties": { + "description": { + "description": "The folder description", + "type": "string" + }, + "detached": { + "description": "This folder does not resolve", + "type": "boolean" + }, + "name": { + "description": "Name is the k8s name (eg, the unique identifier) for a folder", + "type": "string", + "default": "" + }, + "parent": { + "description": "The parent folder UID", + "type": "string" + }, + "title": { + "description": "Title is the display value", + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.pkg.apis.folder.v1.FolderInfoList": { + "description": "FolderInfoList returns a list of folder references (parents or children) Unlike FolderList, each item is not a full k8s object", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.FolderInfo" + } + ] + }, + "x-kubernetes-list-map-keys": [ + "uid" + ], + "x-kubernetes-list-type": "map" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "FolderInfoList", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "FolderInfoList", + "version": "v1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v1.FolderList": { + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.pkg.apis.folder.v1.Folder" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "folder.grafana.app", + "kind": "FolderList", + "version": "__internal" + }, + { + "group": "folder.grafana.app", + "kind": "FolderList", + "version": "v1" + } + ] + }, + "com.github.grafana.grafana.pkg.apis.folder.v1.ResourceStats": { + "type": "object", + "required": [ + "group", + "resource", + "count" + ], + "properties": { + "count": { + "type": "integer", + "format": "int64", + "default": 0 + }, + "group": { + "type": "string", + "default": "" + }, + "resource": { + "type": "string", + "default": "" + } + } + }, + "com.github.grafana.grafana.pkg.apis.folder.v1.Spec": { + "type": "object", + "required": [ + "title" + ], + "properties": { + "description": { + "description": "Describe the feature toggle", + "type": "string" + }, + "title": { + "description": "Describe the feature toggle", + "type": "string", + "default": "" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResource": { + "description": "APIResource specifies the name of a resource and whether it is namespaced.", + "type": "object", + "required": [ + "name", + "singularName", + "namespaced", + "kind", + "verbs" + ], + "properties": { + "categories": { + "description": "categories is a list of the grouped resources this resource belongs to (e.g. 'all')", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "group is the preferred group of the resource. Empty implies the group of the containing resource list. For subresources, this may have a different value, for example: Scale\".", + "type": "string" + }, + "kind": { + "description": "kind is the kind for the resource (e.g. 'Foo' is the kind for a resource 'foo')", + "type": "string", + "default": "" + }, + "name": { + "description": "name is the plural name of the resource.", + "type": "string", + "default": "" + }, + "namespaced": { + "description": "namespaced indicates if a resource is namespaced or not.", + "type": "boolean", + "default": false + }, + "shortNames": { + "description": "shortNames is a list of suggested short names of the resource.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "singularName": { + "description": "singularName is the singular name of the resource. This allows clients to handle plural and singular opaquely. The singularName is more correct for reporting status on a single item and both singular and plural are allowed from the kubectl CLI interface.", + "type": "string", + "default": "" + }, + "storageVersionHash": { + "description": "The hash value of the storage version, the version this resource is converted to when written to the data store. Value must be treated as opaque by clients. Only equality comparison on the value is valid. This is an alpha feature and may change or be removed in the future. The field is populated by the apiserver only if the StorageVersionHash feature gate is enabled. This field will remain optional even if it graduates.", + "type": "string" + }, + "verbs": { + "description": "verbs is a list of supported kube verbs (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy)", + "type": "array", + "items": { + "type": "string", + "default": "" + } + }, + "version": { + "description": "version is the preferred version of the resource. Empty implies the version of the containing resource list For subresources, this may have a different value, for example: v1 (while inside a v1beta1 version of the core resource's group)\".", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.APIResourceList": { + "description": "APIResourceList is a list of APIResource, it is used to expose the name of the resources supported in a specific group and version, and if the resource is namespaced.", + "type": "object", + "required": [ + "groupVersion", + "resources" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "groupVersion": { + "description": "groupVersion is the group and version this APIResourceList is for.", + "type": "string", + "default": "" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "resources": { + "description": "resources contains the name of the resources and if they are namespaced.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.APIResource" + } + ] + }, + "x-kubernetes-list-type": "atomic" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions": { + "description": "DeleteOptions may be provided when deleting an API object.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "dryRun": { + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "atomic" + }, + "gracePeriodSeconds": { + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "type": "integer", + "format": "int64" + }, + "ignoreStoreReadErrorWithClusterBreakingPotential": { + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "type": "boolean" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "orphanDependents": { + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "type": "boolean" + }, + "preconditions": { + "description": "Must be fulfilled before a deletion is carried out. If not possible, a 409 Conflict status will be returned.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions" + } + ] + }, + "propagationPolicy": { + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1": { + "description": "FieldsV1 stores a set of fields in a data structure like a Trie, in JSON format.\n\nEach key is either a '.' representing the field itself, and will always map to an empty set, or a string representing a sub-field or item. The string will follow one of these four formats: 'f:\u003cname\u003e', where \u003cname\u003e is the name of a field in a struct, or key in a map 'v:\u003cvalue\u003e', where \u003cvalue\u003e is the exact json formatted value of a list item 'i:\u003cindex\u003e', where \u003cindex\u003e is position of a item in a list 'k:\u003ckeys\u003e', where \u003ckeys\u003e is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta": { + "description": "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", + "type": "object", + "properties": { + "continue": { + "description": "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", + "type": "string" + }, + "remainingItemCount": { + "description": "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", + "type": "integer", + "format": "int64" + }, + "resourceVersion": { + "description": "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry": { + "description": "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + "type": "string" + }, + "fieldsType": { + "description": "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + "type": "string" + }, + "fieldsV1": { + "description": "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1" + } + ] + }, + "manager": { + "description": "Manager is an identifier of the workflow managing these fields.", + "type": "string" + }, + "operation": { + "description": "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + "type": "string" + }, + "subresource": { + "description": "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + "type": "string" + }, + "time": { + "description": "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta": { + "description": "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + "type": "object", + "properties": { + "annotations": { + "description": "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "creationTimestamp": { + "description": "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "deletionGracePeriodSeconds": { + "description": "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + "type": "integer", + "format": "int64" + }, + "deletionTimestamp": { + "description": "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Time" + } + ] + }, + "finalizers": { + "description": "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + "type": "array", + "items": { + "type": "string", + "default": "" + }, + "x-kubernetes-list-type": "set", + "x-kubernetes-patch-strategy": "merge" + }, + "generateName": { + "description": "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + "type": "string" + }, + "generation": { + "description": "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + "type": "integer", + "format": "int64" + }, + "labels": { + "description": "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + "type": "object", + "additionalProperties": { + "type": "string", + "default": "" + } + }, + "managedFields": { + "description": "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "name": { + "description": "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string" + }, + "namespace": { + "description": "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + "type": "string" + }, + "ownerReferences": { + "description": "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference" + } + ] + }, + "x-kubernetes-list-map-keys": [ + "uid" + ], + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge" + }, + "resourceVersion": { + "description": "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "selfLink": { + "description": "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + "type": "string" + }, + "uid": { + "description": "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference": { + "description": "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + "type": "object", + "required": [ + "apiVersion", + "kind", + "name", + "uid" + ], + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string", + "default": "" + }, + "blockOwnerDeletion": { + "description": "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + "type": "boolean" + }, + "controller": { + "description": "If true, this reference points to the managing controller.", + "type": "boolean" + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string", + "default": "" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + "type": "string", + "default": "" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string", + "default": "" + } + }, + "x-kubernetes-map-type": "atomic" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Patch": { + "description": "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", + "type": "object" + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions": { + "description": "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", + "type": "object", + "properties": { + "resourceVersion": { + "description": "Specifies the target ResourceVersion", + "type": "string" + }, + "uid": { + "description": "Specifies the target UID.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Status": { + "description": "Status is a return value for calls that don't return other objects.", + "type": "object", + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "code": { + "description": "Suggested HTTP return code for this status, 0 if not set.", + "type": "integer", + "format": "int32" + }, + "details": { + "description": "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails" + } + ], + "x-kubernetes-list-type": "atomic" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "message": { + "description": "A human-readable description of the status of this operation.", + "type": "string" + }, + "metadata": { + "description": "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + }, + "reason": { + "description": "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", + "type": "string" + }, + "status": { + "description": "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause": { + "description": "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", + "type": "object", + "properties": { + "field": { + "description": "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", + "type": "string" + }, + "message": { + "description": "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", + "type": "string" + }, + "reason": { + "description": "A machine-readable description of the cause of the error. If this value is empty there is no information available.", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails": { + "description": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", + "type": "object", + "properties": { + "causes": { + "description": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause" + } + ] + }, + "x-kubernetes-list-type": "atomic" + }, + "group": { + "description": "The group attribute of the resource associated with the status StatusReason.", + "type": "string" + }, + "kind": { + "description": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", + "type": "string" + }, + "retryAfterSeconds": { + "description": "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", + "type": "integer", + "format": "int32" + }, + "uid": { + "description": "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + "type": "string" + } + } + }, + "io.k8s.apimachinery.pkg.apis.meta.v1.Time": { + "description": "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + "type": "string", + "format": "date-time" + } + } + } +} \ No newline at end of file diff --git a/pkg/tests/apis/openapi_test.go b/pkg/tests/apis/openapi_test.go index e2a9f7c9700..4ab3464d928 100644 --- a/pkg/tests/apis/openapi_test.go +++ b/pkg/tests/apis/openapi_test.go @@ -71,7 +71,7 @@ func TestIntegrationOpenAPIs(t *testing.T) { Version: "v2alpha1", }, { Group: "folder.grafana.app", - Version: "v0alpha1", + Version: "v1", }, { Group: "provisioning.grafana.app", Version: "v0alpha1", diff --git a/pkg/tests/apis/provisioning/helper_test.go b/pkg/tests/apis/provisioning/helper_test.go index c503d1c11aa..d59a01421ca 100644 --- a/pkg/tests/apis/provisioning/helper_test.go +++ b/pkg/tests/apis/provisioning/helper_test.go @@ -24,7 +24,7 @@ import ( "k8s.io/client-go/rest" dashboard "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1alpha1" - folder "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" + folder "github.com/grafana/grafana/pkg/apis/folder/v1" provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/registry/apis/provisioning/jobs" From 1291b60bbee0e5027eeafc5172ecf1febcb6d1a8 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 11 Apr 2025 15:32:02 +0300 Subject: [PATCH 37/37] Checkbox: Add z-index to description (#103847) * Checkbox: Add z-index to description * Add comment --- packages/grafana-ui/src/components/Forms/Checkbox.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/grafana-ui/src/components/Forms/Checkbox.tsx b/packages/grafana-ui/src/components/Forms/Checkbox.tsx index 3e5e2693f80..fa3f9222b71 100644 --- a/packages/grafana-ui/src/components/Forms/Checkbox.tsx +++ b/packages/grafana-ui/src/components/Forms/Checkbox.tsx @@ -212,6 +212,8 @@ export const getCheckboxStyles = (theme: GrafanaTheme2, invalid = false) => { gridRowStart: 2, lineHeight: theme.typography.bodySmall.lineHeight, marginTop: 0 /* The margin effectively comes from the top: -2px on the label above it */, + // Enable interacting with description when checkbox is disabled + zIndex: 1, }) ), };