diff --git a/pkg/apimachinery/identity/namespace.go b/pkg/apimachinery/identity/namespace.go
index 2b72ae71f58..aa3a38d3994 100644
--- a/pkg/apimachinery/identity/namespace.go
+++ b/pkg/apimachinery/identity/namespace.go
@@ -15,7 +15,6 @@ const (
NamespaceAnonymous Namespace = "anonymous"
NamespaceRenderService Namespace = "render"
NamespaceAccessPolicy Namespace = "access-policy"
- NamespaceProvisioning Namespace = "provisioning"
NamespaceEmpty Namespace = ""
)
diff --git a/pkg/registry/apis/dashboard/access/sql_dashboards.go b/pkg/registry/apis/dashboard/access/sql_dashboards.go
new file mode 100644
index 00000000000..86bbbfe1239
--- /dev/null
+++ b/pkg/registry/apis/dashboard/access/sql_dashboards.go
@@ -0,0 +1,409 @@
+package access
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "path/filepath"
+ "strings"
+ "time"
+
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
+
+ "github.com/grafana/grafana/pkg/apimachinery/utils"
+ dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/infra/appcontext"
+ "github.com/grafana/grafana/pkg/infra/db"
+ "github.com/grafana/grafana/pkg/services/accesscontrol"
+ "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
+ gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
+ "github.com/grafana/grafana/pkg/services/dashboards"
+ "github.com/grafana/grafana/pkg/services/provisioning"
+ "github.com/grafana/grafana/pkg/services/sqlstore/session"
+)
+
+var (
+ _ DashboardAccess = (*dashboardSqlAccess)(nil)
+)
+
+type dashboardRow struct {
+ // Dashboard resource
+ Dash *dashboardsV0.Dashboard
+
+ // Title -- this may come from saved metadata rather than the body
+ Title string
+
+ // The folder UID (needed for access control checks)
+ FolderUID string
+
+ // Needed for fast summary access
+ Tags []string
+
+ // Size (in bytes) of the dashboard payload
+ Bytes int
+
+ // The token we can use that will start a new connection that includes
+ // this same dashboard
+ token *continueToken
+}
+
+type dashboardSqlAccess struct {
+ sql db.DB
+ sess *session.SessionDB
+ namespacer request.NamespaceMapper
+ dashStore dashboards.Store
+ provisioning provisioning.ProvisioningService
+}
+
+func NewDashboardAccess(sql db.DB, namespacer request.NamespaceMapper, dashStore dashboards.Store, provisioning provisioning.ProvisioningService) DashboardAccess {
+ return &dashboardSqlAccess{
+ sql: sql,
+ sess: sql.GetSqlxSession(),
+ namespacer: namespacer,
+ dashStore: dashStore,
+ provisioning: provisioning,
+ }
+}
+
+const selector = `SELECT
+ dashboard.org_id, dashboard.id,
+ dashboard.uid,slug,
+ dashboard.folder_uid,
+ dashboard.created,dashboard.created_by,CreatedUSER.login,
+ dashboard.updated,dashboard.updated_by,UpdatedUSER.login,
+ plugin_id,
+ dashboard_provisioning.name as origin_name,
+ dashboard_provisioning.external_id as origin_path,
+ dashboard_provisioning.check_sum as origin_key,
+ dashboard_provisioning.updated as origin_ts,
+ dashboard.version,
+ title,
+ dashboard.data
+ FROM dashboard
+ LEFT OUTER JOIN dashboard_provisioning ON dashboard.id = dashboard_provisioning.dashboard_id
+ LEFT OUTER JOIN user AS CreatedUSER ON dashboard.created_by = CreatedUSER.id
+ LEFT OUTER JOIN user AS UpdatedUSER ON dashboard.created_by = UpdatedUSER.id
+ WHERE is_folder = false`
+
+func (a *dashboardSqlAccess) getRows(ctx context.Context, query *DashboardQuery, onlySummary bool) (*rowsWrapper, int, error) {
+ if !query.Labels.Empty() {
+ return nil, 0, fmt.Errorf("label selection not yet supported")
+ }
+ if len(query.Requirements.SortBy) > 0 {
+ return nil, 0, fmt.Errorf("sorting not yet supported")
+ }
+ if query.Requirements.ListHistory != "" {
+ return nil, 0, fmt.Errorf("ListHistory not yet supported")
+ }
+ if query.Requirements.ListDeleted {
+ return nil, 0, fmt.Errorf("ListDeleted not yet supported")
+ }
+
+ token, err := readContinueToken(query)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ limit := query.Limit
+ if limit < 1 {
+ limit = 15 //
+ }
+ args := []any{query.OrgID}
+
+ sqlcmd := selector
+
+ // We can not do this yet because title + tags are in the body
+ if onlySummary && false {
+ sqlcmd = strings.Replace(sqlcmd, "dashboard.data", `"{}"`, 1)
+ }
+
+ sqlcmd = fmt.Sprintf("%s AND dashboard.org_id=$%d", sqlcmd, len(args))
+ if query.UID != "" {
+ args = append(args, query.UID)
+ sqlcmd = fmt.Sprintf("%s AND dashboard.uid=$%d", sqlcmd, len(args))
+ } else {
+ args = append(args, token.id)
+ sqlcmd = fmt.Sprintf("%s AND dashboard.id>=$%d", sqlcmd, len(args))
+ }
+
+ if query.Requirements.Folder != nil {
+ args = append(args, *query.Requirements.Folder)
+ sqlcmd = fmt.Sprintf("%s AND dashboard.folder_uid=$%d", sqlcmd, len(args))
+ }
+
+ args = append(args, (limit + 2)) // add more so we can include a next token
+ sqlcmd = fmt.Sprintf("%s ORDER BY dashboard.id asc LIMIT $%d", sqlcmd, len(args))
+
+ rows, err := a.doQuery(ctx, sqlcmd, args...)
+ if err != nil {
+ if rows != nil {
+ _ = rows.Close()
+ }
+ rows = nil
+ }
+ return rows, limit, err
+}
+
+// GetDashboards implements DashboardAccess.
+func (a *dashboardSqlAccess) GetDashboards(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardList, error) {
+ rows, limit, err := a.getRows(ctx, query, false)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = rows.Close() }()
+
+ totalSize := 0
+ list := &dashboardsV0.DashboardList{}
+ for {
+ row, err := rows.Next()
+ if err != nil || row == nil {
+ return list, err
+ }
+
+ totalSize += row.Bytes
+ if len(list.Items) > 0 && (totalSize > query.MaxBytes || len(list.Items) >= limit) {
+ if query.Requirements.Folder != nil {
+ row.token.folder = *query.Requirements.Folder
+ }
+ list.Continue = row.token.String() // will skip this one but start here next time
+ return list, err
+ }
+ list.Items = append(list.Items, *row.Dash)
+ }
+}
+
+func (a *dashboardSqlAccess) GetDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, error) {
+ r, err := a.GetDashboards(ctx, &DashboardQuery{
+ OrgID: orgId,
+ UID: uid,
+ Labels: labels.Everything(),
+ })
+ if err != nil {
+ return nil, err
+ }
+ if len(r.Items) > 0 {
+ return &r.Items[0], nil
+ }
+ return nil, fmt.Errorf("not found")
+}
+
+func (a *dashboardSqlAccess) doQuery(ctx context.Context, query string, args ...any) (*rowsWrapper, error) {
+ user, err := appcontext.User(ctx)
+ if err != nil {
+ return nil, err
+ }
+ rows, err := a.sess.Query(ctx, query, args...)
+ return &rowsWrapper{
+ rows: rows,
+ a: a,
+ // This looks up rules from the permissions on a user
+ canReadDashboard: accesscontrol.Checker(user, dashboards.ActionDashboardsRead),
+ }, err
+}
+
+type rowsWrapper struct {
+ a *dashboardSqlAccess
+ rows *sql.Rows
+ idx int
+ total int64
+
+ canReadDashboard func(scopes ...string) bool
+}
+
+func (r *rowsWrapper) Close() error {
+ return r.rows.Close()
+}
+
+func (r *rowsWrapper) Next() (*dashboardRow, error) {
+ // breaks after first readable value
+ for r.rows.Next() {
+ r.idx++
+ d, err := r.a.scanRow(r.rows)
+ if d != nil {
+ // Access control checker
+ scopes := []string{dashboards.ScopeDashboardsProvider.GetResourceScopeUID(d.Dash.Name)}
+ if d.FolderUID != "" { // Copied from searchV2... not sure the logic is right
+ scopes = append(scopes, dashboards.ScopeFoldersProvider.GetResourceScopeUID(d.FolderUID))
+ }
+ if !r.canReadDashboard(scopes...) {
+ continue
+ }
+ d.token.size = r.total // size before next!
+ r.total += int64(d.Bytes)
+ }
+
+ // returns the first folder it can
+ return d, err
+ }
+ return nil, nil
+}
+
+func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) {
+ dash := &dashboardsV0.Dashboard{
+ TypeMeta: dashboardsV0.DashboardResourceInfo.TypeMeta(),
+ ObjectMeta: v1.ObjectMeta{Annotations: make(map[string]string)},
+ }
+ row := &dashboardRow{Dash: dash}
+
+ var dashboard_id int64
+ var orgId int64
+ var slug string
+ var folder_uid sql.NullString
+ var updated time.Time
+ var updatedByID int64
+ var updatedByName sql.NullString
+
+ var created time.Time
+ var createdByID int64
+ var createdByName sql.NullString
+
+ var plugin_id string
+ var origin_name sql.NullString
+ var origin_path sql.NullString
+ var origin_ts sql.NullInt64
+ var origin_hash sql.NullString
+ var data []byte // the dashboard JSON
+ var version int64
+
+ err := rows.Scan(&orgId, &dashboard_id, &dash.Name,
+ &slug, &folder_uid,
+ &created, &createdByID, &createdByName,
+ &updated, &updatedByID, &updatedByName,
+ &plugin_id,
+ &origin_name, &origin_path, &origin_hash, &origin_ts,
+ &version,
+ &row.Title, &data,
+ )
+
+ row.token = &continueToken{orgId: orgId, id: dashboard_id}
+ if err == nil {
+ dash.ResourceVersion = fmt.Sprintf("%d", created.UnixMilli())
+ dash.Namespace = a.namespacer(orgId)
+ dash.UID = gapiutil.CalculateClusterWideUID(dash)
+ dash.SetCreationTimestamp(v1.NewTime(created))
+ meta, err := utils.MetaAccessor(dash)
+ if err != nil {
+ return nil, err
+ }
+ meta.SetUpdatedTimestamp(&updated)
+ meta.SetSlug(slug)
+ if createdByID > 0 {
+ meta.SetCreatedBy(fmt.Sprintf("user:%d/%s", createdByID, createdByName.String))
+ }
+ if updatedByID > 0 {
+ meta.SetUpdatedBy(fmt.Sprintf("user:%d/%s", updatedByID, updatedByName.String))
+ }
+ if folder_uid.Valid {
+ meta.SetFolder(folder_uid.String)
+ row.FolderUID = folder_uid.String
+ }
+
+ if origin_name.Valid {
+ ts := time.Unix(origin_ts.Int64, 0)
+
+ resolvedPath := a.provisioning.GetDashboardProvisionerResolvedPath(origin_name.String)
+ originPath, err := filepath.Rel(
+ resolvedPath,
+ origin_path.String,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ meta.SetOriginInfo(&utils.ResourceOriginInfo{
+ Name: origin_name.String,
+ Path: originPath,
+ Hash: origin_hash.String,
+ Timestamp: &ts,
+ })
+ } else if plugin_id != "" {
+ meta.SetOriginInfo(&utils.ResourceOriginInfo{
+ Name: "plugin",
+ Path: plugin_id,
+ })
+ }
+
+ row.Bytes = len(data)
+ if row.Bytes > 0 {
+ err = dash.Spec.UnmarshalJSON(data)
+ if err != nil {
+ return row, err
+ }
+ dash.Spec.Set("id", dashboard_id) // add it so we can get it from the body later
+ row.Title = dash.Spec.GetNestedString("title")
+ row.Tags = dash.Spec.GetNestedStringSlice("tags")
+ }
+ }
+ return row, err
+}
+
+// DeleteDashboard implements DashboardAccess.
+func (a *dashboardSqlAccess) DeleteDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, bool, error) {
+ dash, err := a.GetDashboard(ctx, orgId, uid)
+ if err != nil {
+ return nil, false, err
+ }
+
+ id := dash.Spec.GetNestedInt64("id")
+ if id == 0 {
+ return nil, false, fmt.Errorf("could not find id in saved body")
+ }
+
+ err = a.dashStore.DeleteDashboard(ctx, &dashboards.DeleteDashboardCommand{
+ OrgID: orgId,
+ ID: id,
+ })
+ if err != nil {
+ return nil, false, err
+ }
+ return dash, true, nil
+}
+
+// SaveDashboard implements DashboardAccess.
+func (a *dashboardSqlAccess) SaveDashboard(ctx context.Context, orgId int64, dash *dashboardsV0.Dashboard) (*dashboardsV0.Dashboard, bool, error) {
+ created := false
+ user, err := appcontext.User(ctx)
+ if err != nil {
+ return nil, created, err
+ }
+ if dash.Name != "" {
+ dash.Spec.Set("uid", dash.Name)
+
+ // Get the previous version to set the internal ID
+ old, _ := a.dashStore.GetDashboard(ctx, &dashboards.GetDashboardQuery{
+ OrgID: orgId,
+ UID: dash.Name,
+ })
+ if old != nil {
+ dash.Spec.Set("id", old.ID)
+ } else {
+ dash.Spec.Remove("id") // existing of "id" makes it an update
+ created = true
+ }
+ } else {
+ dash.Spec.Remove("id")
+ dash.Spec.Remove("uid")
+ }
+
+ meta, err := utils.MetaAccessor(dash)
+ if err != nil {
+ return nil, false, err
+ }
+ out, err := a.dashStore.SaveDashboard(ctx, dashboards.SaveDashboardCommand{
+ OrgID: orgId,
+ Dashboard: simplejson.NewFromAny(dash.Spec.UnstructuredContent()),
+ FolderUID: meta.GetFolder(),
+ Overwrite: true, // already passed the revisionVersion checks!
+ UserID: user.UserID,
+ })
+ if err != nil {
+ return nil, false, err
+ }
+ if out != nil {
+ created = (out.Created.Unix() == out.Updated.Unix()) // and now?
+ }
+ dash, err = a.GetDashboard(ctx, orgId, out.UID)
+ return dash, created, err
+}
diff --git a/pkg/registry/apis/dashboard/access/token.go b/pkg/registry/apis/dashboard/access/token.go
new file mode 100644
index 00000000000..f8b32bbcb5d
--- /dev/null
+++ b/pkg/registry/apis/dashboard/access/token.go
@@ -0,0 +1,67 @@
+package access
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+
+ "github.com/grafana/grafana/pkg/util"
+)
+
+type continueToken struct {
+ orgId int64
+ id int64 // the internal id (sort by!)
+ folder string // from the query
+ size int64
+}
+
+func readContinueToken(q *DashboardQuery) (continueToken, error) {
+ var err error
+ token := continueToken{}
+ if q.ContinueToken == "" {
+ return token, nil
+ }
+ parts := strings.Split(q.ContinueToken, "/")
+ if len(parts) < 3 {
+ return token, fmt.Errorf("invalid continue token (too few parts)")
+ }
+ sub := strings.Split(parts[0], ":")
+ if sub[0] != "org" {
+ return token, fmt.Errorf("expected org in first slug")
+ }
+ token.orgId, err = strconv.ParseInt(sub[1], 10, 64)
+ if err != nil {
+ return token, fmt.Errorf("error parsing orgid")
+ }
+
+ sub = strings.Split(parts[1], ":")
+ if sub[0] != "start" {
+ return token, fmt.Errorf("expected internal ID in second slug")
+ }
+ token.id, err = strconv.ParseInt(sub[1], 10, 64)
+ if err != nil {
+ return token, fmt.Errorf("error parsing updated")
+ }
+
+ sub = strings.Split(parts[2], ":")
+ if sub[0] != "folder" {
+ return token, fmt.Errorf("expected folder UID in third slug")
+ }
+ token.folder = sub[1]
+
+ // Check if the folder filter is the same from the previous query
+ if q.Requirements.Folder == nil {
+ if token.folder != "" {
+ return token, fmt.Errorf("invalid token, the folder must match previous query")
+ }
+ } else if token.folder != *q.Requirements.Folder {
+ return token, fmt.Errorf("invalid token, the folder must match previous query")
+ }
+
+ return token, err
+}
+
+func (r *continueToken) String() string {
+ return fmt.Sprintf("org:%d/start:%d/folder:%s/%s",
+ r.orgId, r.id, r.folder, util.ByteCountSI(r.size))
+}
diff --git a/pkg/registry/apis/dashboard/access/types.go b/pkg/registry/apis/dashboard/access/types.go
new file mode 100644
index 00000000000..50065c8f2a2
--- /dev/null
+++ b/pkg/registry/apis/dashboard/access/types.go
@@ -0,0 +1,35 @@
+package access
+
+import (
+ "context"
+
+ "k8s.io/apimachinery/pkg/labels"
+
+ dashboardsV0 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
+ "github.com/grafana/grafana/pkg/services/apiserver/storage/entity"
+)
+
+// This does not check if you have permissions!
+
+type DashboardQuery struct {
+ OrgID int64
+ UID string // to select a single dashboard
+ Limit int
+ MaxBytes int
+
+ // FolderUID etc
+ Requirements entity.Requirements
+ // Post processing label filter
+ Labels labels.Selector
+
+ // The token from previous query
+ ContinueToken string
+}
+
+type DashboardAccess interface {
+ GetDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, error)
+ GetDashboards(ctx context.Context, query *DashboardQuery) (*dashboardsV0.DashboardList, error)
+
+ SaveDashboard(ctx context.Context, orgId int64, dash *dashboardsV0.Dashboard) (*dashboardsV0.Dashboard, bool, error)
+ DeleteDashboard(ctx context.Context, orgId int64, uid string) (*dashboardsV0.Dashboard, bool, error)
+}
diff --git a/pkg/registry/apis/dashboard/legacy_storage.go b/pkg/registry/apis/dashboard/legacy_storage.go
index b41c995386f..f08ac34550f 100644
--- a/pkg/registry/apis/dashboard/legacy_storage.go
+++ b/pkg/registry/apis/dashboard/legacy_storage.go
@@ -1,71 +1,170 @@
package dashboard
import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "k8s.io/apimachinery/pkg/apis/meta/internalversion"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apiserver/pkg/registry/generic"
- genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
- grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
- grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
- "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
- "github.com/grafana/grafana/pkg/storage/unified/apistore"
- "github.com/grafana/grafana/pkg/storage/unified/resource"
+ "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
+ "github.com/grafana/grafana/pkg/registry/apis/dashboard/access"
+ "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
+ "github.com/grafana/grafana/pkg/services/apiserver/storage/entity"
+)
+
+var (
+ _ rest.Storage = (*dashboardStorage)(nil)
+ _ rest.Scoper = (*dashboardStorage)(nil)
+ _ rest.SingularNameProvider = (*dashboardStorage)(nil)
+ _ rest.Getter = (*dashboardStorage)(nil)
+ _ rest.Lister = (*dashboardStorage)(nil)
+ _ rest.Creater = (*dashboardStorage)(nil)
+ _ rest.Updater = (*dashboardStorage)(nil)
+ _ rest.GracefulDeleter = (*dashboardStorage)(nil)
)
type dashboardStorage struct {
resource common.ResourceInfo
- access legacy.DashboardAccess
+ access access.DashboardAccess
tableConverter rest.TableConvertor
-
- server resource.ResourceServer
}
-func (s *dashboardStorage) newStore(scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter) (grafanarest.LegacyStorage, error) {
- server, err := resource.NewResourceServer(resource.ResourceServerOptions{
- Backend: s.access,
- Search: s.access,
- Blob: s.access,
- // WriteAccess: resource.WriteAccessHooks{
- // Folder: func(ctx context.Context, user identity.Requester, uid string) bool {
- // // ???
- // },
- // },
- })
+func (s *dashboardStorage) New() runtime.Object {
+ return s.resource.NewFunc()
+}
+
+func (s *dashboardStorage) Destroy() {}
+
+func (s *dashboardStorage) NamespaceScoped() bool {
+ return true
+}
+
+func (s *dashboardStorage) GetSingularName() string {
+ return s.resource.GetSingularName()
+}
+
+func (s *dashboardStorage) NewList() runtime.Object {
+ return s.resource.NewListFunc()
+}
+
+func (s *dashboardStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
+ return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
+}
+
+func (s *dashboardStorage) Create(ctx context.Context,
+ obj runtime.Object,
+ createValidation rest.ValidateObjectFunc,
+ options *metav1.CreateOptions,
+) (runtime.Object, error) {
+ info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
- s.server = server
- resourceInfo := s.resource
- defaultOpts, err := defaultOptsGetter.GetRESTOptions(resourceInfo.GroupResource())
+ p, ok := obj.(*v0alpha1.Dashboard)
+ if !ok {
+ return nil, fmt.Errorf("expected dashboard?")
+ }
+
+ // HACK to simplify unique name testing from kubectl
+ t := p.Spec.GetNestedString("title")
+ if strings.Contains(t, "${NOW}") {
+ t = strings.ReplaceAll(t, "${NOW}", fmt.Sprintf("%d", time.Now().Unix()))
+ p.Spec.Set("title", t)
+ }
+
+ dash, _, err := s.access.SaveDashboard(ctx, info.OrgID, p)
+ return dash, err
+}
+
+func (s *dashboardStorage) Update(ctx context.Context,
+ name string,
+ objInfo rest.UpdatedObjectInfo,
+ createValidation rest.ValidateObjectFunc,
+ updateValidation rest.ValidateObjectUpdateFunc,
+ forceAllowCreate bool,
+ options *metav1.UpdateOptions,
+) (runtime.Object, bool, error) {
+ info, err := request.NamespaceInfoFrom(ctx, true)
+ if err != nil {
+ return nil, false, err
+ }
+
+ created := false
+ old, err := s.Get(ctx, name, nil)
+ if err != nil {
+ return old, created, err
+ }
+
+ obj, err := objInfo.UpdatedObject(ctx, old)
+ if err != nil {
+ return old, created, err
+ }
+ p, ok := obj.(*v0alpha1.Dashboard)
+ if !ok {
+ return nil, created, fmt.Errorf("expected dashboard after update")
+ }
+
+ _, created, err = s.access.SaveDashboard(ctx, info.OrgID, p)
+ if err == nil {
+ r, err := s.Get(ctx, name, nil)
+ return r, created, err
+ }
+ return nil, created, err
+}
+
+// GracefulDeleter
+func (s *dashboardStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
+ info, err := request.NamespaceInfoFrom(ctx, true)
+ if err != nil {
+ return nil, false, err
+ }
+
+ return s.access.DeleteDashboard(ctx, info.OrgID, name)
+}
+
+func (s *dashboardStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
+ orgId, err := request.OrgIDForList(ctx)
if err != nil {
return nil, err
}
- client := resource.NewLocalResourceStoreClient(server)
- optsGetter := apistore.NewRESTOptionsGetter(client,
- defaultOpts.StorageConfig.Codec,
- )
- strategy := grafanaregistry.NewStrategy(scheme)
- store := &genericregistry.Store{
- NewFunc: resourceInfo.NewFunc,
- NewListFunc: resourceInfo.NewListFunc,
- KeyRootFunc: grafanaregistry.KeyRootFunc(resourceInfo.GroupResource()),
- KeyFunc: grafanaregistry.NamespaceKeyFunc(resourceInfo.GroupResource()),
- PredicateFunc: grafanaregistry.Matcher,
- DefaultQualifiedResource: resourceInfo.GroupResource(),
- SingularQualifiedResource: resourceInfo.SingularGroupResource(),
- CreateStrategy: strategy,
- UpdateStrategy: strategy,
- DeleteStrategy: strategy,
- TableConvertor: s.tableConverter,
- }
+ // fmt.Printf("LIST: %s\n", options.Continue)
- options := &generic.StoreOptions{RESTOptions: optsGetter}
- if err := store.CompleteWithOptions(options); err != nil {
+ // translate grafana.app/* label selectors into field requirements
+ requirements, newSelector, err := entity.ReadLabelSelectors(options.LabelSelector)
+ if err != nil {
return nil, err
}
- return store, err
+
+ query := &access.DashboardQuery{
+ OrgID: orgId,
+ Limit: int(options.Limit),
+ MaxBytes: 2 * 1024 * 1024, // 2MB,
+ ContinueToken: options.Continue,
+ Requirements: requirements,
+ Labels: newSelector,
+ }
+
+ return s.access.GetDashboards(ctx, query)
+}
+
+func (s *dashboardStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
+ info, err := request.NamespaceInfoFrom(ctx, true)
+ if err != nil {
+ return nil, err
+ }
+
+ return s.access.GetDashboard(ctx, info.OrgID, name)
+}
+
+// GracefulDeleter
+func (s *dashboardStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
+ return nil, fmt.Errorf("DeleteCollection for dashboards not implemented")
}
diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go
index 269e4ec6c9f..ff30d0b90f5 100644
--- a/pkg/registry/apis/dashboard/register.go
+++ b/pkg/registry/apis/dashboard/register.go
@@ -1,10 +1,6 @@
package dashboard
import (
- "fmt"
- "time"
-
- "github.com/prometheus/client_golang/prometheus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -15,22 +11,22 @@ import (
common "k8s.io/kube-openapi/pkg/common"
"k8s.io/kube-openapi/pkg/spec3"
- dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
+ "github.com/prometheus/client_golang/prometheus"
+
+ "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
- "github.com/grafana/grafana/pkg/infra/tracing"
- "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
+ "github.com/grafana/grafana/pkg/registry/apis/dashboard/access"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
- gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
"github.com/grafana/grafana/pkg/services/dashboards"
+ dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/setting"
- "github.com/grafana/grafana/pkg/storage/unified/apistore"
)
var _ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
@@ -39,8 +35,11 @@ var _ builder.APIGroupBuilder = (*DashboardsAPIBuilder)(nil)
type DashboardsAPIBuilder struct {
dashboardService dashboards.DashboardService
- accessControl accesscontrol.AccessControl
- legacy *dashboardStorage
+ dashboardVersionService dashver.Service
+ accessControl accesscontrol.AccessControl
+ namespacer request.NamespaceMapper
+ access access.DashboardAccess
+ dashStore dashboards.Store
log log.Logger
}
@@ -48,12 +47,12 @@ type DashboardsAPIBuilder struct {
func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
apiregistration builder.APIRegistrar,
dashboardService dashboards.DashboardService,
+ dashboardVersionService dashver.Service,
accessControl accesscontrol.AccessControl,
provisioning provisioning.ProvisioningService,
dashStore dashboards.Store,
reg prometheus.Registerer,
sql db.DB,
- tracing *tracing.TracingService,
) *DashboardsAPIBuilder {
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
return nil // skip registration unless opting into experimental apis
@@ -61,42 +60,20 @@ func RegisterAPIService(cfg *setting.Cfg, features featuremgmt.FeatureToggles,
namespacer := request.GetNamespaceMapper(cfg)
builder := &DashboardsAPIBuilder{
- log: log.New("grafana-apiserver.dashboards"),
-
- dashboardService: dashboardService,
- accessControl: accessControl,
-
- legacy: &dashboardStorage{
- resource: dashboard.DashboardResourceInfo,
- access: legacy.NewDashboardAccess(sql, namespacer, dashStore, provisioning),
- tableConverter: gapiutil.NewTableConverter(
- dashboard.DashboardResourceInfo.GroupResource(),
- []metav1.TableColumnDefinition{
- {Name: "Name", Type: "string", Format: "name"},
- {Name: "Title", Type: "string", Format: "string", Description: "The dashboard name"},
- {Name: "Created At", Type: "date"},
- },
- func(obj any) ([]interface{}, error) {
- dash, ok := obj.(*dashboard.Dashboard)
- if ok {
- if dash != nil {
- return []interface{}{
- dash.Name,
- dash.Spec.GetNestedString("title"),
- dash.CreationTimestamp.UTC().Format(time.RFC3339),
- }, nil
- }
- }
- return nil, fmt.Errorf("expected dashboard or summary")
- }),
- },
+ dashboardService: dashboardService,
+ dashboardVersionService: dashboardVersionService,
+ dashStore: dashStore,
+ accessControl: accessControl,
+ namespacer: namespacer,
+ access: access.NewDashboardAccess(sql, namespacer, dashStore, provisioning),
+ log: log.New("grafana-apiserver.dashboards"),
}
apiregistration.RegisterAPI(builder)
return builder
}
func (b *DashboardsAPIBuilder) GetGroupVersion() schema.GroupVersion {
- return dashboard.DashboardResourceInfo.GroupVersion()
+ return v0alpha1.DashboardResourceInfo.GroupVersion()
}
func (b *DashboardsAPIBuilder) GetDesiredDualWriterMode(dualWrite bool, modeMap map[string]grafanarest.DualWriterMode) grafanarest.DualWriterMode {
@@ -106,18 +83,16 @@ func (b *DashboardsAPIBuilder) GetDesiredDualWriterMode(dualWrite bool, modeMap
func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) {
scheme.AddKnownTypes(gv,
- &dashboard.Dashboard{},
- &dashboard.DashboardList{},
- &dashboard.DashboardWithAccessInfo{},
- &dashboard.DashboardVersionList{},
- &dashboard.VersionsQueryOptions{},
- &metav1.PartialObjectMetadata{},
- &metav1.PartialObjectMetadataList{},
+ &v0alpha1.Dashboard{},
+ &v0alpha1.DashboardList{},
+ &v0alpha1.DashboardWithAccessInfo{},
+ &v0alpha1.DashboardVersionList{},
+ &v0alpha1.VersionsQueryOptions{},
)
}
func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
- resourceInfo := dashboard.DashboardResourceInfo
+ resourceInfo := v0alpha1.DashboardResourceInfo
addKnownTypes(scheme, resourceInfo.GroupVersion())
// Link this version to the internal representation.
@@ -143,44 +118,44 @@ func (b *DashboardsAPIBuilder) GetAPIGroupInfo(
desiredMode grafanarest.DualWriterMode,
reg prometheus.Registerer,
) (*genericapiserver.APIGroupInfo, error) {
- apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(dashboard.GROUP, scheme, metav1.ParameterCodec, codecs)
+ apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(v0alpha1.GROUP, scheme, metav1.ParameterCodec, codecs)
- dash := b.legacy.resource
- legacyStore, err := b.legacy.newStore(scheme, optsGetter)
+ resourceInfo := v0alpha1.DashboardResourceInfo
+ store, err := newStorage(scheme)
if err != nil {
return nil, err
}
+ legacyStore := &dashboardStorage{
+ resource: resourceInfo,
+ access: b.access,
+ tableConverter: store.TableConvertor,
+ }
+
storage := map[string]rest.Storage{}
- storage[dash.StoragePath()] = legacyStore
- storage[dash.StoragePath("dto")] = &DTOConnector{
+ storage[resourceInfo.StoragePath()] = legacyStore
+ storage[resourceInfo.StoragePath("dto")] = &DTOConnector{
+ builder: b,
+ }
+ storage[resourceInfo.StoragePath("versions")] = &VersionsREST{
builder: b,
}
- storage[dash.StoragePath("history")] = apistore.NewHistoryConnector(
- b.legacy.server, // as client???
- dashboard.DashboardResourceInfo.GroupResource(),
- )
// Dual writes if a RESTOptionsGetter is provided
if desiredMode != grafanarest.Mode0 && optsGetter != nil {
- store, err := newStorage(scheme)
- if err != nil {
- return nil, err
- }
-
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: grafanaregistry.GetAttrs}
if err := store.CompleteWithOptions(options); err != nil {
return nil, err
}
- storage[dash.StoragePath()] = grafanarest.NewDualWriter(grafanarest.Mode1, legacyStore, store, reg)
+ storage[resourceInfo.StoragePath()] = grafanarest.NewDualWriter(grafanarest.Mode1, legacyStore, store, reg)
}
- apiGroupInfo.VersionedResourcesStorageMap[dashboard.VERSION] = storage
+ apiGroupInfo.VersionedResourcesStorageMap[v0alpha1.VERSION] = storage
return &apiGroupInfo, nil
}
func (b *DashboardsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
- return dashboard.GetOpenAPIDefinitions
+ return v0alpha1.GetOpenAPIDefinitions
}
func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) {
@@ -191,8 +166,8 @@ func (b *DashboardsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.Op
root := "/apis/" + b.GetGroupVersion().String() + "/"
// Hide the ability to list or watch across all tenants
- delete(oas.Paths.Paths, root+dashboard.DashboardResourceInfo.GroupResource().Resource)
- delete(oas.Paths.Paths, root+"watch/"+dashboard.DashboardResourceInfo.GroupResource().Resource)
+ delete(oas.Paths.Paths, root+v0alpha1.DashboardResourceInfo.GroupResource().Resource)
+ delete(oas.Paths.Paths, root+"watch/"+v0alpha1.DashboardResourceInfo.GroupResource().Resource)
// The root API discovery list
sub := oas.Paths.Paths[root]
diff --git a/pkg/registry/apis/dashboard/sub_dto.go b/pkg/registry/apis/dashboard/sub_dto.go
index a336cf61b10..3a5a86cba94 100644
--- a/pkg/registry/apis/dashboard/sub_dto.go
+++ b/pkg/registry/apis/dashboard/sub_dto.go
@@ -2,7 +2,6 @@ package dashboard
import (
"context"
- "encoding/json"
"fmt"
"net/http"
@@ -10,7 +9,6 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/grafana/pkg/apimachinery/identity"
- "github.com/grafana/grafana/pkg/apimachinery/utils"
dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
"github.com/grafana/grafana/pkg/infra/appcontext"
"github.com/grafana/grafana/pkg/infra/slugify"
@@ -18,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/guardian"
- "github.com/grafana/grafana/pkg/storage/unified/resource"
)
// The DTO returns everything the UI needs in a single request
@@ -26,10 +23,8 @@ type DTOConnector struct {
builder *DashboardsAPIBuilder
}
-var (
- _ rest.Connecter = (*DTOConnector)(nil)
- _ rest.StorageMetadata = (*DTOConnector)(nil)
-)
+var _ = rest.Connecter(&DTOConnector{})
+var _ = rest.StorageMetadata(&DTOConnector{})
func (r *DTOConnector) New() runtime.Object {
return &dashboard.DashboardWithAccessInfo{}
@@ -93,35 +88,10 @@ func (r *DTOConnector) Connect(ctx context.Context, name string, opts runtime.Ob
r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Dashboard, accesscontrol.ScopeAnnotationsTypeDashboard)
r.getAnnotationPermissionsByScope(ctx, user, &access.AnnotationsPermissions.Organization, accesscontrol.ScopeAnnotationsTypeOrganization)
- key := &resource.ResourceKey{
- Namespace: info.Value,
- Group: dashboard.GROUP,
- Resource: dashboard.DashboardResourceInfo.GroupResource().Resource,
- Name: name,
- }
- store := r.builder.legacy.access
- rsp, err := store.Read(ctx, &resource.ReadRequest{Key: key})
+ dash, err := r.builder.access.GetDashboard(ctx, info.OrgID, name)
if err != nil {
return nil, err
}
- dash := &dashboard.Dashboard{}
- err = json.Unmarshal(rsp.Value, dash)
- if err != nil {
- return nil, err
- }
-
- // TODO, load the full spec from blob storage
- if false {
- blob, err := store.GetBlob(ctx, key, &utils.BlobInfo{UID: "dto"}, true)
- if err != nil {
- return nil, err
- }
- err = json.Unmarshal(blob.Value, &dash.Spec)
- if err != nil {
- return nil, err
- }
- }
-
access.Slug = slugify.Slugify(dash.Spec.GetNestedString("title"))
access.Url = dashboards.GetDashboardFolderURL(false, name, access.Slug)
diff --git a/pkg/registry/apis/dashboard/sub_versions.go b/pkg/registry/apis/dashboard/sub_versions.go
new file mode 100644
index 00000000000..4787c5387df
--- /dev/null
+++ b/pkg/registry/apis/dashboard/sub_versions.go
@@ -0,0 +1,117 @@
+package dashboard
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apiserver/pkg/registry/rest"
+
+ common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
+ dashboard "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
+ "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
+ dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
+)
+
+type VersionsREST struct {
+ builder *DashboardsAPIBuilder
+}
+
+var _ = rest.Connecter(&VersionsREST{})
+var _ = rest.StorageMetadata(&VersionsREST{})
+
+func (r *VersionsREST) New() runtime.Object {
+ return &dashboard.DashboardVersionList{}
+}
+
+func (r *VersionsREST) Destroy() {
+}
+
+func (r *VersionsREST) ConnectMethods() []string {
+ return []string{"GET"}
+}
+
+func (r *VersionsREST) ProducesMIMETypes(verb string) []string {
+ return nil
+}
+
+func (r *VersionsREST) ProducesObject(verb string) interface{} {
+ return &dashboard.DashboardVersionList{}
+}
+
+func (r *VersionsREST) NewConnectOptions() (runtime.Object, bool, string) {
+ return nil, true, ""
+}
+
+func (r *VersionsREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
+ info, err := request.NamespaceInfoFrom(ctx, true)
+ if err != nil {
+ return nil, err
+ }
+
+ return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+ path := req.URL.Path
+ idx := strings.LastIndex(path, "/versions/")
+ if idx > 0 {
+ key := path[strings.LastIndex(path, "/")+1:]
+ version, err := strconv.Atoi(key)
+ if err != nil {
+ responder.Error(err)
+ return
+ }
+
+ dto, err := r.builder.dashboardVersionService.Get(ctx, &dashver.GetDashboardVersionQuery{
+ DashboardUID: uid,
+ OrgID: info.OrgID,
+ Version: version,
+ })
+ if err != nil {
+ responder.Error(err)
+ return
+ }
+
+ data, _ := dto.Data.Map()
+
+ // Convert the version to a regular dashboard
+ dash := &dashboard.Dashboard{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: uid,
+ CreationTimestamp: metav1.NewTime(dto.Created),
+ },
+ Spec: common.Unstructured{Object: data},
+ }
+ responder.Object(100, dash)
+ return
+ }
+
+ // Or list versions
+ rsp, err := r.builder.dashboardVersionService.List(ctx, &dashver.ListDashboardVersionsQuery{
+ DashboardUID: uid,
+ OrgID: info.OrgID,
+ })
+ if err != nil {
+ responder.Error(err)
+ return
+ }
+ versions := &dashboard.DashboardVersionList{}
+ for _, v := range rsp {
+ info := dashboard.DashboardVersionInfo{
+ Version: v.Version,
+ Created: v.Created.UnixMilli(),
+ Message: v.Message,
+ }
+ if v.ParentVersion != v.Version {
+ info.ParentVersion = v.ParentVersion
+ }
+ if v.CreatedBy > 0 {
+ info.CreatedBy = fmt.Sprintf("%d", v.CreatedBy)
+ }
+ versions.Items = append(versions.Items, info)
+ }
+ responder.Object(http.StatusOK, versions)
+ }), nil
+}
diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go
index 018d59d4c88..ae88297da96 100644
--- a/pkg/services/sqlstore/migrations/migrations.go
+++ b/pkg/services/sqlstore/migrations/migrations.go
@@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ssosettings"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert"
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
- basicResourceMigrations "github.com/grafana/grafana/pkg/storage/unified/basic/migrations"
)
// --- Migration Guide line ---
@@ -124,8 +123,6 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) {
accesscontrol.AddManagedFolderAlertingSilencesActionsMigrator(mg)
ualert.AddRecordingRuleColumns(mg)
-
- basicResourceMigrations.AddBasicResourceMigrations(mg)
}
func addStarMigrations(mg *Migrator) {
diff --git a/pkg/storage/unified/README.md b/pkg/storage/unified/README.md
deleted file mode 100644
index cd85de407f8..00000000000
--- a/pkg/storage/unified/README.md
+++ /dev/null
@@ -1,30 +0,0 @@
-This includes four packages
-
-## resource
-
-this is a go module that can be imported into external projects
-
-This includes the protobuf based client+server and all the logic required to convert requests into write events.
-
-Protobuf TODO?
-* can/should we use upstream k8s proto for query object?
-* starting a project today... should we use proto3?
-
-
-## apistore
-
-The apiserver storage.Interface that links the storage to kubernetes
-
-Mostly a copy of te
-
-
-## entitybridge
-
-Implementes a resource store using the existing entity service. This will let us evolve the
-kubernetes interface.Store using existing system structures while we explore better options.
-
-
-## sqlnext
-
-VERY early stub exploring alternative sql structure... really just a stub right now
-
diff --git a/pkg/storage/unified/apistore/history.go b/pkg/storage/unified/apistore/history.go
deleted file mode 100644
index 9116645656f..00000000000
--- a/pkg/storage/unified/apistore/history.go
+++ /dev/null
@@ -1,103 +0,0 @@
-package apistore
-
-import (
- "context"
- "encoding/json"
- "net/http"
- "strconv"
-
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/runtime"
- "k8s.io/apimachinery/pkg/runtime/schema"
- "k8s.io/apiserver/pkg/registry/rest"
-
- "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
- "github.com/grafana/grafana/pkg/storage/unified/resource"
-)
-
-type HistoryConnector interface {
- rest.Storage
- rest.Connecter
- rest.StorageMetadata
-}
-
-func NewHistoryConnector(search resource.ResourceIndexServer, gr schema.GroupResource) HistoryConnector {
- return &historyREST{
- search: search,
- gr: gr,
- }
-}
-
-type historyREST struct {
- search resource.ResourceIndexServer // should be a client!
- gr schema.GroupResource
-}
-
-func (r *historyREST) New() runtime.Object {
- return &metav1.PartialObjectMetadataList{}
-}
-
-func (r *historyREST) Destroy() {
-}
-
-func (r *historyREST) ConnectMethods() []string {
- return []string{"GET"}
-}
-
-func (r *historyREST) ProducesMIMETypes(verb string) []string {
- return nil
-}
-
-func (r *historyREST) ProducesObject(verb string) interface{} {
- return &metav1.PartialObjectMetadataList{}
-}
-
-func (r *historyREST) NewConnectOptions() (runtime.Object, bool, string) {
- return nil, false, ""
-}
-
-func (r *historyREST) Connect(ctx context.Context, uid string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
- info, err := request.NamespaceInfoFrom(ctx, true)
- if err != nil {
- return nil, err
- }
-
- key := &resource.ResourceKey{
- Namespace: info.Value,
- Group: r.gr.Group,
- Resource: r.gr.Resource,
- Name: uid,
- }
-
- return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
- query := req.URL.Query()
- rsp, err := r.search.History(ctx, &resource.HistoryRequest{
- NextPageToken: query.Get("token"),
- Limit: 100, // TODO, from query
- Key: key,
- })
- if err != nil {
- responder.Error(err)
- return
- }
-
- list := &metav1.PartialObjectMetadataList{
- ListMeta: metav1.ListMeta{
- Continue: rsp.NextPageToken,
- },
- }
- if rsp.ResourceVersion > 0 {
- list.ResourceVersion = strconv.FormatInt(rsp.ResourceVersion, 10)
- }
- for _, v := range rsp.Items {
- partial := metav1.PartialObjectMetadata{}
- err = json.Unmarshal(v.PartialObjectMeta, &partial)
- if err != nil {
- responder.Error(err)
- return
- }
- list.Items = append(list.Items, partial)
- }
- responder.Object(http.StatusOK, list)
- }), nil
-}
diff --git a/pkg/storage/unified/apistore/storage.go b/pkg/storage/unified/apistore/storage.go
index 1f158bcd655..24df3d67ca9 100644
--- a/pkg/storage/unified/apistore/storage.go
+++ b/pkg/storage/unified/apistore/storage.go
@@ -86,9 +86,9 @@ func getKey(val string) (*resource.ResourceKey, error) {
if err != nil {
return nil, err
}
- if k.Group == "" {
- return nil, apierrors.NewInternalError(fmt.Errorf("missing group in request"))
- }
+ // if k.Group == "" {
+ // return nil, apierrors.NewInternalError(fmt.Errorf("missing group in request"))
+ // }
if k.Resource == "" {
return nil, apierrors.NewInternalError(fmt.Errorf("missing resource in request"))
}
@@ -164,9 +164,9 @@ func (s *Storage) Delete(ctx context.Context, key string, out runtime.Object, pr
return err
}
- if validateDeletion != nil {
- return fmt.Errorf("not supported (validate deletion)")
- }
+ // if validateDeletion != nil {
+ // return fmt.Errorf("not supported (validate deletion)")
+ // }
cmd := &resource.DeleteRequest{Key: k}
if preconditions != nil {
diff --git a/pkg/storage/unified/basic/basic_sql_backend.go b/pkg/storage/unified/basic/basic_sql_backend.go
deleted file mode 100644
index d746a0d73fe..00000000000
--- a/pkg/storage/unified/basic/basic_sql_backend.go
+++ /dev/null
@@ -1,227 +0,0 @@
-package basic
-
-import (
- "context"
- "database/sql"
- "fmt"
- "log/slog"
- "time"
-
- "go.opentelemetry.io/otel/trace"
- "go.opentelemetry.io/otel/trace/noop"
- "k8s.io/apimachinery/pkg/runtime/schema"
-
- "github.com/grafana/grafana/pkg/infra/db"
- "github.com/grafana/grafana/pkg/services/sqlstore/session"
- "github.com/grafana/grafana/pkg/storage/unified/resource"
-)
-
-const trace_prefix = "basic.sql.resource."
-const table_name = "basic_resource"
-
-type ResourceServerOptions struct {
- DB db.DB
- GroupResource schema.GroupResource
- Tracer trace.Tracer
- MaxItems int
-}
-
-// This storage engine is not designed to support large collections
-// The goal with this package is a production ready implementation that
-// can support modest requirements. By design, this will scan all
-// results on all list operations, so we do not want this to grow too big
-func NewResourceServer(opts ResourceServerOptions) (resource.ResourceServer, error) {
- if opts.Tracer == nil {
- opts.Tracer = noop.NewTracerProvider().Tracer("resource-server")
- }
-
- store := &basicSQLBackend{
- db: opts.DB,
- gr: opts.GroupResource,
- tracer: opts.Tracer,
- log: slog.Default().With("logger", "basic-sql-resource"),
- }
-
- return resource.NewResourceServer(resource.ResourceServerOptions{
- Tracer: opts.Tracer,
- Backend: store,
- Diagnostics: store,
- Lifecycle: store,
- })
-}
-
-type basicSQLBackend struct {
- log *slog.Logger
- db db.DB
- gr schema.GroupResource
- maxItems int
- tracer trace.Tracer
-
- // Simple watch stream -- NOTE, this only works for single tenant!
- broadcaster resource.Broadcaster[*resource.WrittenEvent]
-
- stream chan<- *resource.WrittenEvent
-}
-
-func (s *basicSQLBackend) Init() (err error) {
- s.broadcaster, err = resource.NewBroadcaster(context.Background(), func(c chan<- *resource.WrittenEvent) error {
- s.stream = c
- return nil
- })
- return
-}
-
-func (s *basicSQLBackend) IsHealthy(ctx context.Context, r *resource.HealthCheckRequest) (*resource.HealthCheckResponse, error) {
- return &resource.HealthCheckResponse{Status: resource.HealthCheckResponse_SERVING}, nil
-}
-
-func (s *basicSQLBackend) Stop() {
- if s.stream != nil {
- close(s.stream)
- }
-}
-
-func (s *basicSQLBackend) validateKey(key *resource.ResourceKey) error {
- if s.gr.Group != "" && s.gr.Group != key.Group {
- return fmt.Errorf("expected group: %s, found: %s", s.gr.Group, key.Group)
- }
- if s.gr.Resource != "" && s.gr.Resource != key.Resource {
- return fmt.Errorf("expected resource: %s, found: %s", s.gr.Resource, key.Resource)
- }
- return nil
-}
-
-func (s *basicSQLBackend) WriteEvent(ctx context.Context, event resource.WriteEvent) (rv int64, err error) {
- _, span := s.tracer.Start(ctx, trace_prefix+"WriteEvent")
- defer span.End()
-
- key := event.Key
- err = s.validateKey(key)
- if err != nil {
- return
- }
- gvk := event.Object.GetGroupVersionKind()
-
- // This delegates resource version creation to auto-increment
- // At scale, this is not a great strategy since everything is locked across all resources while this executes
- appender := func(tx *session.SessionTx) (int64, error) {
- return tx.ExecWithReturningId(ctx,
- `INSERT INTO `+table_name+` (api_group,api_version,namespace,resource,name,value) VALUES($1,$2,$3,$4,$5,$6)`,
- key.Group, gvk.Version, key.Namespace, key.Resource, key.Name, event.Value)
- }
-
- wiper := func(tx *session.SessionTx) (sql.Result, error) {
- return tx.Exec(ctx, `DELETE FROM `+table_name+` WHERE `+
- `api_group=$1 AND `+
- `namespace=$2 AND `+
- `resource=$3 AND `+
- `name=$4`,
- key.Group, key.Namespace, key.Resource, key.Name)
- }
-
- err = s.db.GetSqlxSession().WithTransaction(ctx, func(tx *session.SessionTx) error {
- switch event.Type {
- case resource.WatchEvent_ADDED:
- count := 0
- err = tx.Get(ctx, &count, `SELECT count(*) FROM `+table_name+` WHERE api_group=$1 AND resource=$2`, key.Group, key.Resource)
- if err != nil {
- return err
- }
- if count >= s.maxItems {
- return fmt.Errorf("the storage backend only supports %d items", s.maxItems)
- }
- rv, err = appender(tx)
-
- case resource.WatchEvent_MODIFIED:
- _, err = wiper(tx)
- if err == nil {
- rv, err = appender(tx)
- }
- case resource.WatchEvent_DELETED:
- _, err = wiper(tx)
- default:
- return fmt.Errorf("unsupported event type")
- }
- return err
- })
-
- // Async notify all subscribers
- if s.stream != nil {
- go func() {
- write := &resource.WrittenEvent{
- WriteEvent: event,
- Timestamp: time.Now().UnixMilli(),
- ResourceVersion: rv,
- }
- s.stream <- write
- }()
- }
- return
-}
-
-func (s *basicSQLBackend) WatchWriteEvents(ctx context.Context) (<-chan *resource.WrittenEvent, error) {
- return s.broadcaster.Subscribe(ctx)
-}
-
-func (s *basicSQLBackend) Read(ctx context.Context, req *resource.ReadRequest) (*resource.ReadResponse, error) {
- _, span := s.tracer.Start(ctx, trace_prefix+"Read")
- defer span.End()
-
- key := req.Key
- err := s.validateKey(key)
- if err != nil {
- return nil, err
- }
-
- rows, err := s.db.GetSqlxSession().Query(ctx, "SELECT rv,value FROM "+table_name+" WHERE api_group=$1 AND namespace=$2 AND resource=$3 AND name=$4",
- key.Group, key.Namespace, key.Resource, key.Name)
- if err != nil {
- return nil, err
- }
- if rows.Next() {
- rsp := &resource.ReadResponse{}
- err = rows.Scan(&rsp.ResourceVersion, &rsp.Value)
- if err == nil && rows.Next() {
- return nil, fmt.Errorf("unexpected multiple results found") // should not be possible with the index strategy
- }
- return rsp, err
- }
- return nil, fmt.Errorf("NOT FOUND ERROR")
-}
-
-// This implementation is only ever called from inside single tenant grafana, so there is no need to decode
-// the value and try filtering first -- that will happen one layer up anyway
-func (s *basicSQLBackend) PrepareList(ctx context.Context, req *resource.ListRequest) (*resource.ListResponse, error) {
- if req.NextPageToken != "" {
- return nil, fmt.Errorf("this storage backend does not support paging")
- }
- _, span := s.tracer.Start(ctx, trace_prefix+"PrepareList")
- defer span.End()
-
- key := req.Options.Key
- err := s.validateKey(key)
- if err != nil {
- return nil, err
- }
- rsp := &resource.ListResponse{}
- rows, err := s.db.GetSqlxSession().Query(ctx,
- "SELECT rv,value FROM "+table_name+
- " WHERE api_group=$1 AND namespace=$2 AND resource=$3 "+
- " ORDER BY name asc LIMIT $4",
- key.Group, key.Namespace, key.Resource, s.maxItems+1)
- if err != nil {
- return nil, err
- }
- for rows.Next() {
- wrapper := &resource.ResourceWrapper{}
- err = rows.Scan(&wrapper.ResourceVersion, &wrapper.Value)
- if err != nil {
- break
- }
- rsp.Items = append(rsp.Items, wrapper)
- }
- if len(rsp.Items) > s.maxItems {
- err = fmt.Errorf("more values that are supported by this storage engine")
- }
- return rsp, err
-}
diff --git a/pkg/storage/unified/basic/migrations/migrations.go b/pkg/storage/unified/basic/migrations/migrations.go
deleted file mode 100644
index 6b11d762539..00000000000
--- a/pkg/storage/unified/basic/migrations/migrations.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package migrations
-
-import "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
-
-func AddBasicResourceMigrations(mg *migrator.Migrator) {
- mg.AddMigration("create unified storage basic resource table", migrator.NewAddTableMigration(migrator.Table{
- Name: "basic_resource",
- Columns: []*migrator.Column{
- // Sequential resource version
- {Name: "rv", Type: migrator.DB_BigInt, Nullable: false, IsPrimaryKey: true, IsAutoIncrement: true},
-
- // Properties that exist in path/key (and duplicated in the json value)
- {Name: "api_group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, // avoid "group" so escaping is easier :)
- {Name: "api_version", Type: migrator.DB_NVarchar, Length: 32, Nullable: false}, // informational
- {Name: "namespace", Type: migrator.DB_NVarchar, Length: 63, Nullable: true}, // namespace is not required (cluster scope)
- {Name: "resource", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
- {Name: "name", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
-
- // The k8s resource JSON text (without the resourceVersion populated)
- {Name: "value", Type: migrator.DB_MediumText, Nullable: false},
- },
- Indices: []*migrator.Index{
- {Cols: []string{"api_group", "resource", "namespace", "name"}, Type: migrator.UniqueIndex},
- },
- }))
-}
diff --git a/pkg/storage/unified/entitybridge/entitybridge.go b/pkg/storage/unified/entitybridge/entitybridge.go
index aa33261b0cc..77ffd3f70c1 100644
--- a/pkg/storage/unified/entitybridge/entitybridge.go
+++ b/pkg/storage/unified/entitybridge/entitybridge.go
@@ -3,101 +3,35 @@ package entitybridge
import (
"context"
"fmt"
- "os"
- "path/filepath"
- "time"
- "gocloud.dev/blob/fileblob"
"k8s.io/apimachinery/pkg/selection"
"k8s.io/klog/v2"
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
- "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
- "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/store/entity"
- "github.com/grafana/grafana/pkg/services/store/entity/db/dbimpl"
"github.com/grafana/grafana/pkg/services/store/entity/sqlstash"
- "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
// Creates a ResourceServer using the existing entity tables
-// NOTE: most of the field values are ignored
-func ProvideResourceServer(db db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tracer tracing.Tracer) (resource.ResourceServer, error) {
- opts := resource.ResourceServerOptions{
- Tracer: tracer,
+// NOTE: the server is optional and only used to pass init+close functions
+func EntityAsResourceServer(client entity.EntityStoreClient, server sqlstash.SqlEntityServer, tracer tracing.Tracer) (resource.ResourceServer, error) {
+ if client == nil {
+ return nil, fmt.Errorf("client must be defined")
}
- supportBlobs := true
- useEntitySQL := true
-
- // Create a local blob filesystem blob store
- if supportBlobs {
- dir := filepath.Join(cfg.DataPath, "unistore", "blobs")
- if err := os.MkdirAll(dir, 0o750); err != nil {
- return nil, err
- }
-
- bucket, err := fileblob.OpenBucket(dir, &fileblob.Options{
- CreateDir: true,
- Metadata: fileblob.MetadataDontWrite, // skip
- })
- if err != nil {
- return nil, err
- }
- opts.Blob, err = resource.NewCDKBlobStore(context.Background(), resource.CDKBlobStoreOptions{
- Tracer: tracer,
- Bucket: bucket,
- URLExpiration: time.Minute * 20,
- })
- if err != nil {
- return nil, err
- }
+ // Use this bridge as the resource store
+ bridge := &entityBridge{
+ client: client,
+ server: server,
}
-
- if useEntitySQL {
- eDB, err := dbimpl.ProvideEntityDB(db, cfg, features, tracer)
- if err != nil {
- return nil, err
- }
-
- server, err := sqlstash.ProvideSQLEntityServer(eDB, tracer)
- if err != nil {
- return nil, err
- }
- client := entity.NewEntityStoreClientLocal(server)
-
- // Use this bridge as the resource store
- bridge := &entityBridge{
- server: server,
- client: client,
- }
- opts.Backend = bridge
- opts.Diagnostics = bridge
- opts.Lifecycle = bridge
- } else {
- dir := filepath.Join(cfg.DataPath, "unistore", "resource")
- if err := os.MkdirAll(dir, 0o750); err != nil {
- return nil, err
- }
-
- bucket, err := fileblob.OpenBucket(dir, &fileblob.Options{
- CreateDir: true,
- Metadata: fileblob.MetadataDontWrite, // skip
- })
- if err != nil {
- return nil, err
- }
- opts.Backend, err = resource.NewCDKBackend(context.Background(), resource.CDKBackendOptions{
- Tracer: tracer,
- Bucket: bucket,
- })
- if err != nil {
- return nil, err
- }
- }
- return resource.NewResourceServer(opts)
+ return resource.NewResourceServer(resource.ResourceServerOptions{
+ Tracer: tracer,
+ Backend: bridge,
+ Diagnostics: bridge,
+ Lifecycle: bridge,
+ })
}
// This is only created if we use the entity implementation
diff --git a/pkg/storage/unified/resource/cdk_backend.go b/pkg/storage/unified/resource/cdk_backend.go
index fc201b13413..fd25da06ed3 100644
--- a/pkg/storage/unified/resource/cdk_backend.go
+++ b/pkg/storage/unified/resource/cdk_backend.go
@@ -9,6 +9,7 @@ import (
"strconv"
"strings"
"sync"
+ "sync/atomic"
"time"
"go.opentelemetry.io/otel/trace"
@@ -25,8 +26,6 @@ type CDKBackendOptions struct {
Tracer trace.Tracer
Bucket *blob.Bucket
RootFolder string
-
- NextResourceVersion NextResourceVersion
}
func NewCDKBackend(ctx context.Context, opts CDKBackendOptions) (StorageBackend, error) {
@@ -49,25 +48,22 @@ func NewCDKBackend(ctx context.Context, opts CDKBackendOptions) (StorageBackend,
return nil, fmt.Errorf("the root folder does not exist")
}
- // This is not safe when running in HA!
- if opts.NextResourceVersion == nil {
- opts.NextResourceVersion = newResourceVersionCounter(time.Now().UnixMilli())
- }
-
- return &cdkBackend{
+ backend := &cdkBackend{
tracer: opts.Tracer,
bucket: opts.Bucket,
root: opts.RootFolder,
- nextRV: opts.NextResourceVersion,
- }, nil
+ }
+ backend.rv.Swap(time.Now().UnixMilli())
+ return backend, nil
}
type cdkBackend struct {
tracer trace.Tracer
bucket *blob.Bucket
root string
- nextRV NextResourceVersion
- mutex sync.Mutex
+
+ mutex sync.Mutex
+ rv atomic.Int64
// Simple watch stream -- NOTE, this only works for single tenant!
broadcaster Broadcaster[*WrittenEvent]
@@ -117,7 +113,7 @@ func (s *cdkBackend) WriteEvent(ctx context.Context, event WriteEvent) (rv int64
s.mutex.Lock()
defer s.mutex.Unlock()
- rv = s.nextRV()
+ rv = s.rv.Add(1)
err = s.bucket.WriteAll(ctx, s.getPath(event.Key, rv), event.Value, &blob.WriterOptions{
ContentType: "application/json",
})
@@ -163,7 +159,7 @@ func (s *cdkBackend) Read(ctx context.Context, req *ReadRequest) (*ReadResponse,
}
raw, err := s.bucket.ReadAll(ctx, path)
- if err == nil && isDeletedMarker(raw) {
+ if raw == nil || (err == nil && isDeletedMarker(raw)) {
return nil, apierrors.NewNotFound(schema.GroupResource{
Group: req.Key.Group,
Resource: req.Key.Resource,
@@ -193,7 +189,9 @@ func (s *cdkBackend) PrepareList(ctx context.Context, req *ListRequest) (*ListRe
return nil, err
}
- rsp := &ListResponse{}
+ rsp := &ListResponse{
+ ResourceVersion: s.rv.Load(),
+ }
for _, item := range resources {
latest := item.versions[0]
raw, err := s.bucket.ReadAll(ctx, latest.key)
diff --git a/pkg/storage/unified/resource/cdk_blob.go b/pkg/storage/unified/resource/cdk_blob.go
deleted file mode 100644
index e97682e999a..00000000000
--- a/pkg/storage/unified/resource/cdk_blob.go
+++ /dev/null
@@ -1,176 +0,0 @@
-package resource
-
-import (
- "bytes"
- context "context"
- "crypto/md5"
- "encoding/hex"
- "fmt"
- "mime"
- "time"
-
- "github.com/google/uuid"
- "go.opentelemetry.io/otel/trace"
- "go.opentelemetry.io/otel/trace/noop"
- "gocloud.dev/blob"
- _ "gocloud.dev/blob/fileblob"
- _ "gocloud.dev/blob/memblob"
-
- "github.com/grafana/grafana/pkg/apimachinery/utils"
-)
-
-type CDKBlobStoreOptions struct {
- Tracer trace.Tracer
- Bucket *blob.Bucket
- RootFolder string
- URLExpiration time.Duration
-}
-
-func NewCDKBlobStore(ctx context.Context, opts CDKBlobStoreOptions) (BlobStore, error) {
- if opts.Tracer == nil {
- opts.Tracer = noop.NewTracerProvider().Tracer("cdk-blob-store")
- }
-
- if opts.Bucket == nil {
- return nil, fmt.Errorf("missing bucket")
- }
- if opts.URLExpiration < 1 {
- opts.URLExpiration = time.Minute * 10 // 10 min default
- }
-
- found, _, err := opts.Bucket.ListPage(ctx, blob.FirstPageToken, 1, &blob.ListOptions{
- Prefix: opts.RootFolder,
- Delimiter: "/",
- })
- if err != nil {
- return nil, err
- }
- if found == nil {
- return nil, fmt.Errorf("the root folder does not exist")
- }
-
- return &cdkBlobStore{
- tracer: opts.Tracer,
- bucket: opts.Bucket,
- root: opts.RootFolder,
- cansignurls: false, // TODO depends on the implementation
- expiration: opts.URLExpiration,
- }, nil
-}
-
-type cdkBlobStore struct {
- tracer trace.Tracer
- bucket *blob.Bucket
- root string
- cansignurls bool
- expiration time.Duration
-}
-
-func (s *cdkBlobStore) getBlobPath(key *ResourceKey, info *utils.BlobInfo) (string, error) {
- var buffer bytes.Buffer
- buffer.WriteString(s.root)
-
- if key.Namespace == "" {
- buffer.WriteString("__cluster__/")
- } else {
- buffer.WriteString(key.Namespace)
- buffer.WriteString("/")
- }
-
- if key.Group == "" {
- return "", fmt.Errorf("missing group")
- }
- buffer.WriteString(key.Group)
- buffer.WriteString("/")
-
- if key.Resource == "" {
- return "", fmt.Errorf("missing resource")
- }
- buffer.WriteString(key.Resource)
- buffer.WriteString("/")
-
- if key.Name == "" {
- return "", fmt.Errorf("missing name")
- }
- buffer.WriteString(key.Name)
- buffer.WriteString("/")
- buffer.WriteString(info.UID)
-
- ext, err := mime.ExtensionsByType(info.MimeType)
- if err != nil {
- return "", err
- }
- if len(ext) > 0 {
- buffer.WriteString(ext[0])
- }
- return buffer.String(), nil
-}
-
-func (s *cdkBlobStore) SupportsSignedURLs() bool {
- return s.cansignurls
-}
-
-func (s *cdkBlobStore) PutBlob(ctx context.Context, req *PutBlobRequest) (*PutBlobResponse, error) {
- info := &utils.BlobInfo{
- UID: uuid.New().String(),
- }
- info.SetContentType(req.ContentType)
- path, err := s.getBlobPath(req.Resource, info)
- if err != nil {
- return nil, err
- }
-
- rsp := &PutBlobResponse{Uid: info.UID, MimeType: info.MimeType, Charset: info.Charset}
- if req.Method == PutBlobRequest_HTTP {
- rsp.Url, err = s.bucket.SignedURL(ctx, path, &blob.SignedURLOptions{
- Method: "PUT",
- Expiry: s.expiration,
- ContentType: req.ContentType,
- })
- return rsp, err
- }
- if len(req.Value) < 1 {
- return nil, fmt.Errorf("missing content value")
- }
-
- // Write the value
- err = s.bucket.WriteAll(ctx, path, req.Value, &blob.WriterOptions{
- ContentType: req.ContentType,
- })
- if err != nil {
- return nil, err
- }
-
- attrs, err := s.bucket.Attributes(ctx, path)
- if err != nil {
- return nil, err
- }
- rsp.Size = attrs.Size
-
- // Set the MD5 hash if missing
- if len(attrs.MD5) == 0 {
- h := md5.New()
- _, _ = h.Write(req.Value)
- attrs.MD5 = h.Sum(nil)
- }
- rsp.Hash = hex.EncodeToString(attrs.MD5[:])
- return rsp, err
-}
-
-func (s *cdkBlobStore) GetBlob(ctx context.Context, resource *ResourceKey, info *utils.BlobInfo, mustProxy bool) (*GetBlobResponse, error) {
- path, err := s.getBlobPath(resource, info)
- if err != nil {
- return nil, err
- }
- rsp := &GetBlobResponse{ContentType: info.ContentType()}
- if mustProxy || !s.cansignurls {
- rsp.Value, err = s.bucket.ReadAll(ctx, path)
- return rsp, err
- }
- rsp.Url, err = s.bucket.SignedURL(ctx, path, &blob.SignedURLOptions{
- Method: "GET",
- Expiry: s.expiration,
- ContentType: rsp.ContentType,
- })
- return rsp, err
-}
diff --git a/pkg/storage/unified/resource/cdk_blob_test.go b/pkg/storage/unified/resource/cdk_blob_test.go
deleted file mode 100644
index a0ce25a3ff8..00000000000
--- a/pkg/storage/unified/resource/cdk_blob_test.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package resource
-
-import (
- "context"
- "fmt"
- "os"
- "testing"
-
- "github.com/stretchr/testify/require"
- "gocloud.dev/blob/fileblob"
- "gocloud.dev/blob/memblob"
-
- "github.com/grafana/grafana/pkg/apimachinery/utils"
-)
-
-func TestCDKBlobStore(t *testing.T) {
- bucket := memblob.OpenBucket(nil)
- if false {
- tmp, err := os.MkdirTemp("", "xxx-*")
- require.NoError(t, err)
-
- bucket, err = fileblob.OpenBucket(tmp, &fileblob.Options{
- CreateDir: true,
- Metadata: fileblob.MetadataDontWrite, // skip
- })
- require.NoError(t, err)
-
- fmt.Printf("ROOT: %s\n\n", tmp)
- }
- ctx := context.Background()
-
- store, err := NewCDKBlobStore(ctx, CDKBlobStoreOptions{
- Bucket: bucket,
- //RootFolder: "xyz",
- })
- require.NoError(t, err)
-
- t.Run("can write then read a blob", func(t *testing.T) {
- raw := testdata(t, "01_create_playlist.json")
- key := &ResourceKey{
- Group: "playlist.grafana.app",
- Resource: "rrrr", // can be anything
- Namespace: "default",
- Name: "fdgsv37qslr0ga",
- }
-
- rsp, err := store.PutBlob(ctx, &PutBlobRequest{
- Resource: key,
- Method: PutBlobRequest_GRPC,
- ContentType: "application/json",
- Value: raw,
- })
- require.NoError(t, err)
- require.Equal(t, "4933beea0c6d6dfd73150451098c70f0", rsp.Hash)
-
- found, err := store.GetBlob(ctx, key, &utils.BlobInfo{
- UID: rsp.Uid,
- Size: rsp.Size,
- Hash: rsp.Hash,
- MimeType: rsp.MimeType,
- Charset: rsp.Charset,
- }, false)
- require.NoError(t, err)
- require.Equal(t, raw, found.Value)
- require.Equal(t, "application/json", found.ContentType)
- })
-}
diff --git a/pkg/storage/unified/resource/client_wrapper.go b/pkg/storage/unified/resource/client_wrapper.go
index e4ecf52d06d..7ad67e937d8 100644
--- a/pkg/storage/unified/resource/client_wrapper.go
+++ b/pkg/storage/unified/resource/client_wrapper.go
@@ -25,22 +25,6 @@ func NewLocalResourceStoreClient(server ResourceStoreServer) ResourceStoreClient
return NewResourceStoreClient(grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor))
}
-func NewLocalResourceSearchClient(server ResourceStoreServer) ResourceIndexClient {
- channel := &inprocgrpc.Channel{}
-
- auth := &grpcUtils.Authenticator{}
-
- channel.RegisterService(
- grpchan.InterceptServer(
- &ResourceStore_ServiceDesc,
- grpcAuth.UnaryServerInterceptor(auth.Authenticate),
- grpcAuth.StreamServerInterceptor(auth.Authenticate),
- ),
- server,
- )
- return NewResourceIndexClient(grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor))
-}
-
func NewResourceStoreClientGRPC(channel *grpc.ClientConn) ResourceStoreClient {
return NewResourceStoreClient(grpchan.InterceptClientConn(channel, grpcUtils.UnaryClientInterceptor, grpcUtils.StreamClientInterceptor))
}
diff --git a/pkg/storage/unified/resource/go.sum b/pkg/storage/unified/resource/go.sum
index d1d127ec502..1ffe513565f 100644
--- a/pkg/storage/unified/resource/go.sum
+++ b/pkg/storage/unified/resource/go.sum
@@ -80,8 +80,9 @@ google.golang.org/api v0.176.0 h1:dHj1/yv5Dm/eQTXiP9hNCRT3xzJHWXeNdRq29XbMxoE=
google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY=
google.golang.org/genproto/googleapis/api v0.0.0-20240604185151-ef581f913117 h1:+rdxYoE3E5htTEWIe15GlN6IfvbURM//Jt0mmkmm6ZU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
-google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
diff --git a/pkg/storage/unified/resource/noop.go b/pkg/storage/unified/resource/noop.go
index b24c4ef0259..92778817ba1 100644
--- a/pkg/storage/unified/resource/noop.go
+++ b/pkg/storage/unified/resource/noop.go
@@ -2,14 +2,11 @@ package resource
import (
"context"
-
- "github.com/grafana/grafana/pkg/apimachinery/utils"
)
var (
- _ ResourceIndexServer = &noopService{}
- _ DiagnosticsServer = &noopService{}
- _ LifecycleHooks = &noopService{}
+ _ DiagnosticsServer = &noopService{}
+ _ LifecycleHooks = &noopService{}
)
// noopService is a helper implementation to simplify tests
@@ -42,25 +39,3 @@ func (n *noopService) Read(context.Context, *ReadRequest) (*ReadResponse, error)
func (n *noopService) List(context.Context, *ListRequest) (*ListResponse, error) {
return nil, ErrNotImplementedYet
}
-
-// History implements ResourceServer.
-func (n *noopService) History(context.Context, *HistoryRequest) (*HistoryResponse, error) {
- return nil, ErrNotImplementedYet
-}
-
-// Origin implements ResourceServer.
-func (n *noopService) Origin(context.Context, *OriginRequest) (*OriginResponse, error) {
- return nil, ErrNotImplementedYet
-}
-
-func (n *noopService) SupportsSignedURLs() bool {
- return false
-}
-
-func (n *noopService) PutBlob(context.Context, *PutBlobRequest) (*PutBlobResponse, error) {
- return nil, ErrNotImplementedYet
-}
-
-func (n *noopService) GetBlob(ctx context.Context, resource *ResourceKey, info *utils.BlobInfo, mustProxy bool) (*GetBlobResponse, error) {
- return nil, ErrNotImplementedYet
-}
diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go
index f4fa4297747..4bd90a20330 100644
--- a/pkg/storage/unified/resource/resource.pb.go
+++ b/pkg/storage/unified/resource/resource.pb.go
@@ -66,52 +66,6 @@ func (ResourceVersionMatch) EnumDescriptor() ([]byte, []int) {
return file_resource_proto_rawDescGZIP(), []int{0}
}
-type Sort_Order int32
-
-const (
- Sort_ASC Sort_Order = 0
- Sort_DESC Sort_Order = 1
-)
-
-// Enum value maps for Sort_Order.
-var (
- Sort_Order_name = map[int32]string{
- 0: "ASC",
- 1: "DESC",
- }
- Sort_Order_value = map[string]int32{
- "ASC": 0,
- "DESC": 1,
- }
-)
-
-func (x Sort_Order) Enum() *Sort_Order {
- p := new(Sort_Order)
- *p = x
- return p
-}
-
-func (x Sort_Order) String() string {
- return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
-}
-
-func (Sort_Order) Descriptor() protoreflect.EnumDescriptor {
- return file_resource_proto_enumTypes[1].Descriptor()
-}
-
-func (Sort_Order) Type() protoreflect.EnumType {
- return &file_resource_proto_enumTypes[1]
-}
-
-func (x Sort_Order) Number() protoreflect.EnumNumber {
- return protoreflect.EnumNumber(x)
-}
-
-// Deprecated: Use Sort_Order.Descriptor instead.
-func (Sort_Order) EnumDescriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{13, 0}
-}
-
type WatchEvent_Type int32
const (
@@ -154,11 +108,11 @@ func (x WatchEvent_Type) String() string {
}
func (WatchEvent_Type) Descriptor() protoreflect.EnumDescriptor {
- return file_resource_proto_enumTypes[2].Descriptor()
+ return file_resource_proto_enumTypes[1].Descriptor()
}
func (WatchEvent_Type) Type() protoreflect.EnumType {
- return &file_resource_proto_enumTypes[2]
+ return &file_resource_proto_enumTypes[1]
}
func (x WatchEvent_Type) Number() protoreflect.EnumNumber {
@@ -167,7 +121,7 @@ func (x WatchEvent_Type) Number() protoreflect.EnumNumber {
// Deprecated: Use WatchEvent_Type.Descriptor instead.
func (WatchEvent_Type) EnumDescriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{18, 0}
+ return file_resource_proto_rawDescGZIP(), []int{17, 0}
}
type HealthCheckResponse_ServingStatus int32
@@ -206,11 +160,11 @@ func (x HealthCheckResponse_ServingStatus) String() string {
}
func (HealthCheckResponse_ServingStatus) Descriptor() protoreflect.EnumDescriptor {
- return file_resource_proto_enumTypes[3].Descriptor()
+ return file_resource_proto_enumTypes[2].Descriptor()
}
func (HealthCheckResponse_ServingStatus) Type() protoreflect.EnumType {
- return &file_resource_proto_enumTypes[3]
+ return &file_resource_proto_enumTypes[2]
}
func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber {
@@ -219,55 +173,7 @@ func (x HealthCheckResponse_ServingStatus) Number() protoreflect.EnumNumber {
// Deprecated: Use HealthCheckResponse_ServingStatus.Descriptor instead.
func (HealthCheckResponse_ServingStatus) EnumDescriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{25, 0}
-}
-
-type PutBlobRequest_Method int32
-
-const (
- // Use the inline raw []byte
- PutBlobRequest_GRPC PutBlobRequest_Method = 0
- // Get a signed URL and PUT the value
- PutBlobRequest_HTTP PutBlobRequest_Method = 1
-)
-
-// Enum value maps for PutBlobRequest_Method.
-var (
- PutBlobRequest_Method_name = map[int32]string{
- 0: "GRPC",
- 1: "HTTP",
- }
- PutBlobRequest_Method_value = map[string]int32{
- "GRPC": 0,
- "HTTP": 1,
- }
-)
-
-func (x PutBlobRequest_Method) Enum() *PutBlobRequest_Method {
- p := new(PutBlobRequest_Method)
- *p = x
- return p
-}
-
-func (x PutBlobRequest_Method) String() string {
- return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
-}
-
-func (PutBlobRequest_Method) Descriptor() protoreflect.EnumDescriptor {
- return file_resource_proto_enumTypes[4].Descriptor()
-}
-
-func (PutBlobRequest_Method) Type() protoreflect.EnumType {
- return &file_resource_proto_enumTypes[4]
-}
-
-func (x PutBlobRequest_Method) Number() protoreflect.EnumNumber {
- return protoreflect.EnumNumber(x)
-}
-
-// Deprecated: Use PutBlobRequest_Method.Descriptor instead.
-func (PutBlobRequest_Method) EnumDescriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{26, 0}
+ return file_resource_proto_rawDescGZIP(), []int{19, 0}
}
type ResourceKey struct {
@@ -895,10 +801,8 @@ type DeleteResponse struct {
// Status code
Status *StatusResult `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
- // The new resource version
+ // The resource version for the deletion marker
ResourceVersion int64 `protobuf:"varint,2,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"`
- // The deleted payload
- Value []byte `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"`
}
func (x *DeleteResponse) Reset() {
@@ -947,13 +851,6 @@ func (x *DeleteResponse) GetResourceVersion() int64 {
return 0
}
-func (x *DeleteResponse) GetValue() []byte {
- if x != nil {
- return x.Value
- }
- return nil
-}
-
type ReadRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1141,61 +1038,6 @@ func (x *Requirement) GetValues() []string {
return nil
}
-type Sort struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- Field string `protobuf:"bytes,1,opt,name=field,proto3" json:"field,omitempty"`
- Order Sort_Order `protobuf:"varint,2,opt,name=order,proto3,enum=resource.Sort_Order" json:"order,omitempty"`
-}
-
-func (x *Sort) Reset() {
- *x = Sort{}
- if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[13]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *Sort) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*Sort) ProtoMessage() {}
-
-func (x *Sort) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[13]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use Sort.ProtoReflect.Descriptor instead.
-func (*Sort) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{13}
-}
-
-func (x *Sort) GetField() string {
- if x != nil {
- return x.Field
- }
- return ""
-}
-
-func (x *Sort) GetOrder() Sort_Order {
- if x != nil {
- return x.Order
- }
- return Sort_ASC
-}
-
type ListOptions struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1216,7 +1058,7 @@ type ListOptions struct {
func (x *ListOptions) Reset() {
*x = ListOptions{}
if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[14]
+ mi := &file_resource_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1229,7 +1071,7 @@ func (x *ListOptions) String() string {
func (*ListOptions) ProtoMessage() {}
func (x *ListOptions) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[14]
+ mi := &file_resource_proto_msgTypes[13]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1242,7 +1084,7 @@ func (x *ListOptions) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListOptions.ProtoReflect.Descriptor instead.
func (*ListOptions) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{14}
+ return file_resource_proto_rawDescGZIP(), []int{13}
}
func (x *ListOptions) GetKey() *ResourceKey {
@@ -1287,7 +1129,7 @@ type ListRequest struct {
func (x *ListRequest) Reset() {
*x = ListRequest{}
if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[15]
+ mi := &file_resource_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1300,7 +1142,7 @@ func (x *ListRequest) String() string {
func (*ListRequest) ProtoMessage() {}
func (x *ListRequest) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[15]
+ mi := &file_resource_proto_msgTypes[14]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1313,7 +1155,7 @@ func (x *ListRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListRequest.ProtoReflect.Descriptor instead.
func (*ListRequest) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{15}
+ return file_resource_proto_rawDescGZIP(), []int{14}
}
func (x *ListRequest) GetNextPageToken() string {
@@ -1377,7 +1219,7 @@ type ListResponse struct {
func (x *ListResponse) Reset() {
*x = ListResponse{}
if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[16]
+ mi := &file_resource_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1390,7 +1232,7 @@ func (x *ListResponse) String() string {
func (*ListResponse) ProtoMessage() {}
func (x *ListResponse) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[16]
+ mi := &file_resource_proto_msgTypes[15]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1403,7 +1245,7 @@ func (x *ListResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListResponse.ProtoReflect.Descriptor instead.
func (*ListResponse) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{16}
+ return file_resource_proto_rawDescGZIP(), []int{15}
}
func (x *ListResponse) GetItems() []*ResourceWrapper {
@@ -1452,7 +1294,7 @@ type WatchRequest struct {
func (x *WatchRequest) Reset() {
*x = WatchRequest{}
if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[17]
+ mi := &file_resource_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1465,7 +1307,7 @@ func (x *WatchRequest) String() string {
func (*WatchRequest) ProtoMessage() {}
func (x *WatchRequest) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[17]
+ mi := &file_resource_proto_msgTypes[16]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1478,7 +1320,7 @@ func (x *WatchRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use WatchRequest.ProtoReflect.Descriptor instead.
func (*WatchRequest) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{17}
+ return file_resource_proto_rawDescGZIP(), []int{16}
}
func (x *WatchRequest) GetSince() int64 {
@@ -1527,7 +1369,7 @@ type WatchEvent struct {
func (x *WatchEvent) Reset() {
*x = WatchEvent{}
if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[18]
+ mi := &file_resource_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1540,7 +1382,7 @@ func (x *WatchEvent) String() string {
func (*WatchEvent) ProtoMessage() {}
func (x *WatchEvent) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[18]
+ mi := &file_resource_proto_msgTypes[17]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1553,7 +1395,7 @@ func (x *WatchEvent) ProtoReflect() protoreflect.Message {
// Deprecated: Use WatchEvent.ProtoReflect.Descriptor instead.
func (*WatchEvent) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{18}
+ return file_resource_proto_rawDescGZIP(), []int{17}
}
func (x *WatchEvent) GetTimestamp() int64 {
@@ -1584,388 +1426,6 @@ func (x *WatchEvent) GetPrevious() *WatchEvent_Resource {
return nil
}
-type HistoryRequest struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- // Starting from the requested page (other query parameters must match!)
- NextPageToken string `protobuf:"bytes,1,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"`
- // Maximum number of items to return
- Limit int64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
- // Resource identifier
- Key *ResourceKey `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"`
- // List the deleted values (eg, show trash)
- ShowDeleted bool `protobuf:"varint,4,opt,name=show_deleted,json=showDeleted,proto3" json:"show_deleted,omitempty"`
-}
-
-func (x *HistoryRequest) Reset() {
- *x = HistoryRequest{}
- if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[19]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *HistoryRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*HistoryRequest) ProtoMessage() {}
-
-func (x *HistoryRequest) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[19]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use HistoryRequest.ProtoReflect.Descriptor instead.
-func (*HistoryRequest) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{19}
-}
-
-func (x *HistoryRequest) GetNextPageToken() string {
- if x != nil {
- return x.NextPageToken
- }
- return ""
-}
-
-func (x *HistoryRequest) GetLimit() int64 {
- if x != nil {
- return x.Limit
- }
- return 0
-}
-
-func (x *HistoryRequest) GetKey() *ResourceKey {
- if x != nil {
- return x.Key
- }
- return nil
-}
-
-func (x *HistoryRequest) GetShowDeleted() bool {
- if x != nil {
- return x.ShowDeleted
- }
- return false
-}
-
-type HistoryResponse struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- Items []*ResourceMeta `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"`
- // More results exist... pass this in the next request
- NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"`
- // ResourceVersion of the list response
- ResourceVersion int64 `protobuf:"varint,3,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"`
-}
-
-func (x *HistoryResponse) Reset() {
- *x = HistoryResponse{}
- if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[20]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *HistoryResponse) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*HistoryResponse) ProtoMessage() {}
-
-func (x *HistoryResponse) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[20]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use HistoryResponse.ProtoReflect.Descriptor instead.
-func (*HistoryResponse) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{20}
-}
-
-func (x *HistoryResponse) GetItems() []*ResourceMeta {
- if x != nil {
- return x.Items
- }
- return nil
-}
-
-func (x *HistoryResponse) GetNextPageToken() string {
- if x != nil {
- return x.NextPageToken
- }
- return ""
-}
-
-func (x *HistoryResponse) GetResourceVersion() int64 {
- if x != nil {
- return x.ResourceVersion
- }
- return 0
-}
-
-type OriginRequest struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- // Starting from the requested page (other query parameters must match!)
- NextPageToken string `protobuf:"bytes,1,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"`
- // Maximum number of items to return
- Limit int64 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
- // Resource identifier
- Key *ResourceKey `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"`
- // List the deleted values (eg, show trash)
- Origin string `protobuf:"bytes,4,opt,name=origin,proto3" json:"origin,omitempty"`
-}
-
-func (x *OriginRequest) Reset() {
- *x = OriginRequest{}
- if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[21]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *OriginRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*OriginRequest) ProtoMessage() {}
-
-func (x *OriginRequest) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[21]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use OriginRequest.ProtoReflect.Descriptor instead.
-func (*OriginRequest) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{21}
-}
-
-func (x *OriginRequest) GetNextPageToken() string {
- if x != nil {
- return x.NextPageToken
- }
- return ""
-}
-
-func (x *OriginRequest) GetLimit() int64 {
- if x != nil {
- return x.Limit
- }
- return 0
-}
-
-func (x *OriginRequest) GetKey() *ResourceKey {
- if x != nil {
- return x.Key
- }
- return nil
-}
-
-func (x *OriginRequest) GetOrigin() string {
- if x != nil {
- return x.Origin
- }
- return ""
-}
-
-type ResourceOriginInfo struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- // The resource
- Key *ResourceKey `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
- // Size of the full resource body
- ResourceSize int32 `protobuf:"varint,2,opt,name=resource_size,json=resourceSize,proto3" json:"resource_size,omitempty"`
- // Hash for the resource
- ResourceHash string `protobuf:"bytes,3,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"`
- // The origin name
- Origin string `protobuf:"bytes,4,opt,name=origin,proto3" json:"origin,omitempty"`
- // Path on the origin
- Path string `protobuf:"bytes,5,opt,name=path,proto3" json:"path,omitempty"`
- // Verification hash from the origin
- Hash string `protobuf:"bytes,6,opt,name=hash,proto3" json:"hash,omitempty"`
- // Change time from the origin
- Timestamp int64 `protobuf:"varint,7,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
-}
-
-func (x *ResourceOriginInfo) Reset() {
- *x = ResourceOriginInfo{}
- if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[22]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *ResourceOriginInfo) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*ResourceOriginInfo) ProtoMessage() {}
-
-func (x *ResourceOriginInfo) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[22]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use ResourceOriginInfo.ProtoReflect.Descriptor instead.
-func (*ResourceOriginInfo) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{22}
-}
-
-func (x *ResourceOriginInfo) GetKey() *ResourceKey {
- if x != nil {
- return x.Key
- }
- return nil
-}
-
-func (x *ResourceOriginInfo) GetResourceSize() int32 {
- if x != nil {
- return x.ResourceSize
- }
- return 0
-}
-
-func (x *ResourceOriginInfo) GetResourceHash() string {
- if x != nil {
- return x.ResourceHash
- }
- return ""
-}
-
-func (x *ResourceOriginInfo) GetOrigin() string {
- if x != nil {
- return x.Origin
- }
- return ""
-}
-
-func (x *ResourceOriginInfo) GetPath() string {
- if x != nil {
- return x.Path
- }
- return ""
-}
-
-func (x *ResourceOriginInfo) GetHash() string {
- if x != nil {
- return x.Hash
- }
- return ""
-}
-
-func (x *ResourceOriginInfo) GetTimestamp() int64 {
- if x != nil {
- return x.Timestamp
- }
- return 0
-}
-
-type OriginResponse struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- Items []*ResourceOriginInfo `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"`
- // More results exist... pass this in the next request
- NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"`
- // ResourceVersion of the list response
- ResourceVersion int64 `protobuf:"varint,3,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"`
-}
-
-func (x *OriginResponse) Reset() {
- *x = OriginResponse{}
- if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[23]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *OriginResponse) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*OriginResponse) ProtoMessage() {}
-
-func (x *OriginResponse) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[23]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use OriginResponse.ProtoReflect.Descriptor instead.
-func (*OriginResponse) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{23}
-}
-
-func (x *OriginResponse) GetItems() []*ResourceOriginInfo {
- if x != nil {
- return x.Items
- }
- return nil
-}
-
-func (x *OriginResponse) GetNextPageToken() string {
- if x != nil {
- return x.NextPageToken
- }
- return ""
-}
-
-func (x *OriginResponse) GetResourceVersion() int64 {
- if x != nil {
- return x.ResourceVersion
- }
- return 0
-}
-
type HealthCheckRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1977,7 +1437,7 @@ type HealthCheckRequest struct {
func (x *HealthCheckRequest) Reset() {
*x = HealthCheckRequest{}
if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[24]
+ mi := &file_resource_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1990,7 +1450,7 @@ func (x *HealthCheckRequest) String() string {
func (*HealthCheckRequest) ProtoMessage() {}
func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[24]
+ mi := &file_resource_proto_msgTypes[18]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2003,7 +1463,7 @@ func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead.
func (*HealthCheckRequest) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{24}
+ return file_resource_proto_rawDescGZIP(), []int{18}
}
func (x *HealthCheckRequest) GetService() string {
@@ -2024,7 +1484,7 @@ type HealthCheckResponse struct {
func (x *HealthCheckResponse) Reset() {
*x = HealthCheckResponse{}
if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[25]
+ mi := &file_resource_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2037,7 +1497,7 @@ func (x *HealthCheckResponse) String() string {
func (*HealthCheckResponse) ProtoMessage() {}
func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[25]
+ mi := &file_resource_proto_msgTypes[19]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2050,7 +1510,7 @@ func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead.
func (*HealthCheckResponse) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{25}
+ return file_resource_proto_rawDescGZIP(), []int{19}
}
func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus {
@@ -2060,327 +1520,6 @@ func (x *HealthCheckResponse) GetStatus() HealthCheckResponse_ServingStatus {
return HealthCheckResponse_UNKNOWN
}
-type PutBlobRequest struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- // The resource that will use this blob
- // NOTE: the name may not yet exist, but group+resource are required
- Resource *ResourceKey `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"`
- // How to upload
- Method PutBlobRequest_Method `protobuf:"varint,2,opt,name=method,proto3,enum=resource.PutBlobRequest_Method" json:"method,omitempty"`
- // Content type header
- ContentType string `protobuf:"bytes,3,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"`
- // Raw value to write
- // Not valid when method == HTTP
- Value []byte `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"`
-}
-
-func (x *PutBlobRequest) Reset() {
- *x = PutBlobRequest{}
- if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[26]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *PutBlobRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*PutBlobRequest) ProtoMessage() {}
-
-func (x *PutBlobRequest) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[26]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use PutBlobRequest.ProtoReflect.Descriptor instead.
-func (*PutBlobRequest) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{26}
-}
-
-func (x *PutBlobRequest) GetResource() *ResourceKey {
- if x != nil {
- return x.Resource
- }
- return nil
-}
-
-func (x *PutBlobRequest) GetMethod() PutBlobRequest_Method {
- if x != nil {
- return x.Method
- }
- return PutBlobRequest_GRPC
-}
-
-func (x *PutBlobRequest) GetContentType() string {
- if x != nil {
- return x.ContentType
- }
- return ""
-}
-
-func (x *PutBlobRequest) GetValue() []byte {
- if x != nil {
- return x.Value
- }
- return nil
-}
-
-type PutBlobResponse struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- // Status code
- Status *StatusResult `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
- // The blob uid. This must be saved into the resource to support access
- Uid string `protobuf:"bytes,2,opt,name=uid,proto3" json:"uid,omitempty"`
- // The URL where this value can be PUT
- Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
- // Size of the uploaded blob
- Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"`
- // Content hash used for an etag
- Hash string `protobuf:"bytes,5,opt,name=hash,proto3" json:"hash,omitempty"`
- // Validated mimetype (from content_type)
- MimeType string `protobuf:"bytes,6,opt,name=mime_type,json=mimeType,proto3" json:"mime_type,omitempty"`
- // Validated charset (from content_type)
- Charset string `protobuf:"bytes,7,opt,name=charset,proto3" json:"charset,omitempty"`
-}
-
-func (x *PutBlobResponse) Reset() {
- *x = PutBlobResponse{}
- if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[27]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *PutBlobResponse) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*PutBlobResponse) ProtoMessage() {}
-
-func (x *PutBlobResponse) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[27]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use PutBlobResponse.ProtoReflect.Descriptor instead.
-func (*PutBlobResponse) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{27}
-}
-
-func (x *PutBlobResponse) GetStatus() *StatusResult {
- if x != nil {
- return x.Status
- }
- return nil
-}
-
-func (x *PutBlobResponse) GetUid() string {
- if x != nil {
- return x.Uid
- }
- return ""
-}
-
-func (x *PutBlobResponse) GetUrl() string {
- if x != nil {
- return x.Url
- }
- return ""
-}
-
-func (x *PutBlobResponse) GetSize() int64 {
- if x != nil {
- return x.Size
- }
- return 0
-}
-
-func (x *PutBlobResponse) GetHash() string {
- if x != nil {
- return x.Hash
- }
- return ""
-}
-
-func (x *PutBlobResponse) GetMimeType() string {
- if x != nil {
- return x.MimeType
- }
- return ""
-}
-
-func (x *PutBlobResponse) GetCharset() string {
- if x != nil {
- return x.Charset
- }
- return ""
-}
-
-type GetBlobRequest struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- Resource *ResourceKey `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"`
- // The new resource version
- ResourceVersion int64 `protobuf:"varint,2,opt,name=resource_version,json=resourceVersion,proto3" json:"resource_version,omitempty"`
- // Do not return a pre-signed URL (when possible)
- MustProxyBytes bool `protobuf:"varint,3,opt,name=must_proxy_bytes,json=mustProxyBytes,proto3" json:"must_proxy_bytes,omitempty"`
-}
-
-func (x *GetBlobRequest) Reset() {
- *x = GetBlobRequest{}
- if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[28]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *GetBlobRequest) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*GetBlobRequest) ProtoMessage() {}
-
-func (x *GetBlobRequest) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[28]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use GetBlobRequest.ProtoReflect.Descriptor instead.
-func (*GetBlobRequest) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{28}
-}
-
-func (x *GetBlobRequest) GetResource() *ResourceKey {
- if x != nil {
- return x.Resource
- }
- return nil
-}
-
-func (x *GetBlobRequest) GetResourceVersion() int64 {
- if x != nil {
- return x.ResourceVersion
- }
- return 0
-}
-
-func (x *GetBlobRequest) GetMustProxyBytes() bool {
- if x != nil {
- return x.MustProxyBytes
- }
- return false
-}
-
-type GetBlobResponse struct {
- state protoimpl.MessageState
- sizeCache protoimpl.SizeCache
- unknownFields protoimpl.UnknownFields
-
- // Status code sent on errors
- Status *StatusResult `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
- // (optional) When possible, the system will return a presigned URL
- // that can be used to actually read the full blob+metadata
- // When this is set, neither info nor value will be set
- Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"`
- // Content type
- ContentType string `protobuf:"bytes,3,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"`
- // The raw object value
- Value []byte `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"`
-}
-
-func (x *GetBlobResponse) Reset() {
- *x = GetBlobResponse{}
- if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[29]
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- ms.StoreMessageInfo(mi)
- }
-}
-
-func (x *GetBlobResponse) String() string {
- return protoimpl.X.MessageStringOf(x)
-}
-
-func (*GetBlobResponse) ProtoMessage() {}
-
-func (x *GetBlobResponse) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[29]
- if protoimpl.UnsafeEnabled && x != nil {
- ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
- if ms.LoadMessageInfo() == nil {
- ms.StoreMessageInfo(mi)
- }
- return ms
- }
- return mi.MessageOf(x)
-}
-
-// Deprecated: Use GetBlobResponse.ProtoReflect.Descriptor instead.
-func (*GetBlobResponse) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{29}
-}
-
-func (x *GetBlobResponse) GetStatus() *StatusResult {
- if x != nil {
- return x.Status
- }
- return nil
-}
-
-func (x *GetBlobResponse) GetUrl() string {
- if x != nil {
- return x.Url
- }
- return ""
-}
-
-func (x *GetBlobResponse) GetContentType() string {
- if x != nil {
- return x.ContentType
- }
- return ""
-}
-
-func (x *GetBlobResponse) GetValue() []byte {
- if x != nil {
- return x.Value
- }
- return nil
-}
-
type WatchEvent_Resource struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -2393,7 +1532,7 @@ type WatchEvent_Resource struct {
func (x *WatchEvent_Resource) Reset() {
*x = WatchEvent_Resource{}
if protoimpl.UnsafeEnabled {
- mi := &file_resource_proto_msgTypes[30]
+ mi := &file_resource_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2406,7 +1545,7 @@ func (x *WatchEvent_Resource) String() string {
func (*WatchEvent_Resource) ProtoMessage() {}
func (x *WatchEvent_Resource) ProtoReflect() protoreflect.Message {
- mi := &file_resource_proto_msgTypes[30]
+ mi := &file_resource_proto_msgTypes[20]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2419,7 +1558,7 @@ func (x *WatchEvent_Resource) ProtoReflect() protoreflect.Message {
// Deprecated: Use WatchEvent_Resource.ProtoReflect.Descriptor instead.
func (*WatchEvent_Resource) Descriptor() ([]byte, []int) {
- return file_resource_proto_rawDescGZIP(), []int{18, 0}
+ return file_resource_proto_rawDescGZIP(), []int{17, 0}
}
func (x *WatchEvent_Resource) GetVersion() int64 {
@@ -2505,280 +1644,154 @@ var file_resource_proto_rawDesc = []byte{
0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72,
0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x75,
- 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x81, 0x01,
- 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
- 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74,
- 0x75, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
- 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72,
- 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76,
- 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
- 0x65, 0x22, 0x61, 0x0a, 0x0b, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e,
- 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20,
- 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72,
- 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x7f, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01,
- 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
- 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74,
- 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f,
- 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12,
- 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05,
- 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x53, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65,
- 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74,
- 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74,
- 0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03,
- 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x64, 0x0a, 0x04, 0x53, 0x6f,
- 0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x2a, 0x0a, 0x05, 0x6f, 0x72, 0x64, 0x65,
- 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
- 0x63, 0x65, 0x2e, 0x53, 0x6f, 0x72, 0x74, 0x2e, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x05, 0x6f,
- 0x72, 0x64, 0x65, 0x72, 0x22, 0x1a, 0x0a, 0x05, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x07, 0x0a,
- 0x03, 0x41, 0x53, 0x43, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x45, 0x53, 0x43, 0x10, 0x01,
- 0x22, 0x94, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
- 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e,
- 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x06, 0x6c, 0x61, 0x62,
- 0x65, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74,
- 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x66, 0x69, 0x65, 0x6c,
- 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52,
- 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x22, 0xec, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74,
- 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f,
- 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12,
+ 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x22, 0x6b, 0x0a,
+ 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x2e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75,
+ 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73,
0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x43, 0x0a, 0x0d, 0x76, 0x65,
- 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28,
- 0x0e, 0x32, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63,
- 0x68, 0x52, 0x0c, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12,
- 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05,
- 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
- 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f,
- 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xc4, 0x01, 0x0a, 0x0c, 0x4c, 0x69, 0x73, 0x74, 0x52,
- 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73,
- 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72, 0x61, 0x70, 0x70, 0x65,
- 0x72, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74,
- 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
- 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72,
- 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x14, 0x72,
- 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x63, 0x6f,
- 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x61, 0x69,
- 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xb9, 0x01,
- 0x0a, 0x0c, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14,
- 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73,
- 0x69, 0x6e, 0x63, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18,
- 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70,
- 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x65, 0x6e, 0x64, 0x5f, 0x69, 0x6e,
- 0x69, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x01,
- 0x28, 0x08, 0x52, 0x11, 0x73, 0x65, 0x6e, 0x64, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x45,
- 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x77,
- 0x61, 0x74, 0x63, 0x68, 0x5f, 0x62, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x73, 0x18, 0x05,
- 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x57, 0x61, 0x74, 0x63, 0x68,
- 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x73, 0x22, 0xdf, 0x02, 0x0a, 0x0a, 0x57, 0x61,
- 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65,
- 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d,
- 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
- 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52,
- 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
- 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
- 0x12, 0x39, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61,
- 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x52, 0x08, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x1a, 0x3a, 0x0a, 0x08, 0x52,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
- 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
- 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
- 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x52, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12,
- 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05,
- 0x41, 0x44, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x4f, 0x44, 0x49, 0x46,
- 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x44,
- 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d, 0x41, 0x52, 0x4b, 0x10, 0x04,
- 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x9a, 0x01, 0x0a, 0x0e,
- 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26,
- 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65,
- 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67,
- 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a, 0x03,
- 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79,
- 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x68, 0x6f, 0x77, 0x5f, 0x64, 0x65,
- 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x73, 0x68, 0x6f,
- 0x77, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x22, 0x92, 0x01, 0x0a, 0x0f, 0x48, 0x69, 0x73,
- 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2c, 0x0a, 0x05,
- 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d,
- 0x65, 0x74, 0x61, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65,
- 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20,
+ 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x61, 0x0a, 0x0b, 0x52, 0x65,
+ 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
+ 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b,
+ 0x65, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76,
+ 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x7f, 0x0a,
+ 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a,
+ 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e,
+ 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52,
+ 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a,
+ 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
+ 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
+ 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
+ 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x53,
+ 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x10, 0x0a,
+ 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12,
+ 0x1a, 0x0a, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x08, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x76,
+ 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x76, 0x61, 0x6c,
+ 0x75, 0x65, 0x73, 0x22, 0x94, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69,
+ 0x6f, 0x6e, 0x73, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2d, 0x0a, 0x06,
+ 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72,
+ 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d,
+ 0x65, 0x6e, 0x74, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x2d, 0x0a, 0x06, 0x66,
+ 0x69, 0x65, 0x6c, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x6d, 0x65,
+ 0x6e, 0x74, 0x52, 0x06, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x22, 0xec, 0x01, 0x0a, 0x0b, 0x4c,
+ 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65,
+ 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b,
0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76,
- 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x8e, 0x01,
- 0x0a, 0x0d, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
- 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b,
- 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61,
- 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x27, 0x0a,
- 0x03, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65,
- 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e,
- 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x22, 0xe5,
- 0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67, 0x69,
- 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x27, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x23,
- 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53,
- 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f,
- 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x72, 0x69, 0x67,
- 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e,
- 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
- 0x70, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65,
- 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d,
- 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x97, 0x01, 0x0a, 0x0e, 0x4f, 0x72, 0x69, 0x67, 0x69,
- 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x69, 0x74, 0x65,
- 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4f, 0x72, 0x69, 0x67,
- 0x69, 0x6e, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a,
- 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65,
- 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
- 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
- 0x22, 0x2e, 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52,
- 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
- 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
- 0x22, 0xab, 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b,
- 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74,
- 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52,
- 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53,
- 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a,
- 0x0d, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b,
- 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53,
- 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f,
- 0x53, 0x45, 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52,
- 0x56, 0x49, 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x22, 0xd3,
- 0x01, 0x0a, 0x0e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
- 0x74, 0x12, 0x31, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
- 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d,
- 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x21, 0x0a,
- 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65,
- 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52,
- 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x1c, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64,
- 0x12, 0x08, 0x0a, 0x04, 0x47, 0x52, 0x50, 0x43, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x54,
- 0x54, 0x50, 0x10, 0x01, 0x22, 0xc4, 0x01, 0x0a, 0x0f, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62,
- 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74,
- 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74,
- 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72,
- 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04,
- 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65,
- 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
- 0x68, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6d, 0x65, 0x5f, 0x74, 0x79, 0x70,
- 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69, 0x6d, 0x65, 0x54, 0x79, 0x70,
- 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x18, 0x07, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x22, 0x98, 0x01, 0x0a, 0x0e,
- 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31,
- 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
- 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x76, 0x65,
- 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x10,
- 0x6d, 0x75, 0x73, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x5f, 0x62, 0x79, 0x74, 0x65, 0x73,
- 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6d, 0x75, 0x73, 0x74, 0x50, 0x72, 0x6f, 0x78,
- 0x79, 0x42, 0x79, 0x74, 0x65, 0x73, 0x22, 0x8c, 0x01, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x6c,
- 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x74,
- 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x65, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x75,
- 0x6c, 0x74, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72,
- 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x21, 0x0a, 0x0c,
- 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12,
- 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05,
- 0x76, 0x61, 0x6c, 0x75, 0x65, 0x2a, 0x33, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a,
- 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72, 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12,
- 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10, 0x01, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04,
- 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
- 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e,
+ 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x43, 0x0a,
+ 0x0d, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x18, 0x03,
+ 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
+ 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d,
+ 0x61, 0x74, 0x63, 0x68, 0x52, 0x0c, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x61, 0x74,
+ 0x63, 0x68, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28,
+ 0x03, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69,
+ 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73,
+ 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xc4, 0x01, 0x0a, 0x0c, 0x4c, 0x69,
+ 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x05, 0x69, 0x74,
+ 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x57, 0x72, 0x61,
+ 0x70, 0x70, 0x65, 0x72, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e,
+ 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f,
+ 0x6b, 0x65, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f,
+ 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x72,
+ 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30,
+ 0x0a, 0x14, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x69, 0x74, 0x65, 0x6d,
+ 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x72, 0x65,
+ 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x43, 0x6f, 0x75, 0x6e, 0x74,
+ 0x22, 0xb9, 0x01, 0x0a, 0x0c, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
+ 0x52, 0x05, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f,
+ 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52,
+ 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x65, 0x6e, 0x64,
+ 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18,
+ 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x73, 0x65, 0x6e, 0x64, 0x49, 0x6e, 0x69, 0x74, 0x69,
+ 0x61, 0x6c, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x61, 0x6c, 0x6c, 0x6f,
+ 0x77, 0x5f, 0x77, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x62, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b,
+ 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x57, 0x61,
+ 0x74, 0x63, 0x68, 0x42, 0x6f, 0x6f, 0x6b, 0x6d, 0x61, 0x72, 0x6b, 0x73, 0x22, 0xdf, 0x02, 0x0a,
+ 0x0a, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74,
+ 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09,
+ 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x2d, 0x0a, 0x04, 0x74, 0x79, 0x70,
+ 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
+ 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x54, 0x79,
+ 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74,
+ 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x12, 0x39, 0x0a, 0x08, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x18,
+ 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+ 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x1a, 0x3a,
+ 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65,
+ 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72,
+ 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x0c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x52, 0x0a, 0x04, 0x54, 0x79,
+ 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12,
+ 0x09, 0x0a, 0x05, 0x41, 0x44, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x4f,
+ 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x4c, 0x45,
+ 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x42, 0x4f, 0x4f, 0x4b, 0x4d, 0x41, 0x52,
+ 0x4b, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x05, 0x22, 0x2e,
+ 0x0a, 0x12, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x22, 0xab,
+ 0x01, 0x0a, 0x13, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x43, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
+ 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61,
+ 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x4f, 0x0a, 0x0d, 0x53,
+ 0x65, 0x72, 0x76, 0x69, 0x6e, 0x67, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07,
+ 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x45, 0x52,
+ 0x56, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x4e, 0x4f, 0x54, 0x5f, 0x53, 0x45,
+ 0x52, 0x56, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x53, 0x45, 0x52, 0x56, 0x49,
+ 0x43, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x2a, 0x33, 0x0a, 0x14,
+ 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d,
+ 0x61, 0x74, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x4f, 0x6c, 0x64, 0x65, 0x72,
+ 0x54, 0x68, 0x61, 0x6e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x78, 0x61, 0x63, 0x74, 0x10,
+ 0x01, 0x32, 0xed, 0x02, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74,
+ 0x6f, 0x72, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65,
+ 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x43, 0x72,
+ 0x65, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
+ 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e,
0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52,
- 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55,
- 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a,
- 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
- 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65,
- 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69,
- 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69,
- 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
- 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63, 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61,
- 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x32, 0xc3, 0x01, 0x0a, 0x0d, 0x52,
- 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x35, 0x0a, 0x04,
- 0x52, 0x65, 0x61, 0x64, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e,
- 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x72, 0x65,
- 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x18,
- 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72,
- 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x2e, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x12, 0x17, 0x2e,
- 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52,
- 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63,
- 0x65, 0x2e, 0x4f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x32, 0x8b, 0x01, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x62, 0x53, 0x74, 0x6f, 0x72, 0x65, 0x12, 0x3e,
- 0x0a, 0x07, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x50,
- 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e,
- 0x0a, 0x07, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f,
- 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75,
- 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x47,
- 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x57,
- 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x48, 0x0a,
- 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e, 0x72, 0x65, 0x73,
- 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63,
- 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75,
- 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52,
- 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, 0x75,
- 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x67, 0x72,
- 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
- 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
- 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x55, 0x70, 0x64, 0x61, 0x74,
+ 0x65, 0x12, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64,
+ 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x17,
+ 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72,
+ 0x63, 0x65, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x12, 0x35, 0x0a, 0x04, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x1a, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x05, 0x57, 0x61, 0x74, 0x63,
+ 0x68, 0x12, 0x16, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74,
+ 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x72, 0x65, 0x73, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x2e, 0x57, 0x61, 0x74, 0x63, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30,
+ 0x01, 0x32, 0x57, 0x0a, 0x0b, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73,
+ 0x12, 0x48, 0x0a, 0x09, 0x49, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x1c, 0x2e,
+ 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43,
+ 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x72, 0x65,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65,
+ 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69,
+ 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61,
+ 0x2f, 0x67, 0x72, 0x61, 0x66, 0x61, 0x6e, 0x61, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x73, 0x74, 0x6f,
+ 0x72, 0x61, 0x67, 0x65, 0x2f, 0x75, 0x6e, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2f, 0x72, 0x65, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -2793,106 +1806,73 @@ func file_resource_proto_rawDescGZIP() []byte {
return file_resource_proto_rawDescData
}
-var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
-var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 31)
+var file_resource_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
+var file_resource_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
var file_resource_proto_goTypes = []interface{}{
(ResourceVersionMatch)(0), // 0: resource.ResourceVersionMatch
- (Sort_Order)(0), // 1: resource.Sort.Order
- (WatchEvent_Type)(0), // 2: resource.WatchEvent.Type
- (HealthCheckResponse_ServingStatus)(0), // 3: resource.HealthCheckResponse.ServingStatus
- (PutBlobRequest_Method)(0), // 4: resource.PutBlobRequest.Method
- (*ResourceKey)(nil), // 5: resource.ResourceKey
- (*ResourceWrapper)(nil), // 6: resource.ResourceWrapper
- (*ResourceMeta)(nil), // 7: resource.ResourceMeta
- (*StatusResult)(nil), // 8: resource.StatusResult
- (*CreateRequest)(nil), // 9: resource.CreateRequest
- (*CreateResponse)(nil), // 10: resource.CreateResponse
- (*UpdateRequest)(nil), // 11: resource.UpdateRequest
- (*UpdateResponse)(nil), // 12: resource.UpdateResponse
- (*DeleteRequest)(nil), // 13: resource.DeleteRequest
- (*DeleteResponse)(nil), // 14: resource.DeleteResponse
- (*ReadRequest)(nil), // 15: resource.ReadRequest
- (*ReadResponse)(nil), // 16: resource.ReadResponse
- (*Requirement)(nil), // 17: resource.Requirement
- (*Sort)(nil), // 18: resource.Sort
- (*ListOptions)(nil), // 19: resource.ListOptions
- (*ListRequest)(nil), // 20: resource.ListRequest
- (*ListResponse)(nil), // 21: resource.ListResponse
- (*WatchRequest)(nil), // 22: resource.WatchRequest
- (*WatchEvent)(nil), // 23: resource.WatchEvent
- (*HistoryRequest)(nil), // 24: resource.HistoryRequest
- (*HistoryResponse)(nil), // 25: resource.HistoryResponse
- (*OriginRequest)(nil), // 26: resource.OriginRequest
- (*ResourceOriginInfo)(nil), // 27: resource.ResourceOriginInfo
- (*OriginResponse)(nil), // 28: resource.OriginResponse
- (*HealthCheckRequest)(nil), // 29: resource.HealthCheckRequest
- (*HealthCheckResponse)(nil), // 30: resource.HealthCheckResponse
- (*PutBlobRequest)(nil), // 31: resource.PutBlobRequest
- (*PutBlobResponse)(nil), // 32: resource.PutBlobResponse
- (*GetBlobRequest)(nil), // 33: resource.GetBlobRequest
- (*GetBlobResponse)(nil), // 34: resource.GetBlobResponse
- (*WatchEvent_Resource)(nil), // 35: resource.WatchEvent.Resource
+ (WatchEvent_Type)(0), // 1: resource.WatchEvent.Type
+ (HealthCheckResponse_ServingStatus)(0), // 2: resource.HealthCheckResponse.ServingStatus
+ (*ResourceKey)(nil), // 3: resource.ResourceKey
+ (*ResourceWrapper)(nil), // 4: resource.ResourceWrapper
+ (*ResourceMeta)(nil), // 5: resource.ResourceMeta
+ (*StatusResult)(nil), // 6: resource.StatusResult
+ (*CreateRequest)(nil), // 7: resource.CreateRequest
+ (*CreateResponse)(nil), // 8: resource.CreateResponse
+ (*UpdateRequest)(nil), // 9: resource.UpdateRequest
+ (*UpdateResponse)(nil), // 10: resource.UpdateResponse
+ (*DeleteRequest)(nil), // 11: resource.DeleteRequest
+ (*DeleteResponse)(nil), // 12: resource.DeleteResponse
+ (*ReadRequest)(nil), // 13: resource.ReadRequest
+ (*ReadResponse)(nil), // 14: resource.ReadResponse
+ (*Requirement)(nil), // 15: resource.Requirement
+ (*ListOptions)(nil), // 16: resource.ListOptions
+ (*ListRequest)(nil), // 17: resource.ListRequest
+ (*ListResponse)(nil), // 18: resource.ListResponse
+ (*WatchRequest)(nil), // 19: resource.WatchRequest
+ (*WatchEvent)(nil), // 20: resource.WatchEvent
+ (*HealthCheckRequest)(nil), // 21: resource.HealthCheckRequest
+ (*HealthCheckResponse)(nil), // 22: resource.HealthCheckResponse
+ (*WatchEvent_Resource)(nil), // 23: resource.WatchEvent.Resource
}
var file_resource_proto_depIdxs = []int32{
- 5, // 0: resource.CreateRequest.key:type_name -> resource.ResourceKey
- 8, // 1: resource.CreateResponse.status:type_name -> resource.StatusResult
- 5, // 2: resource.UpdateRequest.key:type_name -> resource.ResourceKey
- 8, // 3: resource.UpdateResponse.status:type_name -> resource.StatusResult
- 5, // 4: resource.DeleteRequest.key:type_name -> resource.ResourceKey
- 8, // 5: resource.DeleteResponse.status:type_name -> resource.StatusResult
- 5, // 6: resource.ReadRequest.key:type_name -> resource.ResourceKey
- 8, // 7: resource.ReadResponse.status:type_name -> resource.StatusResult
- 1, // 8: resource.Sort.order:type_name -> resource.Sort.Order
- 5, // 9: resource.ListOptions.key:type_name -> resource.ResourceKey
- 17, // 10: resource.ListOptions.labels:type_name -> resource.Requirement
- 17, // 11: resource.ListOptions.fields:type_name -> resource.Requirement
- 0, // 12: resource.ListRequest.version_match:type_name -> resource.ResourceVersionMatch
- 19, // 13: resource.ListRequest.options:type_name -> resource.ListOptions
- 6, // 14: resource.ListResponse.items:type_name -> resource.ResourceWrapper
- 19, // 15: resource.WatchRequest.options:type_name -> resource.ListOptions
- 2, // 16: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type
- 35, // 17: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource
- 35, // 18: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource
- 5, // 19: resource.HistoryRequest.key:type_name -> resource.ResourceKey
- 7, // 20: resource.HistoryResponse.items:type_name -> resource.ResourceMeta
- 5, // 21: resource.OriginRequest.key:type_name -> resource.ResourceKey
- 5, // 22: resource.ResourceOriginInfo.key:type_name -> resource.ResourceKey
- 27, // 23: resource.OriginResponse.items:type_name -> resource.ResourceOriginInfo
- 3, // 24: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus
- 5, // 25: resource.PutBlobRequest.resource:type_name -> resource.ResourceKey
- 4, // 26: resource.PutBlobRequest.method:type_name -> resource.PutBlobRequest.Method
- 8, // 27: resource.PutBlobResponse.status:type_name -> resource.StatusResult
- 5, // 28: resource.GetBlobRequest.resource:type_name -> resource.ResourceKey
- 8, // 29: resource.GetBlobResponse.status:type_name -> resource.StatusResult
- 15, // 30: resource.ResourceStore.Read:input_type -> resource.ReadRequest
- 9, // 31: resource.ResourceStore.Create:input_type -> resource.CreateRequest
- 11, // 32: resource.ResourceStore.Update:input_type -> resource.UpdateRequest
- 13, // 33: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest
- 20, // 34: resource.ResourceStore.List:input_type -> resource.ListRequest
- 22, // 35: resource.ResourceStore.Watch:input_type -> resource.WatchRequest
- 15, // 36: resource.ResourceIndex.Read:input_type -> resource.ReadRequest
- 24, // 37: resource.ResourceIndex.History:input_type -> resource.HistoryRequest
- 26, // 38: resource.ResourceIndex.Origin:input_type -> resource.OriginRequest
- 31, // 39: resource.BlobStore.PutBlob:input_type -> resource.PutBlobRequest
- 33, // 40: resource.BlobStore.GetBlob:input_type -> resource.GetBlobRequest
- 29, // 41: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest
- 16, // 42: resource.ResourceStore.Read:output_type -> resource.ReadResponse
- 10, // 43: resource.ResourceStore.Create:output_type -> resource.CreateResponse
- 12, // 44: resource.ResourceStore.Update:output_type -> resource.UpdateResponse
- 14, // 45: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse
- 21, // 46: resource.ResourceStore.List:output_type -> resource.ListResponse
- 23, // 47: resource.ResourceStore.Watch:output_type -> resource.WatchEvent
- 16, // 48: resource.ResourceIndex.Read:output_type -> resource.ReadResponse
- 25, // 49: resource.ResourceIndex.History:output_type -> resource.HistoryResponse
- 28, // 50: resource.ResourceIndex.Origin:output_type -> resource.OriginResponse
- 32, // 51: resource.BlobStore.PutBlob:output_type -> resource.PutBlobResponse
- 34, // 52: resource.BlobStore.GetBlob:output_type -> resource.GetBlobResponse
- 30, // 53: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse
- 42, // [42:54] is the sub-list for method output_type
- 30, // [30:42] is the sub-list for method input_type
- 30, // [30:30] is the sub-list for extension type_name
- 30, // [30:30] is the sub-list for extension extendee
- 0, // [0:30] is the sub-list for field type_name
+ 3, // 0: resource.CreateRequest.key:type_name -> resource.ResourceKey
+ 6, // 1: resource.CreateResponse.status:type_name -> resource.StatusResult
+ 3, // 2: resource.UpdateRequest.key:type_name -> resource.ResourceKey
+ 6, // 3: resource.UpdateResponse.status:type_name -> resource.StatusResult
+ 3, // 4: resource.DeleteRequest.key:type_name -> resource.ResourceKey
+ 6, // 5: resource.DeleteResponse.status:type_name -> resource.StatusResult
+ 3, // 6: resource.ReadRequest.key:type_name -> resource.ResourceKey
+ 6, // 7: resource.ReadResponse.status:type_name -> resource.StatusResult
+ 3, // 8: resource.ListOptions.key:type_name -> resource.ResourceKey
+ 15, // 9: resource.ListOptions.labels:type_name -> resource.Requirement
+ 15, // 10: resource.ListOptions.fields:type_name -> resource.Requirement
+ 0, // 11: resource.ListRequest.version_match:type_name -> resource.ResourceVersionMatch
+ 16, // 12: resource.ListRequest.options:type_name -> resource.ListOptions
+ 4, // 13: resource.ListResponse.items:type_name -> resource.ResourceWrapper
+ 16, // 14: resource.WatchRequest.options:type_name -> resource.ListOptions
+ 1, // 15: resource.WatchEvent.type:type_name -> resource.WatchEvent.Type
+ 23, // 16: resource.WatchEvent.resource:type_name -> resource.WatchEvent.Resource
+ 23, // 17: resource.WatchEvent.previous:type_name -> resource.WatchEvent.Resource
+ 2, // 18: resource.HealthCheckResponse.status:type_name -> resource.HealthCheckResponse.ServingStatus
+ 13, // 19: resource.ResourceStore.Read:input_type -> resource.ReadRequest
+ 7, // 20: resource.ResourceStore.Create:input_type -> resource.CreateRequest
+ 9, // 21: resource.ResourceStore.Update:input_type -> resource.UpdateRequest
+ 11, // 22: resource.ResourceStore.Delete:input_type -> resource.DeleteRequest
+ 17, // 23: resource.ResourceStore.List:input_type -> resource.ListRequest
+ 19, // 24: resource.ResourceStore.Watch:input_type -> resource.WatchRequest
+ 21, // 25: resource.Diagnostics.IsHealthy:input_type -> resource.HealthCheckRequest
+ 14, // 26: resource.ResourceStore.Read:output_type -> resource.ReadResponse
+ 8, // 27: resource.ResourceStore.Create:output_type -> resource.CreateResponse
+ 10, // 28: resource.ResourceStore.Update:output_type -> resource.UpdateResponse
+ 12, // 29: resource.ResourceStore.Delete:output_type -> resource.DeleteResponse
+ 18, // 30: resource.ResourceStore.List:output_type -> resource.ListResponse
+ 20, // 31: resource.ResourceStore.Watch:output_type -> resource.WatchEvent
+ 22, // 32: resource.Diagnostics.IsHealthy:output_type -> resource.HealthCheckResponse
+ 26, // [26:33] is the sub-list for method output_type
+ 19, // [19:26] is the sub-list for method input_type
+ 19, // [19:19] is the sub-list for extension type_name
+ 19, // [19:19] is the sub-list for extension extendee
+ 0, // [0:19] is the sub-list for field type_name
}
func init() { file_resource_proto_init() }
@@ -3058,18 +2038,6 @@ func file_resource_proto_init() {
}
}
file_resource_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*Sort); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_resource_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListOptions); i {
case 0:
return &v.state
@@ -3081,7 +2049,7 @@ func file_resource_proto_init() {
return nil
}
}
- file_resource_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
+ file_resource_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListRequest); i {
case 0:
return &v.state
@@ -3093,7 +2061,7 @@ func file_resource_proto_init() {
return nil
}
}
- file_resource_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
+ file_resource_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ListResponse); i {
case 0:
return &v.state
@@ -3105,7 +2073,7 @@ func file_resource_proto_init() {
return nil
}
}
- file_resource_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
+ file_resource_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*WatchRequest); i {
case 0:
return &v.state
@@ -3117,7 +2085,7 @@ func file_resource_proto_init() {
return nil
}
}
- file_resource_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
+ file_resource_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*WatchEvent); i {
case 0:
return &v.state
@@ -3129,67 +2097,7 @@ func file_resource_proto_init() {
return nil
}
}
- file_resource_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*HistoryRequest); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_resource_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*HistoryResponse); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_resource_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*OriginRequest); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_resource_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*ResourceOriginInfo); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_resource_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*OriginResponse); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_resource_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
+ file_resource_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HealthCheckRequest); i {
case 0:
return &v.state
@@ -3201,7 +2109,7 @@ func file_resource_proto_init() {
return nil
}
}
- file_resource_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
+ file_resource_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HealthCheckResponse); i {
case 0:
return &v.state
@@ -3213,55 +2121,7 @@ func file_resource_proto_init() {
return nil
}
}
- file_resource_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*PutBlobRequest); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_resource_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*PutBlobResponse); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_resource_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*GetBlobRequest); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_resource_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*GetBlobResponse); i {
- case 0:
- return &v.state
- case 1:
- return &v.sizeCache
- case 2:
- return &v.unknownFields
- default:
- return nil
- }
- }
- file_resource_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} {
+ file_resource_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*WatchEvent_Resource); i {
case 0:
return &v.state
@@ -3279,10 +2139,10 @@ func file_resource_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_resource_proto_rawDesc,
- NumEnums: 5,
- NumMessages: 31,
+ NumEnums: 3,
+ NumMessages: 21,
NumExtensions: 0,
- NumServices: 4,
+ NumServices: 2,
},
GoTypes: file_resource_proto_goTypes,
DependencyIndexes: file_resource_proto_depIdxs,
diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto
index 46fd9a8c7b7..8990e9823a0 100644
--- a/pkg/storage/unified/resource/resource.proto
+++ b/pkg/storage/unified/resource/resource.proto
@@ -122,11 +122,8 @@ message DeleteResponse {
// Status code
StatusResult status = 1;
- // The new resource version
+ // The resource version for the deletion marker
int64 resource_version = 2;
-
- // The deleted payload
- bytes value = 3;
}
message ReadRequest {
@@ -159,14 +156,6 @@ message Requirement {
repeated string values = 3; // typically one value, but depends on the operator
}
-message Sort {
- enum Order {
- ASC = 0;
- DESC = 1;
- }
- string field = 1;
- Order order = 2;
-}
message ListOptions {
// Group+Namespace+Resource (not name)
@@ -270,77 +259,6 @@ message WatchEvent {
Resource previous = 4;
}
-message HistoryRequest {
- // Starting from the requested page (other query parameters must match!)
- string next_page_token = 1;
-
- // Maximum number of items to return
- int64 limit = 2;
-
- // Resource identifier
- ResourceKey key = 3;
-
- // List the deleted values (eg, show trash)
- bool show_deleted = 4;
-}
-
-message HistoryResponse {
- repeated ResourceMeta items = 1;
-
- // More results exist... pass this in the next request
- string next_page_token = 2;
-
- // ResourceVersion of the list response
- int64 resource_version = 3;
-}
-
-message OriginRequest {
- // Starting from the requested page (other query parameters must match!)
- string next_page_token = 1;
-
- // Maximum number of items to return
- int64 limit = 2;
-
- // Resource identifier
- ResourceKey key = 3;
-
- // List the deleted values (eg, show trash)
- string origin = 4;
-}
-
-message ResourceOriginInfo {
- // The resource
- ResourceKey key = 1;
-
- // Size of the full resource body
- int32 resource_size = 2;
-
- // Hash for the resource
- string resource_hash = 3;
-
- // The origin name
- string origin = 4;
-
- // Path on the origin
- string path = 5;
-
- // Verification hash from the origin
- string hash = 6;
-
- // Change time from the origin
- int64 timestamp = 7;
-}
-
-message OriginResponse {
- repeated ResourceOriginInfo items = 1;
-
- // More results exist... pass this in the next request
- string next_page_token = 2;
-
- // ResourceVersion of the list response
- int64 resource_version = 3;
-}
-
message HealthCheckRequest {
string service = 1;
}
@@ -356,84 +274,6 @@ message HealthCheckResponse {
}
-//----------------------------
-// Blob Support
-//----------------------------
-
-message PutBlobRequest {
- enum Method {
- // Use the inline raw []byte
- GRPC = 0;
-
- // Get a signed URL and PUT the value
- HTTP = 1;
- }
-
- // The resource that will use this blob
- // NOTE: the name may not yet exist, but group+resource are required
- ResourceKey resource = 1;
-
- // How to upload
- Method method = 2;
-
- // Content type header
- string content_type = 3;
-
- // Raw value to write
- // Not valid when method == HTTP
- bytes value = 4;
-}
-
-message PutBlobResponse {
- // Status code
- StatusResult status = 1;
-
- // The blob uid. This must be saved into the resource to support access
- string uid = 2;
-
- // The URL where this value can be PUT
- string url = 3;
-
- // Size of the uploaded blob
- int64 size = 4;
-
- // Content hash used for an etag
- string hash = 5;
-
- // Validated mimetype (from content_type)
- string mime_type = 6;
-
- // Validated charset (from content_type)
- string charset = 7;
-}
-
-message GetBlobRequest {
- ResourceKey resource = 1;
-
- // The new resource version
- int64 resource_version = 2;
-
- // Do not return a pre-signed URL (when possible)
- bool must_proxy_bytes = 3;
-}
-
-message GetBlobResponse {
- // Status code sent on errors
- StatusResult status = 1;
-
- // (optional) When possible, the system will return a presigned URL
- // that can be used to actually read the full blob+metadata
- // When this is set, neither info nor value will be set
- string url = 2;
-
- // Content type
- string content_type = 3;
-
- // The raw object value
- bytes value = 4;
-}
-
-
// This provides the CRUD+List+Watch support needed for a k8s apiserver
// The semantics and behaviors of this service are constrained by kubernetes
// This does not understand the resource schemas, only deals with json bytes
@@ -455,32 +295,6 @@ service ResourceStore {
rpc Watch(WatchRequest) returns (stream WatchEvent);
}
-// Unlike the ResourceStore, this service can be exposed to clients directly
-// It should be implemented with efficient indexes and does not need read-after-write semantics
-service ResourceIndex {
- // TODO: rpc Search(...) ... eventually a typed response
-
- rpc Read(ReadRequest) returns (ReadResponse); // Duplicated -- for client read only usage
-
- // Show resource history (and trash)
- rpc History(HistoryRequest) returns (HistoryResponse);
-
- // Used for efficient provisioning
- rpc Origin(OriginRequest) returns (OriginResponse);
-}
-
-service BlobStore {
- // Upload a blob that will be saved in a resource
- rpc PutBlob(PutBlobRequest) returns (PutBlobResponse);
-
- // Get blob contents. When possible, this will return a signed URL
- // For large payloads, signed URLs are required to avoid protobuf message size limits
- rpc GetBlob(GetBlobRequest) returns (GetBlobResponse);
-
- // NOTE: there is no direct access to delete blobs
- // >> cleanup will be managed via garbage collection or direct access to the underlying storage
-}
-
// Clients can use this service directly
// NOTE: This is read only, and no read afer write guarantees
service Diagnostics {
diff --git a/pkg/storage/unified/resource/resource_grpc.pb.go b/pkg/storage/unified/resource/resource_grpc.pb.go
index ff2f9abde25..17b4d1c4c22 100644
--- a/pkg/storage/unified/resource/resource_grpc.pb.go
+++ b/pkg/storage/unified/resource/resource_grpc.pb.go
@@ -347,314 +347,6 @@ var ResourceStore_ServiceDesc = grpc.ServiceDesc{
Metadata: "resource.proto",
}
-const (
- ResourceIndex_Read_FullMethodName = "/resource.ResourceIndex/Read"
- ResourceIndex_History_FullMethodName = "/resource.ResourceIndex/History"
- ResourceIndex_Origin_FullMethodName = "/resource.ResourceIndex/Origin"
-)
-
-// ResourceIndexClient is the client API for ResourceIndex service.
-//
-// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
-//
-// Unlike the ResourceStore, this service can be exposed to clients directly
-// It should be implemented with efficient indexes and does not need read-after-write semantics
-type ResourceIndexClient interface {
- Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error)
- // Show resource history (and trash)
- History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error)
- // Used for efficient provisioning
- Origin(ctx context.Context, in *OriginRequest, opts ...grpc.CallOption) (*OriginResponse, error)
-}
-
-type resourceIndexClient struct {
- cc grpc.ClientConnInterface
-}
-
-func NewResourceIndexClient(cc grpc.ClientConnInterface) ResourceIndexClient {
- return &resourceIndexClient{cc}
-}
-
-func (c *resourceIndexClient) Read(ctx context.Context, in *ReadRequest, opts ...grpc.CallOption) (*ReadResponse, error) {
- cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
- out := new(ReadResponse)
- err := c.cc.Invoke(ctx, ResourceIndex_Read_FullMethodName, in, out, cOpts...)
- if err != nil {
- return nil, err
- }
- return out, nil
-}
-
-func (c *resourceIndexClient) History(ctx context.Context, in *HistoryRequest, opts ...grpc.CallOption) (*HistoryResponse, error) {
- cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
- out := new(HistoryResponse)
- err := c.cc.Invoke(ctx, ResourceIndex_History_FullMethodName, in, out, cOpts...)
- if err != nil {
- return nil, err
- }
- return out, nil
-}
-
-func (c *resourceIndexClient) Origin(ctx context.Context, in *OriginRequest, opts ...grpc.CallOption) (*OriginResponse, error) {
- cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
- out := new(OriginResponse)
- err := c.cc.Invoke(ctx, ResourceIndex_Origin_FullMethodName, in, out, cOpts...)
- if err != nil {
- return nil, err
- }
- return out, nil
-}
-
-// ResourceIndexServer is the server API for ResourceIndex service.
-// All implementations should embed UnimplementedResourceIndexServer
-// for forward compatibility
-//
-// Unlike the ResourceStore, this service can be exposed to clients directly
-// It should be implemented with efficient indexes and does not need read-after-write semantics
-type ResourceIndexServer interface {
- Read(context.Context, *ReadRequest) (*ReadResponse, error)
- // Show resource history (and trash)
- History(context.Context, *HistoryRequest) (*HistoryResponse, error)
- // Used for efficient provisioning
- Origin(context.Context, *OriginRequest) (*OriginResponse, error)
-}
-
-// UnimplementedResourceIndexServer should be embedded to have forward compatible implementations.
-type UnimplementedResourceIndexServer struct {
-}
-
-func (UnimplementedResourceIndexServer) Read(context.Context, *ReadRequest) (*ReadResponse, error) {
- return nil, status.Errorf(codes.Unimplemented, "method Read not implemented")
-}
-func (UnimplementedResourceIndexServer) History(context.Context, *HistoryRequest) (*HistoryResponse, error) {
- return nil, status.Errorf(codes.Unimplemented, "method History not implemented")
-}
-func (UnimplementedResourceIndexServer) Origin(context.Context, *OriginRequest) (*OriginResponse, error) {
- return nil, status.Errorf(codes.Unimplemented, "method Origin not implemented")
-}
-
-// UnsafeResourceIndexServer may be embedded to opt out of forward compatibility for this service.
-// Use of this interface is not recommended, as added methods to ResourceIndexServer will
-// result in compilation errors.
-type UnsafeResourceIndexServer interface {
- mustEmbedUnimplementedResourceIndexServer()
-}
-
-func RegisterResourceIndexServer(s grpc.ServiceRegistrar, srv ResourceIndexServer) {
- s.RegisterService(&ResourceIndex_ServiceDesc, srv)
-}
-
-func _ResourceIndex_Read_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(ReadRequest)
- if err := dec(in); err != nil {
- return nil, err
- }
- if interceptor == nil {
- return srv.(ResourceIndexServer).Read(ctx, in)
- }
- info := &grpc.UnaryServerInfo{
- Server: srv,
- FullMethod: ResourceIndex_Read_FullMethodName,
- }
- handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(ResourceIndexServer).Read(ctx, req.(*ReadRequest))
- }
- return interceptor(ctx, in, info, handler)
-}
-
-func _ResourceIndex_History_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(HistoryRequest)
- if err := dec(in); err != nil {
- return nil, err
- }
- if interceptor == nil {
- return srv.(ResourceIndexServer).History(ctx, in)
- }
- info := &grpc.UnaryServerInfo{
- Server: srv,
- FullMethod: ResourceIndex_History_FullMethodName,
- }
- handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(ResourceIndexServer).History(ctx, req.(*HistoryRequest))
- }
- return interceptor(ctx, in, info, handler)
-}
-
-func _ResourceIndex_Origin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(OriginRequest)
- if err := dec(in); err != nil {
- return nil, err
- }
- if interceptor == nil {
- return srv.(ResourceIndexServer).Origin(ctx, in)
- }
- info := &grpc.UnaryServerInfo{
- Server: srv,
- FullMethod: ResourceIndex_Origin_FullMethodName,
- }
- handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(ResourceIndexServer).Origin(ctx, req.(*OriginRequest))
- }
- return interceptor(ctx, in, info, handler)
-}
-
-// ResourceIndex_ServiceDesc is the grpc.ServiceDesc for ResourceIndex service.
-// It's only intended for direct use with grpc.RegisterService,
-// and not to be introspected or modified (even as a copy)
-var ResourceIndex_ServiceDesc = grpc.ServiceDesc{
- ServiceName: "resource.ResourceIndex",
- HandlerType: (*ResourceIndexServer)(nil),
- Methods: []grpc.MethodDesc{
- {
- MethodName: "Read",
- Handler: _ResourceIndex_Read_Handler,
- },
- {
- MethodName: "History",
- Handler: _ResourceIndex_History_Handler,
- },
- {
- MethodName: "Origin",
- Handler: _ResourceIndex_Origin_Handler,
- },
- },
- Streams: []grpc.StreamDesc{},
- Metadata: "resource.proto",
-}
-
-const (
- BlobStore_PutBlob_FullMethodName = "/resource.BlobStore/PutBlob"
- BlobStore_GetBlob_FullMethodName = "/resource.BlobStore/GetBlob"
-)
-
-// BlobStoreClient is the client API for BlobStore service.
-//
-// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
-type BlobStoreClient interface {
- // Upload a blob that will be saved in a resource
- PutBlob(ctx context.Context, in *PutBlobRequest, opts ...grpc.CallOption) (*PutBlobResponse, error)
- // Get blob contents. When possible, this will return a signed URL
- // For large payloads, signed URLs are required to avoid protobuf message size limits
- GetBlob(ctx context.Context, in *GetBlobRequest, opts ...grpc.CallOption) (*GetBlobResponse, error)
-}
-
-type blobStoreClient struct {
- cc grpc.ClientConnInterface
-}
-
-func NewBlobStoreClient(cc grpc.ClientConnInterface) BlobStoreClient {
- return &blobStoreClient{cc}
-}
-
-func (c *blobStoreClient) PutBlob(ctx context.Context, in *PutBlobRequest, opts ...grpc.CallOption) (*PutBlobResponse, error) {
- cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
- out := new(PutBlobResponse)
- err := c.cc.Invoke(ctx, BlobStore_PutBlob_FullMethodName, in, out, cOpts...)
- if err != nil {
- return nil, err
- }
- return out, nil
-}
-
-func (c *blobStoreClient) GetBlob(ctx context.Context, in *GetBlobRequest, opts ...grpc.CallOption) (*GetBlobResponse, error) {
- cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
- out := new(GetBlobResponse)
- err := c.cc.Invoke(ctx, BlobStore_GetBlob_FullMethodName, in, out, cOpts...)
- if err != nil {
- return nil, err
- }
- return out, nil
-}
-
-// BlobStoreServer is the server API for BlobStore service.
-// All implementations should embed UnimplementedBlobStoreServer
-// for forward compatibility
-type BlobStoreServer interface {
- // Upload a blob that will be saved in a resource
- PutBlob(context.Context, *PutBlobRequest) (*PutBlobResponse, error)
- // Get blob contents. When possible, this will return a signed URL
- // For large payloads, signed URLs are required to avoid protobuf message size limits
- GetBlob(context.Context, *GetBlobRequest) (*GetBlobResponse, error)
-}
-
-// UnimplementedBlobStoreServer should be embedded to have forward compatible implementations.
-type UnimplementedBlobStoreServer struct {
-}
-
-func (UnimplementedBlobStoreServer) PutBlob(context.Context, *PutBlobRequest) (*PutBlobResponse, error) {
- return nil, status.Errorf(codes.Unimplemented, "method PutBlob not implemented")
-}
-func (UnimplementedBlobStoreServer) GetBlob(context.Context, *GetBlobRequest) (*GetBlobResponse, error) {
- return nil, status.Errorf(codes.Unimplemented, "method GetBlob not implemented")
-}
-
-// UnsafeBlobStoreServer may be embedded to opt out of forward compatibility for this service.
-// Use of this interface is not recommended, as added methods to BlobStoreServer will
-// result in compilation errors.
-type UnsafeBlobStoreServer interface {
- mustEmbedUnimplementedBlobStoreServer()
-}
-
-func RegisterBlobStoreServer(s grpc.ServiceRegistrar, srv BlobStoreServer) {
- s.RegisterService(&BlobStore_ServiceDesc, srv)
-}
-
-func _BlobStore_PutBlob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(PutBlobRequest)
- if err := dec(in); err != nil {
- return nil, err
- }
- if interceptor == nil {
- return srv.(BlobStoreServer).PutBlob(ctx, in)
- }
- info := &grpc.UnaryServerInfo{
- Server: srv,
- FullMethod: BlobStore_PutBlob_FullMethodName,
- }
- handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(BlobStoreServer).PutBlob(ctx, req.(*PutBlobRequest))
- }
- return interceptor(ctx, in, info, handler)
-}
-
-func _BlobStore_GetBlob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
- in := new(GetBlobRequest)
- if err := dec(in); err != nil {
- return nil, err
- }
- if interceptor == nil {
- return srv.(BlobStoreServer).GetBlob(ctx, in)
- }
- info := &grpc.UnaryServerInfo{
- Server: srv,
- FullMethod: BlobStore_GetBlob_FullMethodName,
- }
- handler := func(ctx context.Context, req interface{}) (interface{}, error) {
- return srv.(BlobStoreServer).GetBlob(ctx, req.(*GetBlobRequest))
- }
- return interceptor(ctx, in, info, handler)
-}
-
-// BlobStore_ServiceDesc is the grpc.ServiceDesc for BlobStore service.
-// It's only intended for direct use with grpc.RegisterService,
-// and not to be introspected or modified (even as a copy)
-var BlobStore_ServiceDesc = grpc.ServiceDesc{
- ServiceName: "resource.BlobStore",
- HandlerType: (*BlobStoreServer)(nil),
- Methods: []grpc.MethodDesc{
- {
- MethodName: "PutBlob",
- Handler: _BlobStore_PutBlob_Handler,
- },
- {
- MethodName: "GetBlob",
- Handler: _BlobStore_GetBlob_Handler,
- },
- },
- Streams: []grpc.StreamDesc{},
- Metadata: "resource.proto",
-}
-
const (
Diagnostics_IsHealthy_FullMethodName = "/resource.Diagnostics/IsHealthy"
)
diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go
index 17f1fe1a6ce..0f455ca68a5 100644
--- a/pkg/storage/unified/resource/server.go
+++ b/pkg/storage/unified/resource/server.go
@@ -22,7 +22,7 @@ import (
// Package-level errors.
var (
- ErrNotFound = errors.New("resource not found")
+ ErrNotFound = errors.New("entity not found")
ErrOptimisticLockingFailed = errors.New("optimistic locking failed")
ErrUserNotFoundInContext = errors.New("user not found in context")
ErrUnableToReadResourceJSON = errors.New("unable to read resource json")
@@ -32,8 +32,6 @@ var (
// ResourceServer implements all services
type ResourceServer interface {
ResourceStoreServer
- ResourceIndexServer
- BlobStoreServer
DiagnosticsServer
LifecycleHooks
}
@@ -62,23 +60,6 @@ type StorageBackend interface {
WatchWriteEvents(ctx context.Context) (<-chan *WrittenEvent, error)
}
-// This interface is not exposed to end users directly
-// Access to this interface is already gated by access control
-type BlobStore interface {
- // Indicates if storage layer supports signed urls
- SupportsSignedURLs() bool
-
- // Get the raw blob bytes and metadata -- limited to protobuf message size
- // For larger payloads, we should use presigned URLs to upload from the client
- PutBlob(context.Context, *PutBlobRequest) (*PutBlobResponse, error)
-
- // Get blob contents. When possible, this will return a signed URL
- // For large payloads, signed URLs are required to avoid protobuf message size limits
- GetBlob(ctx context.Context, resource *ResourceKey, info *utils.BlobInfo, mustProxy bool) (*GetBlobResponse, error)
-
- // TODO? List+Delete? This is for admin access
-}
-
type ResourceServerOptions struct {
// OTel tracer
Tracer trace.Tracer
@@ -86,12 +67,6 @@ type ResourceServerOptions struct {
// Real storage backend
Backend StorageBackend
- // The blob storage engine
- Blob BlobStore
-
- // Real storage backend
- Search ResourceIndexServer
-
// Diagnostics
Diagnostics DiagnosticsServer
@@ -114,12 +89,6 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
if opts.Backend == nil {
return nil, fmt.Errorf("missing Backend implementation")
}
- if opts.Search == nil {
- opts.Search = &noopService{}
- }
- if opts.Blob == nil {
- opts.Blob = &noopService{}
- }
if opts.Diagnostics == nil {
opts.Diagnostics = &noopService{}
}
@@ -141,7 +110,6 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) {
tracer: opts.Tracer,
log: slog.Default().With("logger", "resource-server"),
backend: opts.Backend,
- search: opts.Search,
diagnostics: opts.Diagnostics,
access: opts.WriteAccess,
lifecycle: opts.Lifecycle,
@@ -157,8 +125,6 @@ type server struct {
tracer trace.Tracer
log *slog.Logger
backend StorageBackend
- search ResourceIndexServer
- blob BlobStore
diagnostics DiagnosticsServer
access WriteAccessHooks
lifecycle LifecycleHooks
@@ -276,24 +242,13 @@ func (s *server) newEventBuilder(ctx context.Context, key *ResourceKey, value, o
}
obj.SetOriginInfo(origin)
- // Make sure old values do not mutate things they should not
+ // Ensure old values do not mutate things they should not
if event.OldMeta != nil {
old := event.OldMeta
- if obj.GetUID() != event.OldMeta.GetUID() {
- return nil, apierrors.NewBadRequest(
- fmt.Sprintf("UIDs do not match (old: %s, new: %s)", old.GetUID(), obj.GetUID()))
- }
-
- // Can not change creation timestamps+user
- if obj.GetCreatedBy() != event.OldMeta.GetCreatedBy() {
- return nil, apierrors.NewBadRequest(
- fmt.Sprintf("created by changed (old: %s, new: %s)", old.GetCreatedBy(), obj.GetCreatedBy()))
- }
- if obj.GetCreationTimestamp() != event.OldMeta.GetCreationTimestamp() {
- return nil, apierrors.NewBadRequest(
- fmt.Sprintf("creation timestamp changed (old:%v, new:%v)", old.GetCreationTimestamp(), obj.GetCreationTimestamp()))
- }
+ obj.SetUID(old.GetUID())
+ obj.SetCreatedBy(old.GetCreatedBy())
+ obj.SetCreationTimestamp(old.GetCreationTimestamp())
}
return event, nil
}
@@ -431,7 +386,7 @@ func (s *server) Delete(ctx context.Context, req *DeleteRequest) (*DeleteRespons
if err != nil {
return nil, err
}
- if latest.ResourceVersion != req.ResourceVersion {
+ if req.ResourceVersion > 0 && latest.ResourceVersion != req.ResourceVersion {
return nil, ErrOptimisticLockingFailed
}
@@ -481,17 +436,15 @@ func (s *server) Read(ctx context.Context, req *ReadRequest) (*ReadResponse, err
return nil, err
}
- if req.Key.Group == "" {
- status, _ := errToStatus(apierrors.NewBadRequest("missing group"))
- return &ReadResponse{Status: status}, nil
- }
+ // if req.Key.Group == "" {
+ // status, _ := errToStatus(apierrors.NewBadRequest("missing group"))
+ // return &ReadResponse{Status: status}, nil
+ // }
if req.Key.Resource == "" {
status, _ := errToStatus(apierrors.NewBadRequest("missing resource"))
return &ReadResponse{Status: status}, nil
}
- // TODO: shall we also check for the namespace and Name ? Or is that a backend concern?
-
rsp, err := s.backend.Read(ctx, req)
if err != nil {
if rsp == nil {
@@ -536,8 +489,6 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error {
return err
}
- fmt.Printf("WATCH %v\n", req.Options.Key)
-
ctx := srv.Context()
// Start listening -- this will buffer any changes that happen while we backfill
@@ -589,76 +540,6 @@ func (s *server) Watch(req *WatchRequest, srv ResourceStore_WatchServer) error {
}
}
-// GetBlob implements ResourceServer.
-func (s *server) PutBlob(ctx context.Context, req *PutBlobRequest) (*PutBlobResponse, error) {
- if err := s.Init(); err != nil {
- return nil, err
- }
- rsp, err := s.blob.PutBlob(ctx, req)
- rsp.Status, err = errToStatus(err)
- return rsp, err
-}
-
-func (s *server) getPartialObject(ctx context.Context, key *ResourceKey, rv int64) (utils.GrafanaMetaAccessor, *StatusResult) {
- rsp, err := s.backend.Read(ctx, &ReadRequest{
- Key: key,
- ResourceVersion: rv,
- })
- if err != nil {
- rsp.Status, _ = errToStatus(err)
- }
- if rsp.Status != nil {
- return nil, rsp.Status
- }
-
- partial := &metav1.PartialObjectMetadata{}
- err = json.Unmarshal(rsp.Value, partial)
- if err != nil {
- rsp.Status, _ = errToStatus(fmt.Errorf("error reading body %w", err))
- return nil, rsp.Status
- }
- obj, err := utils.MetaAccessor(partial)
- if err != nil {
- rsp.Status, _ = errToStatus(fmt.Errorf("error getting accessor %w", err))
- return nil, rsp.Status
- }
- return obj, nil
-}
-
-// GetBlob implements ResourceServer.
-func (s *server) GetBlob(ctx context.Context, req *GetBlobRequest) (*GetBlobResponse, error) {
- if err := s.Init(); err != nil {
- return nil, err
- }
-
- // NOTE: in SQL... this could be simple select rather than a full fetch and extract
- obj, status := s.getPartialObject(ctx, req.Resource, req.ResourceVersion)
- if status != nil {
- return &GetBlobResponse{Status: status}, nil
- }
-
- info := obj.GetBlob()
- if info == nil || info.UID == "" {
- return &GetBlobResponse{Status: &StatusResult{
- Status: "Failure",
- Message: "Resource does not have a linked blob",
- Code: 404,
- }}, nil
- }
-
- rsp, err := s.blob.GetBlob(ctx, req.Resource, info, req.MustProxyBytes)
- rsp.Status, err = errToStatus(err)
- return rsp, err
-}
-
-// History implements ResourceServer.
-func (s *server) History(ctx context.Context, req *HistoryRequest) (*HistoryResponse, error) {
- if err := s.Init(); err != nil {
- return nil, err
- }
- return s.search.History(ctx, req)
-}
-
// IsHealthy implements ResourceServer.
func (s *server) IsHealthy(ctx context.Context, req *HealthCheckRequest) (*HealthCheckResponse, error) {
if err := s.Init(); err != nil {
@@ -666,11 +547,3 @@ func (s *server) IsHealthy(ctx context.Context, req *HealthCheckRequest) (*Healt
}
return s.diagnostics.IsHealthy(ctx, req)
}
-
-// Origin implements ResourceServer.
-func (s *server) Origin(ctx context.Context, req *OriginRequest) (*OriginResponse, error) {
- if err := s.Init(); err != nil {
- return nil, err
- }
- return s.search.Origin(ctx, req)
-}
diff --git a/pkg/storage/unified/resource/server_test.go b/pkg/storage/unified/resource/server_test.go
index cff77381043..e3bfd646e08 100644
--- a/pkg/storage/unified/resource/server_test.go
+++ b/pkg/storage/unified/resource/server_test.go
@@ -38,10 +38,6 @@ func TestSimpleServer(t *testing.T) {
Metadata: fileblob.MetadataDontWrite, // skip
})
require.NoError(t, err)
-<<<<<<< HEAD
-
-=======
->>>>>>> origin/resource-store-bridge
fmt.Printf("ROOT: %s\n\n", tmp)
}
store, err := NewCDKBackend(ctx, CDKBackendOptions{
diff --git a/pkg/storage/unified/sql/server.go b/pkg/storage/unified/sql/server.go
index 1dddd998cb3..eac94aba2b7 100644
--- a/pkg/storage/unified/sql/server.go
+++ b/pkg/storage/unified/sql/server.go
@@ -1,13 +1,6 @@
package sql
import (
- "context"
- "os"
- "path/filepath"
- "time"
-
- "gocloud.dev/blob/fileblob"
-
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -22,32 +15,6 @@ func ProvideResourceServer(db db.DB, cfg *setting.Cfg, features featuremgmt.Feat
Tracer: tracer,
}
- supportBlobs := true
-
- // Create a local blob filesystem blob store
- if supportBlobs {
- dir := filepath.Join(cfg.DataPath, "unistore", "blobs")
- if err := os.MkdirAll(dir, 0o750); err != nil {
- return nil, err
- }
-
- bucket, err := fileblob.OpenBucket(dir, &fileblob.Options{
- CreateDir: true,
- Metadata: fileblob.MetadataDontWrite, // skip
- })
- if err != nil {
- return nil, err
- }
- opts.Blob, err = resource.NewCDKBlobStore(context.Background(), resource.CDKBlobStoreOptions{
- Tracer: tracer,
- Bucket: bucket,
- URLExpiration: time.Minute * 20,
- })
- if err != nil {
- return nil, err
- }
- }
-
eDB, err := dbimpl.ProvideResourceDB(db, cfg, features, tracer)
if err != nil {
return nil, err
diff --git a/pkg/storage/unified/sqlnext/data/common.sql b/pkg/storage/unified/sqlnext/data/common.sql
deleted file mode 100644
index 867061825d2..00000000000
--- a/pkg/storage/unified/sqlnext/data/common.sql
+++ /dev/null
@@ -1,61 +0,0 @@
-{{/*
- This is the list of all the fields in *entity.Entity, in a way that is
- suitable to be imported by other templates that need to select these fields
- from either the "entity" or the "entity_history" tables.
-
- Example usage:
-
- SELECT {{ template "common_entity_select_into" . }}
- FROM {{ .Ident "entity" }} AS e
-
-*/}}
-{{ define "common_entity_select_into" }}
-
- e.{{ .Ident "guid" | .Into .Entity.Guid }},
- e.{{ .Ident "resource_version" | .Into .Entity.ResourceVersion }},
-
- e.{{ .Ident "key" | .Into .Entity.Key }},
-
- e.{{ .Ident "group" | .Into .Entity.Group }},
- e.{{ .Ident "group_version" | .Into .Entity.GroupVersion }},
- e.{{ .Ident "resource" | .Into .Entity.Resource }},
- e.{{ .Ident "namespace" | .Into .Entity.Namespace }},
- e.{{ .Ident "name" | .Into .Entity.Name }},
-
- e.{{ .Ident "folder" | .Into .Entity.Folder }},
-
- e.{{ .Ident "meta" | .Into .Entity.Meta }},
- e.{{ .Ident "body" | .Into .Entity.Body }},
- e.{{ .Ident "status" | .Into .Entity.Status }},
-
- e.{{ .Ident "size" | .Into .Entity.Size }},
- e.{{ .Ident "etag" | .Into .Entity.ETag }},
-
- e.{{ .Ident "created_at" | .Into .Entity.CreatedAt }},
- e.{{ .Ident "created_by" | .Into .Entity.CreatedBy }},
- e.{{ .Ident "updated_at" | .Into .Entity.UpdatedAt }},
- e.{{ .Ident "updated_by" | .Into .Entity.UpdatedBy }},
-
- e.{{ .Ident "origin" | .Into .Entity.Origin.Source }},
- e.{{ .Ident "origin_key" | .Into .Entity.Origin.Key }},
- e.{{ .Ident "origin_ts" | .Into .Entity.Origin.Time }},
-
- e.{{ .Ident "title" | .Into .Entity.Title }},
- e.{{ .Ident "slug" | .Into .Entity.Slug }},
- e.{{ .Ident "description" | .Into .Entity.Description }},
-
- e.{{ .Ident "message" | .Into .Entity.Message }},
- e.{{ .Ident "labels" | .Into .Entity.Labels }},
- e.{{ .Ident "fields" | .Into .Entity.Fields }},
- e.{{ .Ident "errors" | .Into .Entity.Errors }},
-
- e.{{ .Ident "action" | .Into .Entity.Action }}
-{{ end }}
-
-{{/* Build an ORDER BY clause from a []SortBy contained in a .Sort field */}}
-{{ define "common_order_by" }}
- {{ $comma := listSep ", " }}
- {{ range .Sort }}
- {{- call $comma -}} {{ $.Ident .Field }} {{ .Direction.String }}
- {{ end }}
-{{ end }}
diff --git a/pkg/storage/unified/sqlnext/data/resource_get.sql b/pkg/storage/unified/sqlnext/data/resource_get.sql
deleted file mode 100644
index 6da2184d37e..00000000000
--- a/pkg/storage/unified/sqlnext/data/resource_get.sql
+++ /dev/null
@@ -1,34 +0,0 @@
-SELECT
- {{ .Ident "rv" | .Into .Resource.Version }},
- {{ .Ident "value" | .Into .Resource.Value }},
- {{ .Ident "blob" | .Into .Resource.Blob }},
-
-FROM "resource"
-
- WHERE 1 = 1
- AND {{ .Ident "namespace" }} = {{ .Arg .Key.Namespace }}
- AND {{ .Ident "group" }} = {{ .Arg .Key.Group }}
- AND {{ .Ident "resource" }} = {{ .Arg .Key.Resource }}
- AND {{ .Ident "name" }} = {{ .Arg .Key.Name }}
-
- {{/*
- Resource versions work like snapshots at the kind level. Thus, a request
- to retrieve a specific resource version should be interpreted as asking
- for a resource as of how it existed at that point in time. This is why we
- request matching entities with at most the provided resource version, and
- return only the one with the highest resource version. In the case of not
- specifying a resource version (i.e. resource version zero), it is
- interpreted as the latest version of the given entity, thus we instead
- query the "entity" table (which holds only the latest version of
- non-deleted entities) and we don't need to specify anything else. The
- "entity" table has a unique constraint on (namespace, group, resource,
- name), so we're guaranteed to have at most one matching row.
- */}}
- {{ if gt .ResourceVersion 0 }}
- AND {{ .Ident "rv" }} <= {{ .Arg .ResourceVersion }}
- ORDER BY {{ .Ident "rv" }} DESC
- LIMIT 1
- {{ else }}
- AND {{ .Ident "is_current" }} = true
- {{ end }}
-;
\ No newline at end of file
diff --git a/pkg/storage/unified/sqlnext/data/resource_insert.sql b/pkg/storage/unified/sqlnext/data/resource_insert.sql
deleted file mode 100644
index 5497041466e..00000000000
--- a/pkg/storage/unified/sqlnext/data/resource_insert.sql
+++ /dev/null
@@ -1,31 +0,0 @@
-INSERT INTO "resource"
- {{/* Explicitly specify fields that will be set */}}
- (
- {{ .Ident "event" }},
- {{ .Ident "group" }},
- {{ .Ident "api_version" }},
- {{ .Ident "namespace" }},
- {{ .Ident "resource" }},
- {{ .Ident "name" }},
- {{ .Ident "operation" }},
- {{ .Ident "message" }},
- {{ .Ident "value" }},
- {{ .Ident "hash" }},
- {{ .Ident "blob" }},
- )
-
- {{/* Provide the values */}}
- VALUES (
- {{ .Arg .Event.ID }},
- {{ .Arg .Event.Group }},
- {{ .Arg .Event.ApiVersion }},
- {{ .Arg .Event.Namespace }},
- {{ .Arg .Event.Resource }},
- {{ .Arg .Event.Name }},
- {{ .Arg .Event.Operation }},
- {{ .Arg .Event.Message }},
- {{ .Arg .Event.Value }},
- {{ .Arg .Event.Hash }},
- {{ .Arg .Event.Blob }},
- )
-;
diff --git a/pkg/storage/unified/sqlnext/data/rv_get.sql b/pkg/storage/unified/sqlnext/data/rv_get.sql
deleted file mode 100644
index 2a49c64bb05..00000000000
--- a/pkg/storage/unified/sqlnext/data/rv_get.sql
+++ /dev/null
@@ -1,8 +0,0 @@
-SELECT
- {{ .Ident "rv" | .Into .ResourceVersion }}
-
- FROM {{ .Ident "resource_version" }}
- WHERE 1 = 1
- AND {{ .Ident "group" }} = {{ .Arg .Group }}
- AND {{ .Ident "resource" }} = {{ .Arg .Resource }}
-;
diff --git a/pkg/storage/unified/sqlnext/data/rv_inc.sql b/pkg/storage/unified/sqlnext/data/rv_inc.sql
deleted file mode 100644
index da824f71323..00000000000
--- a/pkg/storage/unified/sqlnext/data/rv_inc.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-UPDATE {{ .Ident "resource_version" }}
- SET
- {{ .Ident "rv" }} = {{ .Arg .ResourceVersion }} + 1,
-
- WHERE 1 = 1
- AND {{ .Ident "group" }} = {{ .Arg .Group }}
- AND {{ .Ident "resource" }} = {{ .Arg .Resource }}
- AND {{ .Ident "rv" }} = {{ .Arg .ResourceVersion }}
-;
diff --git a/pkg/storage/unified/sqlnext/data/rv_insert.sql b/pkg/storage/unified/sqlnext/data/rv_insert.sql
deleted file mode 100644
index 88d3cc62866..00000000000
--- a/pkg/storage/unified/sqlnext/data/rv_insert.sql
+++ /dev/null
@@ -1,13 +0,0 @@
-INSERT INTO {{ .Ident "resource_version" }}
- (
- {{ .Ident "group" }},
- {{ .Ident "resource" }},
- {{ .Ident "rv" }},
- )
-
- VALUES (
- {{ .Arg .Group }},
- {{ .Arg .Resource }},
- 1,
- )
-;
diff --git a/pkg/storage/unified/sqlnext/data/rv_lock.sql b/pkg/storage/unified/sqlnext/data/rv_lock.sql
deleted file mode 100644
index 773a9208e0e..00000000000
--- a/pkg/storage/unified/sqlnext/data/rv_lock.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-SELECT {{ .Ident "rv" | .Into .ResourceVersion }}
- FROM {{ .Ident "resource_version" }}
- WHERE 1 = 1
- AND {{ .Ident "group" }} = {{ .Arg .Group }}
- AND {{ .Ident "resource" }} = {{ .Arg .Resource }}
- {{ .SelectFor "UPDATE" }}
-;
diff --git a/pkg/storage/unified/sqlnext/resource_mig.go b/pkg/storage/unified/sqlnext/resource_mig.go
deleted file mode 100644
index 8a9d1d776c0..00000000000
--- a/pkg/storage/unified/sqlnext/resource_mig.go
+++ /dev/null
@@ -1,161 +0,0 @@
-package sqlnext
-
-import (
- "fmt"
-
- "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
-)
-
-func InitResourceTables(mg *migrator.Migrator) string {
- marker := "Initialize resource tables (vX)" // changing this key wipe+rewrite everything
- mg.AddMigration(marker, &migrator.RawSQLMigration{})
-
- tables := []migrator.Table{}
-
- // This table helps support incrementing the resource version within a group+resource
- tables = append(tables, migrator.Table{
- Name: "resource_version",
- Columns: []*migrator.Column{
- {Name: "group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
- {Name: "resource", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
- {Name: "rv", Type: migrator.DB_BigInt, Nullable: false}, // resource version
- },
- Indices: []*migrator.Index{
- {Cols: []string{"group", "resource"}, Type: migrator.UniqueIndex},
- },
- })
-
- tables = append(tables, migrator.Table{
- Name: "resource", // write only log? all events
- Columns: []*migrator.Column{
- // SnowflakeID -- Each Create/Update/Delete call is an event
- // Using snowflake ID doubles this field as an approximate timestamp
- {Name: "event", Type: migrator.DB_BigInt, Nullable: false, IsPrimaryKey: true},
-
- // This will be null on insert, and then updated once we are ready to commit the transaction
- {Name: "rv", Type: migrator.DB_BigInt, Nullable: true},
- {Name: "previous_rv", Type: migrator.DB_BigInt, Nullable: true}, // needed?
-
- // Allows fast search for the first page in any query.
- // Subsequent pages must use MAX(rv) AND is_compacted=false GROUP ...
- {Name: "is_current", Type: migrator.DB_Bool, Nullable: false},
-
- // Indicates that this is no longer the current version
- // This value is updated every few minutes and makes the paged queries more efficient
- {Name: "is_compacted", Type: migrator.DB_Bool, Nullable: false},
-
- // Properties that exist in path/key (and duplicated in the json value)
- {Name: "group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
- {Name: "api_version", Type: migrator.DB_NVarchar, Length: 32, Nullable: false},
- {Name: "namespace", Type: migrator.DB_NVarchar, Length: 63, Nullable: true}, // namespace is not required (cluster scope)
- {Name: "resource", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
- {Name: "name", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
-
- // The operation that wrote this resource version
- // 1: created, 2: updated, 3: deleted
- {Name: "operation", Type: migrator.DB_Int, Nullable: false},
-
- // Optional Commit message (currently only used for dashboards)
- {Name: "message", Type: migrator.DB_Text, Nullable: false}, // defaults to empty string
-
- // The k8s resource JSON text (without the resourceVersion populated)
- {Name: "value", Type: migrator.DB_MediumText, Nullable: false},
-
- // Content hash -- this is appropriate to use for an etag value
- {Name: "hash", Type: migrator.DB_NVarchar, Length: 32, Nullable: false},
-
- // Path to linked blob (or null). This blob may be saved in SQL, or in an object store
- {Name: "blob_uid", Type: migrator.DB_NVarchar, Length: 60, Nullable: true},
- },
- Indices: []*migrator.Index{
- {Cols: []string{"rv"}, Type: migrator.UniqueIndex},
- {Cols: []string{"is_current"}, Type: migrator.IndexType},
- {Cols: []string{"is_compacted"}, Type: migrator.IndexType},
- {Cols: []string{"operation"}, Type: migrator.IndexType},
- {Cols: []string{"namespace"}, Type: migrator.IndexType},
- {Cols: []string{"group", "resource", "name"}, Type: migrator.IndexType},
- {Cols: []string{"blob_uid"}, Type: migrator.IndexType},
- },
- })
-
- // The values in this table are created by parsing the the value JSON and writing these as searchable columns
- // These *could* be in the same table, but this structure allows us to replace the table by first
- // building a parallel structure, then swapping them... maybe :)
- tables = append(tables, migrator.Table{
- Name: "resource_meta", // write only log? all events
- Columns: []*migrator.Column{
- {Name: "event", Type: migrator.DB_BigInt, Nullable: false, IsPrimaryKey: true},
-
- // Hashed label set
- {Name: "label_set", Type: migrator.DB_NVarchar, Length: 64, Nullable: true}, // null is no labels
-
- // Helpful filters
- {Name: "folder", Type: migrator.DB_NVarchar, Length: 190, Nullable: true}, // uid of folder
-
- // For sorting values come from metadata.annotations#grafana.app/*
- {Name: "created_at", Type: migrator.DB_BigInt, Nullable: false},
- {Name: "updated_at", Type: migrator.DB_BigInt, Nullable: false},
-
- // Origin metadata helps implement efficient provisioning checks
- {Name: "origin", Type: migrator.DB_NVarchar, Length: 64, Nullable: true}, // The origin name
- {Name: "origin_path", Type: migrator.DB_Text, Nullable: true}, // Path to resource
- {Name: "origin_hash", Type: migrator.DB_NVarchar, Length: 128, Nullable: true}, // Origin hash
- {Name: "origin_ts", Type: migrator.DB_BigInt, Nullable: true}, // Origin timestamp
- },
- Indices: []*migrator.Index{
- {Cols: []string{"event"}, Type: migrator.IndexType},
- {Cols: []string{"folder"}, Type: migrator.IndexType},
- {Cols: []string{"created_at"}, Type: migrator.IndexType},
- {Cols: []string{"updated_at"}, Type: migrator.IndexType},
- {Cols: []string{"origin"}, Type: migrator.IndexType},
- },
- })
-
- // This table is optional, blobs can also be saved to object store or disk
- // This is an append only store
- tables = append(tables, migrator.Table{
- Name: "resource_blob", // even things that failed?
- Columns: []*migrator.Column{
- {Name: "uid", Type: migrator.DB_NVarchar, Length: 60, Nullable: false, IsPrimaryKey: true},
- {Name: "value", Type: migrator.DB_Blob, Nullable: true},
- {Name: "etag", Type: migrator.DB_NVarchar, Length: 64, Nullable: false},
- {Name: "size", Type: migrator.DB_BigInt, Nullable: false},
- {Name: "content_type", Type: migrator.DB_NVarchar, Length: 255, Nullable: false},
-
- // These is used for auditing and cleanup (could be path?)
- {Name: "namespace", Type: migrator.DB_NVarchar, Length: 63, Nullable: true},
- {Name: "group", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
- {Name: "resource", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
- {Name: "name", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
- },
- Indices: []*migrator.Index{
- {Cols: []string{"uid"}, Type: migrator.UniqueIndex},
-
- // Used for auditing
- {Cols: []string{"namespace", "group", "resource", "name"}, Type: migrator.IndexType},
- },
- })
-
- tables = append(tables, migrator.Table{
- Name: "resource_label_set",
- Columns: []*migrator.Column{
- {Name: "label_set", Type: migrator.DB_NVarchar, Length: 64, Nullable: false},
- {Name: "label", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
- {Name: "value", Type: migrator.DB_Text, Nullable: false},
- },
- Indices: []*migrator.Index{
- {Cols: []string{"label_set", "label"}, Type: migrator.UniqueIndex},
- },
- })
-
- // Initialize all tables
- for t := range tables {
- mg.AddMigration("drop table "+tables[t].Name, migrator.NewDropTableMigration(tables[t].Name))
- mg.AddMigration("create table "+tables[t].Name, migrator.NewAddTableMigration(tables[t]))
- for i := range tables[t].Indices {
- mg.AddMigration(fmt.Sprintf("create table %s, index: %d", tables[t].Name, i), migrator.NewAddIndexMigration(tables[t], tables[t].Indices[i]))
- }
- }
-
- return marker
-}
diff --git a/pkg/storage/unified/sqlnext/sql_resources.go b/pkg/storage/unified/sqlnext/sql_resources.go
deleted file mode 100644
index 93484ed34d6..00000000000
--- a/pkg/storage/unified/sqlnext/sql_resources.go
+++ /dev/null
@@ -1,160 +0,0 @@
-package sqlnext
-
-import (
- "context"
- "errors"
- "fmt"
- "strings"
-
- "github.com/prometheus/client_golang/prometheus"
- "go.opentelemetry.io/otel/trace"
-
- "github.com/grafana/grafana/pkg/infra/log"
- "github.com/grafana/grafana/pkg/infra/tracing"
- "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
- "github.com/grafana/grafana/pkg/services/sqlstore/session"
- "github.com/grafana/grafana/pkg/services/store/entity/db"
- "github.com/grafana/grafana/pkg/services/store/entity/sqlstash"
- "github.com/grafana/grafana/pkg/storage/unified/resource"
- "github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
-)
-
-// Package-level errors.
-var (
- ErrNotImplementedYet = errors.New("not implemented yet (sqlnext)")
-)
-
-func ProvideSQLResourceServer(db db.EntityDBInterface, tracer tracing.Tracer) (resource.ResourceServer, error) {
- ctx, cancel := context.WithCancel(context.Background())
-
- store := &sqlResourceStore{
- db: db,
- log: log.New("sql-resource-server"),
- ctx: ctx,
- cancel: cancel,
- tracer: tracer,
- }
-
- if err := prometheus.Register(sqlstash.NewStorageMetrics()); err != nil {
- return nil, err
- }
-
- return resource.NewResourceServer(resource.ResourceServerOptions{
- Tracer: tracer,
- Backend: store,
- Diagnostics: store,
- Lifecycle: store,
- })
-}
-
-type sqlResourceStore struct {
- log log.Logger
- db db.EntityDBInterface // needed to keep xorm engine in scope
- sess *session.SessionDB
- dialect migrator.Dialect
- ctx context.Context // TODO: remove
- cancel context.CancelFunc
- tracer trace.Tracer
-
- //broadcaster sqlstash.Broadcaster[*resource.WatchEvent]
- //stream chan *resource.WatchEvent
-
- sqlDB db.DB
- sqlDialect sqltemplate.Dialect
-}
-
-func (s *sqlResourceStore) Init() error {
- if s.sess != nil {
- return nil
- }
-
- if s.db == nil {
- return errors.New("missing db")
- }
-
- err := s.db.Init()
- if err != nil {
- return err
- }
-
- sqlDB, err := s.db.GetDB()
- if err != nil {
- return err
- }
- s.sqlDB = sqlDB
-
- driverName := sqlDB.DriverName()
- driverName = strings.TrimSuffix(driverName, "WithHooks")
- switch driverName {
- case db.DriverMySQL:
- s.sqlDialect = sqltemplate.MySQL
- case db.DriverPostgres:
- s.sqlDialect = sqltemplate.PostgreSQL
- case db.DriverSQLite, db.DriverSQLite3:
- s.sqlDialect = sqltemplate.SQLite
- default:
- return fmt.Errorf("no dialect for driver %q", driverName)
- }
-
- sess, err := s.db.GetSession()
- if err != nil {
- return err
- }
-
- engine, err := s.db.GetEngine()
- if err != nil {
- return err
- }
-
- s.sess = sess
- s.dialect = migrator.NewDialect(engine.DriverName())
-
- // TODO.... set up the broadcaster
-
- return nil
-}
-
-func (s *sqlResourceStore) IsHealthy(ctx context.Context, r *resource.HealthCheckRequest) (*resource.HealthCheckResponse, error) {
- // ctxLogger := s.log.FromContext(log.WithContextualAttributes(ctx, []any{"method", "isHealthy"}))
-
- if err := s.sqlDB.PingContext(ctx); err != nil {
- return nil, err
- }
- // TODO: check the status of the watcher implementation as well
- return &resource.HealthCheckResponse{Status: resource.HealthCheckResponse_SERVING}, nil
-}
-
-func (s *sqlResourceStore) Stop() {
- s.cancel()
-}
-
-func (s *sqlResourceStore) WriteEvent(ctx context.Context, event resource.WriteEvent) (int64, error) {
- _, span := s.tracer.Start(ctx, "storage_server.WriteEvent")
- defer span.End()
-
- // TODO... actually write write the event!
-
- return 0, ErrNotImplementedYet
-}
-
-func (s *sqlResourceStore) WatchWriteEvents(ctx context.Context) (<-chan *resource.WrittenEvent, error) {
- return nil, ErrNotImplementedYet
-}
-
-func (s *sqlResourceStore) Read(ctx context.Context, req *resource.ReadRequest) (*resource.ReadResponse, error) {
- _, span := s.tracer.Start(ctx, "storage_server.GetResource")
- defer span.End()
-
- fmt.Printf("TODO, GET: %+v", req.Key)
-
- return nil, ErrNotImplementedYet
-}
-
-func (s *sqlResourceStore) PrepareList(ctx context.Context, req *resource.ListRequest) (*resource.ListResponse, error) {
- _, span := s.tracer.Start(ctx, "storage_server.List")
- defer span.End()
-
- fmt.Printf("TODO, LIST: %+v", req.Options.Key)
-
- return nil, ErrNotImplementedYet
-}
diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx
index ca58940a152..6de0eb2d84f 100644
--- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx
+++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx
@@ -1,16 +1,19 @@
import { css } from '@emotion/css';
import { DOMAttributes } from '@react-types/shared';
-import { memo, forwardRef } from 'react';
+import { memo, forwardRef, useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
+import { config, reportInteraction } from '@grafana/runtime';
import { CustomScrollbar, Icon, IconButton, useStyles2, Stack } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { t } from 'app/core/internationalization';
+import { usePatchUserPreferencesMutation } from 'app/features/preferences/api/index';
import { useSelector } from 'app/types';
import { MegaMenuItem } from './MegaMenuItem';
+import { usePinnedItems } from './hooks';
import { enrichWithInteractionTracking, getActiveItem } from './utils';
export const MENU_WIDTH = '300px';
@@ -26,6 +29,8 @@ export const MegaMenu = memo(
const location = useLocation();
const { chrome } = useGrafana();
const state = chrome.useState();
+ const [patchPreferences] = usePatchUserPreferencesMutation();
+ const pinnedItems = usePinnedItems();
// Remove profile + help from tree
const navItems = navTree
@@ -46,6 +51,35 @@ export const MegaMenu = memo(
});
};
+ const isPinned = useCallback(
+ (id?: string) => {
+ if (!id || !pinnedItems?.length) {
+ return false;
+ }
+ return pinnedItems?.includes(id);
+ },
+ [pinnedItems]
+ );
+
+ const onPinItem = (id?: string) => {
+ if (id && config.featureToggles.pinNavItems) {
+ const navItem = navTree.find((item) => item.id === id);
+ const isSaved = isPinned(id);
+ const newItems = isSaved ? pinnedItems.filter((i) => id !== i) : [...pinnedItems, id];
+ const interactionName = isSaved ? 'grafana_nav_item_unpinned' : 'grafana_nav_item_pinned';
+ reportInteraction(interactionName, {
+ path: navItem?.url ?? id,
+ });
+ patchPreferences({
+ patchPrefsCmd: {
+ navbar: {
+ savedItemIds: newItems,
+ },
+ },
+ });
+ }
+ };
+
return (
@@ -79,8 +113,10 @@ export const MegaMenu = memo(
)}
))}
diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx
index aee471e961d..ee5d85cc97c 100644
--- a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx
+++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx
@@ -19,11 +19,13 @@ interface Props {
activeItem?: NavModelItem;
onClick?: () => void;
level?: number;
+ onPin: (id?: string) => void;
+ isPinned: (id?: string) => boolean;
}
const MAX_DEPTH = 2;
-export function MegaMenuItem({ link, activeItem, level = 0, onClick }: Props) {
+export function MegaMenuItem({ link, activeItem, level = 0, onClick, onPin, isPinned }: Props) {
const { chrome } = useGrafana();
const state = chrome.useState();
const menuIsDocked = state.megaMenuDocked;
@@ -102,6 +104,9 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick }: Props) {
}}
target={link.target}
url={link.url}
+ id={link.id}
+ onPin={onPin}
+ isPinned={isPinned(link.id)}
>
))
) : (
diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx
index c6d15f4ab95..573bf0190c0 100644
--- a/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx
+++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuItemText.tsx
@@ -3,6 +3,7 @@ import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
+import { config } from '@grafana/runtime';
import { Icon, Link, useTheme2 } from '@grafana/ui';
export interface Props {
@@ -11,9 +12,12 @@ export interface Props {
onClick?: () => void;
target?: HTMLAnchorElement['target'];
url: string;
+ id?: string;
+ onPin: (id?: string) => void;
+ isPinned?: boolean;
}
-export function MegaMenuItemText({ children, isActive, onClick, target, url }: Props) {
+export function MegaMenuItemText({ children, isActive, onClick, target, url, id, onPin, isPinned }: Props) {
const theme = useTheme2();
const styles = getStyles(theme, isActive);
const LinkComponent = !target && url.startsWith('/') ? Link : 'a';
@@ -26,6 +30,17 @@ export function MegaMenuItemText({ children, isActive, onClick, target, url }: P
// As nav links are supposed to link to internal urls this option should be used with caution
target === '_blank' &&
}
+ {config.featureToggles.pinNavItems && (
+
{
+ e.preventDefault();
+ e.stopPropagation();
+ onPin(id);
+ }}
+ />
+ )}
);
@@ -90,5 +105,17 @@ const getStyles = (theme: GrafanaTheme2, isActive: Props['isActive']) => ({
gap: '0.5rem',
height: '100%',
width: '100%',
+ justifyContent: 'space-between',
+ '.pin-icon': {
+ display: 'none',
+ padding: theme.spacing(0.5),
+ width: theme.spacing(3),
+ height: theme.spacing(3),
+ },
+ '&:hover': {
+ '.pin-icon': {
+ display: 'block',
+ },
+ },
}),
});
diff --git a/public/app/core/components/AppChrome/MegaMenu/hooks.ts b/public/app/core/components/AppChrome/MegaMenu/hooks.ts
new file mode 100644
index 00000000000..0334df81fd0
--- /dev/null
+++ b/public/app/core/components/AppChrome/MegaMenu/hooks.ts
@@ -0,0 +1,14 @@
+import { useMemo } from 'react';
+
+import { config } from '@grafana/runtime';
+import { useGetUserPreferencesQuery } from 'app/features/preferences/api';
+
+export const usePinnedItems = () => {
+ const preferences = useGetUserPreferencesQuery();
+ const pinnedItems = useMemo(() => preferences.data?.navbar?.savedItemIds || [], [preferences]);
+
+ if (config.featureToggles.pinNavItems) {
+ return pinnedItems;
+ }
+ return [];
+};
diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx
index 6f6a667365a..cbe06efd0aa 100644
--- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx
+++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { MouseEvent } from 'react';
import { selectors } from '@grafana/e2e-selectors';
-import { Button, CallToActionCard, Icon, IconName, LinkButton } from '@grafana/ui';
+import { Alert, Button, CallToActionCard, Icon, IconName, LinkButton } from '@grafana/ui';
export interface Props {
title: string;
@@ -59,10 +59,9 @@ const EmptyListCTA = ({
''
)}
{infoBox ? (
-
- {infoBoxTitle &&
{infoBoxTitle} }
+
-
+
) : (
''
)}
diff --git a/public/app/core/components/TagFilter/TagBadge.tsx b/public/app/core/components/TagFilter/TagBadge.tsx
index c685d7c66e4..d5cdce494b5 100644
--- a/public/app/core/components/TagFilter/TagBadge.tsx
+++ b/public/app/core/components/TagFilter/TagBadge.tsx
@@ -1,6 +1,8 @@
+import { css } from '@emotion/css';
import * as React from 'react';
-import { getTagColorsFromName, Icon } from '@grafana/ui';
+import { GrafanaTheme2 } from '@grafana/data';
+import { getTagColorsFromName, Icon, useStyles2 } from '@grafana/ui';
export interface Props {
label: string;
@@ -9,26 +11,39 @@ export interface Props {
onClick?: React.MouseEventHandler
;
}
-export class TagBadge extends React.Component {
- constructor(props: Props) {
- super(props);
- }
+export const TagBadge = ({ count, label, onClick, removeIcon }: Props) => {
+ const { color } = getTagColorsFromName(label);
+ const styles = useStyles2(getStyles);
- render() {
- const { label, removeIcon, count, onClick } = this.props;
- const { color } = getTagColorsFromName(label);
+ const countLabel = count !== 0 && {`(${count})`} ;
- const tagStyle = {
- backgroundColor: color,
- };
+ return (
+
+ {removeIcon && }
+ {label} {countLabel}
+
+ );
+};
- const countLabel = count !== 0 && {`(${count})`} ;
-
- return (
-
- {removeIcon && }
- {label} {countLabel}
-
- );
- }
-}
+export const getStyles = (theme: GrafanaTheme2) => ({
+ badge: css({
+ ...theme.typography.bodySmall,
+ backgroundColor: theme.v1.palette.gray1,
+ borderRadius: theme.shape.radius.default,
+ color: theme.v1.palette.white,
+ display: 'inline-block',
+ height: '20px',
+ lineHeight: '20px',
+ padding: theme.spacing(0, 0.75),
+ verticalAlign: 'baseline',
+ whiteSpace: 'nowrap',
+ '&:hover': {
+ opacity: 0.85,
+ },
+ }),
+});
diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx
index 98081dd6717..26732fc4eb4 100644
--- a/public/app/core/components/TagFilter/TagFilter.tsx
+++ b/public/app/core/components/TagFilter/TagFilter.tsx
@@ -6,7 +6,7 @@ import { escapeStringForRegex, GrafanaTheme2 } from '@grafana/data';
import { Icon, MultiSelect, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
-import { TagBadge } from './TagBadge';
+import { TagBadge, getStyles as getTagBadgeStyles } from './TagBadge';
import { TagOption, TagSelectOption } from './TagOption';
export interface TermCount {
@@ -167,31 +167,35 @@ export const TagFilter = ({
TagFilter.displayName = 'TagFilter';
-const getStyles = (theme: GrafanaTheme2) => ({
- tagFilter: css`
- position: relative;
- min-width: 180px;
- flex-grow: 1;
+const getStyles = (theme: GrafanaTheme2) => {
+ const tagBadgeStyles = getTagBadgeStyles(theme);
- .label-tag {
- margin-left: 6px;
- cursor: pointer;
- }
- `,
- clear: css`
- background: none;
- border: none;
- text-decoration: underline;
- font-size: 12px;
- padding: none;
- position: absolute;
- top: -17px;
- right: 0;
- cursor: pointer;
- color: ${theme.colors.text.secondary};
+ return {
+ tagFilter: css({
+ position: 'relative',
+ minWidth: '180px',
+ flexGrow: 1,
- &:hover {
- color: ${theme.colors.text.primary};
- }
- `,
-});
+ [`.${tagBadgeStyles.badge}`]: {
+ marginLeft: '6px',
+ cursor: 'pointer',
+ },
+ }),
+ clear: css({
+ background: 'none',
+ border: 'none',
+ textDecoration: 'underline',
+ fontSize: '12px',
+ padding: 'none',
+ position: 'absolute',
+ top: '-17px',
+ right: 0,
+ cursor: 'pointer',
+ color: theme.colors.text.secondary,
+
+ '&:hover': {
+ color: theme.colors.text.primary,
+ },
+ }),
+ };
+};
diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts
index ec33626bc43..73b73b52d4d 100644
--- a/public/app/core/reducers/root.ts
+++ b/public/app/core/reducers/root.ts
@@ -28,6 +28,7 @@ import usersReducers from 'app/features/users/state/reducers';
import templatingReducers from 'app/features/variables/state/keyedVariablesReducer';
import { alertingApi } from '../../features/alerting/unified/api/alertingApi';
+import { userPreferencesAPI } from '../../features/preferences/api';
import { queryLibraryApi } from '../../features/query-library/api/factory';
import { cleanUpAction } from '../actions/cleanUp';
@@ -59,6 +60,7 @@ const rootReducers = {
[browseDashboardsAPI.reducerPath]: browseDashboardsAPI.reducer,
[cloudMigrationAPI.reducerPath]: cloudMigrationAPI.reducer,
[queryLibraryApi.reducerPath]: queryLibraryApi.reducer,
+ [userPreferencesAPI.reducerPath]: userPreferencesAPI.reducer,
};
const addedReducers = {};
diff --git a/public/app/core/utils/fetch.ts b/public/app/core/utils/fetch.ts
index 49253f74a31..8b4031123b7 100644
--- a/public/app/core/utils/fetch.ts
+++ b/public/app/core/utils/fetch.ts
@@ -1,6 +1,6 @@
import { omitBy } from 'lodash';
-import { deprecationWarning } from '@grafana/data';
+import { deprecationWarning, safeStringifyValue } from '@grafana/data';
import { BackendSrvRequest } from '@grafana/runtime';
export const parseInitFromOptions = (options: BackendSrvRequest): RequestInit => {
@@ -93,7 +93,7 @@ export const parseBody = (options: BackendSrvRequest, isAppJson: boolean) => {
return options.data;
}
- return isAppJson ? JSON.stringify(options.data) : new URLSearchParams(options.data);
+ return isAppJson ? safeStringifyValue(options.data) : new URLSearchParams(options.data);
};
export async function parseResponseBody(
diff --git a/public/app/features/admin/AdminSettings.tsx b/public/app/features/admin/AdminSettings.tsx
index fd9a4abf5a9..ddad7b7993a 100644
--- a/public/app/features/admin/AdminSettings.tsx
+++ b/public/app/features/admin/AdminSettings.tsx
@@ -1,6 +1,7 @@
import { useAsync } from 'react-use';
import { getBackendSrv } from '@grafana/runtime';
+import { Alert } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { AdminSettingsTable } from './AdminSettingsTable';
@@ -13,10 +14,10 @@ function AdminSettings() {
return (
-
+
These system settings are defined in grafana.ini or custom.ini (or overridden in ENV variables). To change
these you currently need to restart Grafana.
-
+
{loading && }
diff --git a/public/app/features/admin/UserLdapSyncInfo.tsx b/public/app/features/admin/UserLdapSyncInfo.tsx
index df2f270c620..f76ca72a5cd 100644
--- a/public/app/features/admin/UserLdapSyncInfo.tsx
+++ b/public/app/features/admin/UserLdapSyncInfo.tsx
@@ -5,6 +5,8 @@ import { Button, LinkButton } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { AccessControlAction, SyncInfo, UserDTO } from 'app/types';
+import { TagBadge } from '../../core/components/TagFilter/TagBadge';
+
interface Props {
ldapSyncInfo: SyncInfo;
user: UserDTO;
@@ -40,7 +42,7 @@ export class UserLdapSyncInfo extends PureComponent {
External sync
User synced via LDAP. Some changes must be done in LDAP or mappings.
- LDAP
+
diff --git a/public/app/features/alerting/unified/AlertsFolderView.tsx b/public/app/features/alerting/unified/AlertsFolderView.tsx
index 1beb52abf25..bfebca0c03a 100644
--- a/public/app/features/alerting/unified/AlertsFolderView.tsx
+++ b/public/app/features/alerting/unified/AlertsFolderView.tsx
@@ -14,8 +14,9 @@ import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces';
import { usePagination } from './hooks/usePagination';
import { useURLSearchParams } from './hooks/useURLSearchParams';
import { fetchPromRulesAction, fetchRulerRulesAction } from './state/actions';
-import { combineMatcherStrings, labelsMatchMatchers, parseMatchers } from './utils/alertmanager';
+import { combineMatcherStrings, labelsMatchMatchers } from './utils/alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
+import { parsePromQLStyleMatcherLooseSafe } from './utils/matchers';
import { createViewLink } from './utils/misc';
interface Props {
@@ -168,7 +169,7 @@ function filterAndSortRules(
labelFilter: string,
sortOrder: SortOrder
) {
- const matchers = parseMatchers(labelFilter);
+ const matchers = parsePromQLStyleMatcherLooseSafe(labelFilter);
let rules = originalRules.filter(
(rule) => rule.name.toLowerCase().includes(nameFilter.toLowerCase()) && labelsMatchMatchers(rule.labels, matchers)
);
diff --git a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx
index aaa604e5fb3..71d76da285e 100644
--- a/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx
+++ b/public/app/features/alerting/unified/PanelAlertTabContent.test.tsx
@@ -1,6 +1,6 @@
import { render } from '@testing-library/react';
import { TestProvider } from 'test/helpers/TestProvider';
-import { byTestId } from 'testing-library-selector';
+import { byTestId, byText } from 'testing-library-selector';
import { DataSourceApi } from '@grafana/data';
import { PromOptions, PrometheusDatasource } from '@grafana/prometheus';
@@ -25,6 +25,7 @@ import {
mockRulerAlertingRule,
mockRulerRuleGroup,
} from './mocks';
+import { captureRequests } from './mocks/server/events';
import { RuleFormValues } from './types/rule-form';
import * as config from './utils/config';
import { Annotation } from './utils/constants';
@@ -183,6 +184,7 @@ const panel = new PanelModel({
const ui = {
row: byTestId('row'),
createButton: byTestId('create-alert-rule-button'),
+ notSavedYet: byText('Dashboard not saved'),
};
const server = setupMswServer();
@@ -281,6 +283,29 @@ describe('PanelAlertTabContent', () => {
});
});
+ it('should not make requests for unsaved dashboard', async () => {
+ const capture = captureRequests();
+
+ const unsavedDashboard = {
+ ...dashboard,
+ uid: null,
+ } as DashboardModel;
+
+ renderAlertTabContent(
+ unsavedDashboard,
+ new PanelModel({
+ ...panel,
+ datasource: undefined,
+ maxDataPoints: 100,
+ interval: '10s',
+ })
+ );
+
+ expect(await ui.notSavedYet.find()).toBeInTheDocument();
+ const requests = await capture;
+ expect(requests.length).toBe(0);
+ });
+
it('Will take into account datasource minInterval', async () => {
(getDatasourceSrv() as unknown as MockDataSourceSrv).datasources[dataSources.prometheus.uid].interval = '7m';
diff --git a/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx b/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx
index 8643ac99555..52b7ed0652b 100644
--- a/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx
+++ b/public/app/features/alerting/unified/components/alert-groups/MatcherFilter.tsx
@@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Field, Icon, Input, Label, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { logInfo, LogMessages } from '../../Analytics';
-import { parseMatchers } from '../../utils/alertmanager';
+import { parsePromQLStyleMatcherLoose } from '../../utils/matchers';
interface Props {
defaultQueryString?: string;
@@ -28,13 +28,22 @@ export const MatcherFilter = ({ onFilterChange, defaultQueryString }: Props) =>
);
const searchIcon = ;
- const inputInvalid = defaultQueryString ? parseMatchers(defaultQueryString).length === 0 : false;
+ let inputValid = Boolean(defaultQueryString && defaultQueryString.length >= 3);
+ try {
+ if (!defaultQueryString) {
+ inputValid = true;
+ } else {
+ parsePromQLStyleMatcherLoose(defaultQueryString);
+ }
+ } catch (err) {
+ inputValid = false;
+ }
return (
diff --git a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx
index 4c48f4c1a3a..5f75bfcde1c 100644
--- a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx
+++ b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx
@@ -87,7 +87,7 @@ export const AmRootRouteForm = ({
{
- const matchers = parseMatchers(queryString ?? '').map(matcherToObjectMatcher);
+ const matchers = parsePromQLStyleMatcherLooseSafe(queryString ?? '').map(matcherToObjectMatcher);
handleChangeLabels()(matchers);
}, [handleChangeLabels, queryString]);
@@ -50,7 +54,17 @@ const NotificationPoliciesFilter = ({
const selectedContactPoint = receiverOptions.find((option) => option.value === contactPoint) ?? null;
const hasFilters = queryString || contactPoint;
- const inputInvalid = queryString && queryString.length > 3 ? parseMatchers(queryString).length === 0 : false;
+
+ let inputValid = Boolean(queryString && queryString.length > 3);
+ try {
+ if (!queryString) {
+ inputValid = true;
+ } else {
+ parsePromQLStyleMatcherLoose(queryString);
+ }
+ } catch (err) {
+ inputValid = false;
+ }
return (
@@ -73,8 +87,8 @@ const NotificationPoliciesFilter = ({
}
- invalid={inputInvalid}
- error={inputInvalid ? 'Query must use valid matcher syntax' : null}
+ invalid={!inputValid}
+ error={!inputValid ? 'Query must use valid matcher syntax' : null}
>
<>
- Firing alert rule instances are routed to notification policies based on matching labels. All alert rules
- and instances, irrespective of their labels, match the default notification policy. If there are no nested
- policies, or no nested policies match the labels in the alert rule or alert instance, then the default
- notification policy is the matching policy.
+ Firing alert instances are routed to notification policies based on matching labels. The default
+ notification policy matches all alert instances.
>
-
-
- Read about notification routing.
-
-
<>
@@ -194,12 +183,12 @@ function NeedHelpInfoForNotificationPolicy() {
connect them to your notification policy by adding label matchers.
>
- Read about Labels and annotations.
+ Read about notification policies.
@@ -220,20 +209,18 @@ function NeedHelpInfoForContactpoint() {
Notifications for firing alert instances are grouped based on folder and alert rule name.
- The waiting time until the initial notification is sent for a new group created by an incoming alert is 30
- seconds.
+ The wait time before sending the first notification for a new group of alerts is 30 seconds.
- The waiting time to send a batch of new alerts for that group after the first notification was sent is 5
- minutes.
+ The waiting time before sending a notification about changes in the alert group after the first notification
+ has been sent is 5 minutes.
- The waiting time to resend an alert after they have successfully been sent is 4 hours.
+ The wait time before resending a notification that has already been sent successfully is 4 hours.
Grouping and wait time values are defined in your default notification policy.
>
}
- // todo: update the link with the new documentation about simplified routing
- externalLink="`https://grafana.com/docs/grafana/latest/alerting/fundamentals/notification-policies/notifications/`"
- linkText="Read more about notifiying contact points"
+ externalLink="https://grafana.com/docs/grafana/latest/alerting/fundamentals/notifications/"
+ linkText="Read more about notifications"
title="Notify contact points"
/>
);
diff --git a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx
index d8e1d43b7a9..7862535b4e7 100644
--- a/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/QueryRows.tsx
@@ -5,6 +5,7 @@ import { DragDropContext, Droppable, DropResult } from 'react-beautiful-dnd';
import {
DataQuery,
DataSourceInstanceSettings,
+ getDataSourceRef,
LoadingState,
PanelData,
rangeUtil,
@@ -226,10 +227,7 @@ function copyModel(item: AlertQuery, settings: DataSourceInstanceSettings): Omit
...item,
model: {
...omit(item.model, 'datasource'),
- datasource: {
- type: settings.type,
- uid: settings.uid,
- },
+ datasource: getDataSourceRef(settings),
},
datasourceUid: settings.uid,
};
@@ -244,10 +242,7 @@ function newModel(item: AlertQuery, settings: DataSourceInstanceSettings): Omit<
model: {
refId: item.refId,
hide: false,
- datasource: {
- type: settings.type,
- uid: settings.uid,
- },
+ datasource: getDataSourceRef(settings),
},
};
}
diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.tsx
index cfbc879842a..094c9277ecd 100644
--- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.tsx
+++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.tsx
@@ -68,7 +68,7 @@ export const RoutingSettings = ({ alertManager }: RoutingSettingsProps) => {
{overrideGrouping && (
;
diff --git a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx
index 0e635b2dbd7..b9deb610400 100644
--- a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx
+++ b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx
@@ -1,6 +1,6 @@
import { css } from '@emotion/css';
-import { GrafanaTheme2 } from '@grafana/data/src/themes';
+import { GrafanaTheme2 } from '@grafana/data';
import { CallToActionCard, useStyles2, Stack } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
diff --git a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx
index 80ae03c9e10..c9491eda390 100644
--- a/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx
+++ b/public/app/features/alerting/unified/components/rules/RuleDetailsMatchingInstances.tsx
@@ -10,13 +10,14 @@ import {
AlertInstanceStateFilter,
InstanceStateFilter,
} from 'app/features/alerting/unified/components/rules/AlertInstanceStateFilter';
-import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
+import { labelsMatchMatchers } from 'app/features/alerting/unified/utils/alertmanager';
import { createViewLink, sortAlerts } from 'app/features/alerting/unified/utils/misc';
import { SortOrder } from 'app/plugins/panel/alertlist/types';
import { Alert, CombinedRule, PaginationProps } from 'app/types/unified-alerting';
import { mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto';
import { GRAFANA_RULES_SOURCE_NAME, isGrafanaRulesSource } from '../../utils/datasource';
+import { parsePromQLStyleMatcherLooseSafe } from '../../utils/matchers';
import { isAlertingRule } from '../../utils/rules';
import { AlertInstancesTable } from './AlertInstancesTable';
@@ -148,7 +149,7 @@ function filterAlerts(
): Alert[] {
let filteredAlerts = [...alerts];
if (alertInstanceLabel) {
- const matchers = parseMatchers(alertInstanceLabel || '');
+ const matchers = alertInstanceLabel ? parsePromQLStyleMatcherLooseSafe(alertInstanceLabel) : [];
filteredAlerts = filteredAlerts.filter(({ labels }) => labelsMatchMatchers(labels, matchers));
}
if (alertInstanceState) {
diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx
index 6e52b27e1d2..9a1aa40e9cf 100644
--- a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx
+++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx
@@ -64,7 +64,12 @@ export const CentralAlertHistoryScene = () => {
new SceneTimePicker({}),
new SceneRefreshPicker({}),
],
- $timeRange: new SceneTimeRange({}), //needed for using the time range sync in the url
+ // use default time range as from 1 hour ago to now, as the limit of the history api is 5000 events,
+ // and using a wider time range might lead to showing gaps in the events list and the chart.
+ $timeRange: new SceneTimeRange({
+ from: 'now-1h',
+ to: 'now',
+ }),
$variables: new SceneVariableSet({
variables: [filterVariable],
}),
@@ -170,16 +175,16 @@ export const FilterInfo = () => {
-
+
Filter events using label querying without spaces, ex:
{`{severity="critical", instance=~"cluster-us-.+"}`}
- Invalid use of spaces:
- {`{severity= "critical"}`}
+ Invalid use of spaces:
+ {`{severity= "alerting.critical"}`}
{`{severity ="critical"}`}
- Valid use of spaces:
+ Valid use of spaces:
{`{severity=" critical"}`}
-
+
Filter alerts using label querying without braces, ex:
{`severity="critical", instance=~"cluster-us-.+"`}
diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/EventDetails.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/EventDetails.tsx
new file mode 100644
index 00000000000..3b42644ed13
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/central-state-history/EventDetails.tsx
@@ -0,0 +1,236 @@
+import { css } from '@emotion/css';
+import { max, min, uniqBy } from 'lodash';
+import { useMemo } from 'react';
+
+import { FieldType, GrafanaTheme2, LoadingState, PanelData, dateTime, makeTimeRange } from '@grafana/data';
+import { Icon, Stack, Text, useStyles2 } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+import { CombinedRule } from 'app/types/unified-alerting';
+
+import { useCombinedRule } from '../../../hooks/useCombinedRule';
+import { parse } from '../../../utils/rule-id';
+import { isGrafanaRulerRule } from '../../../utils/rules';
+import { MetaText } from '../../MetaText';
+import { VizWrapper } from '../../rule-editor/VizWrapper';
+import { AnnotationValue } from '../../rule-viewer/tabs/Details';
+import { LogRecord } from '../state-history/common';
+
+import { EventState } from './EventListSceneObject';
+
+interface EventDetailsProps {
+ record: LogRecord;
+ logRecords: LogRecord[];
+}
+export function EventDetails({ record, logRecords }: EventDetailsProps) {
+ // get the rule from the ruleUID
+ const ruleUID = record.line?.ruleUID ?? '';
+ const identifier = useMemo(() => {
+ return parse(ruleUID, true);
+ }, [ruleUID]);
+ const { error, loading, result: rule } = useCombinedRule({ ruleIdentifier: identifier });
+
+ if (error) {
+ return (
+
+ Error loading rule for this event.
+
+ );
+ }
+ if (loading) {
+ return (
+
+ Loading...
+
+ );
+ }
+
+ if (!rule) {
+ return (
+
+ Rule not found for this event.
+
+ );
+ }
+
+ const getTransitionsCountByRuleUID = (ruleUID: string) => {
+ return logRecords.filter((record) => record.line.ruleUID === ruleUID).length;
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+interface StateTransitionProps {
+ record: LogRecord;
+}
+function StateTransition({ record }: StateTransitionProps) {
+ return (
+
+
+ State transition
+
+
+
+
+
+
+
+ );
+}
+
+interface AnnotationsProps {
+ rule: CombinedRule;
+}
+const Annotations = ({ rule }: AnnotationsProps) => {
+ const styles = useStyles2(getStyles);
+ const annotations = rule.annotations;
+ if (!annotations) {
+ return null;
+ }
+ return (
+ <>
+
+ Annotations
+
+ {Object.keys(annotations).length === 0 ? (
+
+ No annotations
+
+ ) : (
+
+ {Object.entries(annotations).map(([name, value]) => (
+
+ {name}
+
+
+ ))}
+
+ )}
+ >
+ );
+};
+
+/**
+ *
+ * This component renders the visualization for the rule condition values over the selected time range.
+ * The visualization is a time series graph with the condition values on the y-axis and time on the x-axis.
+ * The values are extracted from the log records already fetched from the history api.
+ * The graph is rendered only if the rule is a Grafana rule.
+ *
+ */
+interface QueryVizualizationProps {
+ ruleUID: string;
+ rule: CombinedRule;
+ logRecords: LogRecord[];
+}
+const QueryVizualization = ({ ruleUID, rule, logRecords }: QueryVizualizationProps) => {
+ if (!isGrafanaRulerRule(rule?.rulerRule)) {
+ return (
+
+ Rule is not a Grafana rule
+
+ );
+ }
+ // get the condition from the rule
+ const condition = rule?.rulerRule.grafana_alert?.condition ?? 'A';
+ // get the panel data for the rule
+ const panelData = getPanelDataForRule(ruleUID, logRecords, condition);
+ // render the visualization
+ return ;
+};
+
+/**
+ * This function returns the time series panel data for the condtion values of the rule, within the selected time range.
+ * The values are extracted from the log records already fetched from the history api.
+ * @param ruleUID
+ * @param logRecords
+ * @param condition
+ * @returns PanelData
+ */
+export function getPanelDataForRule(ruleUID: string, logRecords: LogRecord[], condition: string) {
+ const ruleLogRecords = logRecords
+ .filter((record) => record.line.ruleUID === ruleUID)
+ // sort by timestamp as time series data is expected to be sorted by time
+ .sort((a, b) => a.timestamp - b.timestamp);
+
+ // get unique records by timestamp, as timeseries data should have unique timestamps, and it might be possible to have multiple records with the same timestamp
+ const uniqueRecords = uniqBy(ruleLogRecords, (record) => record.timestamp);
+
+ const timestamps = uniqueRecords.map((record) => record.timestamp);
+ const values = uniqueRecords.map((record) => (record.line.values ? record.line.values[condition] : 0));
+ const minTimestamp = min(timestamps);
+ const maxTimestamp = max(timestamps);
+
+ const PanelDataObj: PanelData = {
+ series: [
+ {
+ name: 'Rule condition history',
+ fields: [
+ { name: 'Time', values: timestamps, config: {}, type: FieldType.time },
+ { name: 'values', values: values, type: FieldType.number, config: {} },
+ ],
+ length: timestamps.length,
+ },
+ ],
+ state: LoadingState.Done,
+ timeRange: makeTimeRange(dateTime(minTimestamp), dateTime(maxTimestamp)),
+ };
+ return PanelDataObj;
+}
+
+interface ValueInTransitionProps {
+ record: LogRecord;
+}
+function ValueInTransition({ record }: ValueInTransitionProps) {
+ const values = record?.line?.values
+ ? JSON.stringify(record.line.values)
+ : t('alerting.central-alert-history.details.no-values', 'No values');
+ return (
+
+
+ Value in transition
+
+
+
+ {values}
+
+
+
+ );
+}
+interface NumberTransitionsProps {
+ transitions: number;
+}
+function NumberTransitions({ transitions }: NumberTransitionsProps) {
+ return (
+
+
+
+ State transitions for selected period
+
+
+
+ {transitions}
+
+
+ );
+}
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ metadataWrapper: css({
+ display: 'grid',
+ gridTemplateColumns: 'auto auto',
+ rowGap: theme.spacing(3),
+ columnGap: theme.spacing(12),
+ }),
+ };
+};
diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx
index 33d39a6eb01..3474049887d 100644
--- a/public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx
+++ b/public/app/features/alerting/unified/components/rules/central-state-history/EventListSceneObject.tsx
@@ -1,13 +1,13 @@
-import { css } from '@emotion/css';
-import { useMemo, useState } from 'react';
+import { css, cx } from '@emotion/css';
+import { ReactElement, useMemo, useState } from 'react';
import { useMeasure } from 'react-use';
-import { DataFrameJSON, GrafanaTheme2, TimeRange } from '@grafana/data';
+import { DataFrameJSON, GrafanaTheme2, IconName, TimeRange } from '@grafana/data';
import { isFetchError } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase, TextBoxVariable, VariableValue, sceneGraph } from '@grafana/scenes';
-import { Alert, Icon, LoadingBar, Stack, Text, Tooltip, useStyles2, withErrorBoundary } from '@grafana/ui';
+import { Alert, Icon, LoadingBar, Pagination, Stack, Text, Tooltip, useStyles2, withErrorBoundary } from '@grafana/ui';
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
-import { t } from 'app/core/internationalization';
+import { Trans, t } from 'app/core/internationalization';
import {
GrafanaAlertStateWithReason,
isAlertStateWithReason,
@@ -17,8 +17,10 @@ import {
} from 'app/types/unified-alerting-dto';
import { stateHistoryApi } from '../../../api/stateHistoryApi';
-import { labelsMatchMatchers, parseMatchers } from '../../../utils/alertmanager';
+import { usePagination } from '../../../hooks/usePagination';
+import { labelsMatchMatchers } from '../../../utils/alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource';
+import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers';
import { stringifyErrorLike } from '../../../utils/misc';
import { AlertLabels } from '../../AlertLabels';
import { CollapseToggle } from '../../CollapseToggle';
@@ -26,8 +28,10 @@ import { LogRecord } from '../state-history/common';
import { isLine, isNumbers } from '../state-history/useRuleHistoryRecords';
import { LABELS_FILTER } from './CentralAlertHistoryScene';
+import { EventDetails } from './EventDetails';
export const LIMIT_EVENTS = 5000; // limit is hard-capped at 5000 at the BE level.
+const PAGE_SIZE = 100;
/**
*
@@ -35,13 +39,11 @@ export const LIMIT_EVENTS = 5000; // limit is hard-capped at 5000 at the BE leve
* It fetches the events from the history api and displays them in a list.
* The list is filtered by the labels in the filter variable and by the time range variable in the scene graph.
*/
-export const HistoryEventsList = ({
- timeRange,
- valueInfilterTextBox,
-}: {
+interface HistoryEventsListProps {
timeRange?: TimeRange;
valueInfilterTextBox: VariableValue;
-}) => {
+}
+export const HistoryEventsList = ({ timeRange, valueInfilterTextBox }: HistoryEventsListProps) => {
const from = timeRange?.from.unix();
const to = timeRange?.to.unix();
@@ -85,12 +87,23 @@ interface HistoryLogEventsProps {
logRecords: LogRecord[];
}
function HistoryLogEvents({ logRecords }: HistoryLogEventsProps) {
+ const { page, pageItems, numberOfPages, onPageChange } = usePagination(logRecords, 1, PAGE_SIZE);
return (
-
- {logRecords.map((record) => {
- return ;
- })}
-
+
+
+ {pageItems.map((record) => {
+ return (
+
+ );
+ })}
+
+ {/* This paginations improves the performance considerably , making the page load faster */}
+
+
);
}
@@ -102,17 +115,25 @@ function HistoryErrorMessage({ error }: HistoryErrorMessageProps) {
if (isFetchError(error) && error.status === 404) {
return ;
}
- const title = t('central-alert-history.error', 'Something went wrong loading the alert state history');
+ const title = t('alerting.central-alert-history.error', 'Something went wrong loading the alert state history');
+ const errorStr = stringifyErrorLike(error);
- return {stringifyErrorLike(error)} ;
+ return {errorStr} ;
}
-function EventRow({ record }: { record: LogRecord }) {
+interface EventRowProps {
+ record: LogRecord;
+ logRecords: LogRecord[];
+}
+function EventRow({ record, logRecords }: EventRowProps) {
const styles = useStyles2(getStyles);
const [isCollapsed, setIsCollapsed] = useState(true);
return (
-
-
+ {!isCollapsed && (
+
+
+
+ )}
+
);
}
-function AlertRuleName({ labels, ruleUID }: { labels: Record
; ruleUID?: string }) {
+interface AlertRuleNameProps {
+ labels: Record;
+ ruleUID?: string;
+}
+function AlertRuleName({ labels, ruleUID }: AlertRuleNameProps) {
const styles = useStyles2(getStyles);
const alertRuleName = labels['alertname'];
if (!ruleUID) {
- return {alertRuleName} ;
+ return (
+
+ Unknown
+ {alertRuleName}
+
+ );
}
return (
@@ -170,55 +205,90 @@ function EventTransition({ previous, current }: EventTransitionProps) {
);
}
-function EventState({ state }: { state: GrafanaAlertStateWithReason }) {
- const styles = useStyles2(getStyles);
+interface StateIconProps {
+ iconName: IconName;
+ iconColor: string;
+ tooltipContent: string;
+ labelText: ReactElement;
+ showLabel: boolean;
+}
+const StateIcon = ({ iconName, iconColor, tooltipContent, labelText, showLabel }: StateIconProps) => (
+
+
+
+ {showLabel && (
+
+ {labelText}
+
+ )}
+
+
+);
+interface EventStateProps {
+ state: GrafanaAlertStateWithReason;
+ showLabel?: boolean;
+}
+export function EventState({ state, showLabel = false }: EventStateProps) {
+ const styles = useStyles2(getStyles);
+ const toolTip = t('alerting.central-alert-history.details.no-recognized-state', 'No recognized state');
if (!isGrafanaAlertState(state) && !isAlertStateWithReason(state)) {
return (
-
-
-
+ Unknown}
+ showLabel={Boolean(showLabel)}
+ iconColor={styles.warningColor}
+ />
);
}
const baseState = mapStateWithReasonToBaseState(state);
const reason = mapStateWithReasonToReason(state);
-
- switch (baseState) {
- case 'Normal':
- return (
-
-
-
- );
- case 'Alerting':
- return (
-
-
-
- );
- case 'NoData': //todo:change icon
- return (
-
-
- {/* no idea which icon to use */}
-
- );
- case 'Error':
- return (
-
-
-
- );
-
- case 'Pending':
- return (
-
-
-
- );
- default:
- return ;
+ interface StateConfig {
+ iconName: IconName;
+ iconColor: string;
+ tooltipContent: string;
+ labelText: ReactElement;
}
+ interface StateConfigMap {
+ [key: string]: StateConfig;
+ }
+ const stateConfig: StateConfigMap = {
+ Normal: {
+ iconName: 'check-circle',
+ iconColor: Boolean(reason) ? styles.warningColor : styles.normalColor,
+ tooltipContent: Boolean(reason) ? `Normal (${reason})` : 'Normal',
+ labelText: Normal ,
+ },
+ Alerting: {
+ iconName: 'exclamation-circle',
+ iconColor: styles.alertingColor,
+ tooltipContent: 'Alerting',
+ labelText: Alerting ,
+ },
+ NoData: {
+ iconName: 'exclamation-triangle',
+ iconColor: styles.warningColor,
+ tooltipContent: 'Insufficient data',
+ labelText: No data ,
+ },
+ Error: {
+ iconName: 'exclamation-circle',
+ tooltipContent: 'Error',
+ iconColor: styles.warningColor,
+ labelText: Error ,
+ },
+ Pending: {
+ iconName: 'circle',
+ iconColor: styles.warningColor,
+ tooltipContent: Boolean(reason) ? `Pending (${reason})` : 'Pending',
+ labelText: Pending ,
+ },
+ };
+
+ const config = stateConfig[baseState] || { iconName: 'exclamation-triangle', tooltipContent: 'Unknown State' };
+ return ;
}
interface TimestampProps {
@@ -253,12 +323,16 @@ export const getStyles = (theme: GrafanaTheme2) => {
alignItems: 'center',
padding: `${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)} 0`,
flexWrap: 'nowrap',
- borderBottom: `1px solid ${theme.colors.border.weak}`,
-
'&:hover': {
backgroundColor: theme.components.table.rowHoverBackground,
},
}),
+ collapsedHeader: css({
+ borderBottom: `1px solid ${theme.colors.border.weak}`,
+ }),
+ notCollapsedHeader: css({
+ borderBottom: 'none',
+ }),
collapseToggle: css({
background: 'none',
@@ -303,6 +377,11 @@ export const getStyles = (theme: GrafanaTheme2) => {
display: 'block',
color: theme.colors.text.link,
}),
+ expandedRow: css({
+ padding: theme.spacing(2),
+ marginLeft: theme.spacing(2),
+ borderLeft: `1px solid ${theme.colors.border.weak}`,
+ }),
};
};
@@ -334,7 +413,7 @@ function useRuleHistoryRecords(stateHistory?: DataFrameJSON, filter?: string) {
return { historyRecords: [] };
}
- const filterMatchers = filter ? parseMatchers(filter) : [];
+ const filterMatchers = filter ? parsePromQLStyleMatcherLooseSafe(filter) : [];
const [tsValues, lines] = stateHistory.data.values;
const timestamps = isNumbers(tsValues) ? tsValues : [];
diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/getPanelDataForRule.test.ts b/public/app/features/alerting/unified/components/rules/central-state-history/getPanelDataForRule.test.ts
new file mode 100644
index 00000000000..43abc2c5d6b
--- /dev/null
+++ b/public/app/features/alerting/unified/components/rules/central-state-history/getPanelDataForRule.test.ts
@@ -0,0 +1,53 @@
+import { dateTime } from '@grafana/data';
+
+import { LogRecord } from '../state-history/common';
+
+import { getPanelDataForRule } from './EventDetails';
+
+const initialTimeStamp = 1000000;
+const instanceLabels = { foo: 'bar', severity: 'critical', cluster: 'dev-us' }; // actually, it doesn't matter what is here
+const records: LogRecord[] = [
+ {
+ timestamp: initialTimeStamp,
+ line: { previous: 'Normal', current: 'Alerting', labels: instanceLabels, ruleUID: 'ruleUID1', values: { C: 1 } },
+ },
+ {
+ timestamp: initialTimeStamp + 1000,
+ line: { previous: 'Alerting', current: 'Normal', labels: instanceLabels, ruleUID: 'ruleUID2' },
+ },
+ {
+ timestamp: initialTimeStamp + 2000,
+ line: { previous: 'Normal', current: 'Alerting', labels: instanceLabels, ruleUID: 'ruleUID3' },
+ },
+ // not sorted by timestamp
+ {
+ timestamp: initialTimeStamp + 4000,
+ line: { previous: 'Normal', current: 'Alerting', labels: instanceLabels, ruleUID: 'ruleUID1', values: { C: 8 } },
+ },
+ {
+ timestamp: initialTimeStamp + 3000,
+ line: { previous: 'Alerting', current: 'Normal', labels: instanceLabels, ruleUID: 'ruleUID1', values: { C: 0 } },
+ },
+ //duplicate record in the same timestamp
+ {
+ timestamp: initialTimeStamp + 3000,
+ line: { previous: 'Alerting', current: 'Normal', labels: instanceLabels, ruleUID: 'ruleUID1', values: { C: 0 } },
+ },
+ {
+ timestamp: initialTimeStamp + 5000,
+ line: { previous: 'Alerting', current: 'Normal', labels: instanceLabels, ruleUID: 'ruleUID1', values: { C: 0 } },
+ },
+];
+describe('getPanelDataForRule', () => {
+ it('should return correct panel data for a given rule (sorted by time and unique)', () => {
+ const result = getPanelDataForRule('ruleUID1', records, 'C');
+
+ expect(result.series[0].fields[0].values).toEqual([1000000, 1003000, 1004000, 1005000]);
+ expect(result.series[0].fields[1].values).toEqual([1, 0, 8, 0]);
+ expect(result.series[0].fields[0].type).toEqual('time');
+ expect(result.series[0].fields[1].type).toEqual('number');
+ expect(result.state).toEqual('Done');
+ expect(result.timeRange.from).toEqual(dateTime(1000000));
+ expect(result.timeRange.to).toEqual(dateTime(1005000));
+ });
+});
diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts b/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts
index 02cd7a7dc8d..324849c6da5 100644
--- a/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts
+++ b/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts
@@ -3,7 +3,8 @@ import { groupBy } from 'lodash';
import { DataFrame, Field as DataFrameField, DataFrameJSON, Field, FieldType } from '@grafana/data';
import { fieldIndexComparer } from '@grafana/data/src/field/fieldComparers';
-import { labelsMatchMatchers, parseMatchers } from '../../../utils/alertmanager';
+import { labelsMatchMatchers } from '../../../utils/alertmanager';
+import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers';
import { LogRecord } from '../state-history/common';
import { isLine, isNumbers } from '../state-history/useRuleHistoryRecords';
@@ -61,7 +62,7 @@ function groupDataFramesByTimeAndFilterByLabels(dataFrames: DataFrame[]): DataFr
const filterValue = getFilterInQueryParams();
const dataframesFiltered = dataFrames.filter((frame) => {
const labels = JSON.parse(frame.name ?? ''); // in name we store the labels stringified
- const matchers = Boolean(filterValue) ? parseMatchers(filterValue) : [];
+ const matchers = Boolean(filterValue) ? parsePromQLStyleMatcherLooseSafe(filterValue) : [];
return labelsMatchMatchers(labels, matchers);
});
// Extract time fields from filtered data frames
diff --git a/public/app/features/alerting/unified/components/rules/state-history/useRuleHistoryRecords.tsx b/public/app/features/alerting/unified/components/rules/state-history/useRuleHistoryRecords.tsx
index 67efa185b3a..8187fb06e8a 100644
--- a/public/app/features/alerting/unified/components/rules/state-history/useRuleHistoryRecords.tsx
+++ b/public/app/features/alerting/unified/components/rules/state-history/useRuleHistoryRecords.tsx
@@ -13,7 +13,8 @@ import { fieldIndexComparer } from '@grafana/data/src/field/fieldComparers';
import { MappingType, ThresholdsMode } from '@grafana/schema';
import { useTheme2 } from '@grafana/ui';
-import { labelsMatchMatchers, parseMatchers } from '../../../utils/alertmanager';
+import { labelsMatchMatchers } from '../../../utils/alertmanager';
+import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers';
import { extractCommonLabels, Line, LogRecord, omitLabels } from './common';
@@ -50,7 +51,7 @@ export function useRuleHistoryRecords(stateHistory?: DataFrameJSON, filter?: str
const commonLabels = extractCommonLabels(groupLabelsArray);
- const filterMatchers = filter ? parseMatchers(filter) : [];
+ const filterMatchers = filter ? parsePromQLStyleMatcherLooseSafe(filter) : [];
const filteredGroupedLines = Object.entries(logRecordsByInstance).filter(([key]) => {
const labels = JSON.parse(key);
return labelsMatchMatchers(labels, filterMatchers);
diff --git a/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx b/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx
index 7cb57c91055..a15698fc893 100644
--- a/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx
+++ b/public/app/features/alerting/unified/components/silences/SilencesFilter.tsx
@@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Button, Field, Icon, Input, Label, Tooltip, useStyles2, Stack } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
-import { parseMatchers } from '../../utils/alertmanager';
+import { parsePromQLStyleMatcherLoose } from '../../utils/matchers';
import { getSilenceFiltersFromUrlParams } from '../../utils/misc';
const getQueryStringKey = () => uniqueId('query-string-');
@@ -30,7 +30,16 @@ export const SilencesFilter = () => {
setTimeout(() => setQueryStringKey(getQueryStringKey()));
};
- const inputInvalid = queryString && queryString.length > 3 ? parseMatchers(queryString).length === 0 : false;
+ let inputValid = queryString && queryString.length > 3;
+ try {
+ if (!queryString) {
+ inputValid = true;
+ } else {
+ parsePromQLStyleMatcherLoose(queryString);
+ }
+ } catch (err) {
+ inputValid = false;
+ }
return (
@@ -53,8 +62,8 @@ export const SilencesFilter = () => {
}
- invalid={inputInvalid}
- error={inputInvalid ? 'Query must use valid matcher syntax' : null}
+ invalid={!inputValid}
+ error={!inputValid ? 'Query must use valid matcher syntax' : null}
>
{
}
}
if (queryString) {
- const matchers = parseMatchers(queryString);
+ const matchers = parsePromQLStyleMatcherLooseSafe(queryString);
const matchersMatch = matchers.every((matcher) =>
silence.matchers?.some(
({ name, value, isEqual, isRegex }) =>
diff --git a/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts b/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts
index 737d01d532c..4aef92130ab 100644
--- a/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts
+++ b/public/app/features/alerting/unified/hooks/useCombinedRuleNamespaces.ts
@@ -468,7 +468,7 @@ function hashQuery(query: string) {
This hook returns combined Grafana rules. Optionally, it can filter rules by dashboard UID and panel ID.
*/
export function useCombinedRules(
- dashboardUID?: string,
+ dashboardUID?: string | null,
panelId?: number,
poll?: boolean
): {
@@ -483,10 +483,12 @@ export function useCombinedRules(
} = alertRuleApi.endpoints.prometheusRuleNamespaces.useQuery(
{
ruleSourceName: GRAFANA_RULES_SOURCE_NAME,
- dashboardUid: dashboardUID,
+ dashboardUid: dashboardUID ?? undefined,
panelId,
},
{
+ // "null" means the dashboard isn't saved yet, as opposed to "undefined" which means we don't want to filter by dashboard UID
+ skip: dashboardUID === null,
pollingInterval: poll ? RULE_LIST_POLL_INTERVAL_MS : undefined,
}
);
@@ -498,10 +500,11 @@ export function useCombinedRules(
} = alertRuleApi.endpoints.rulerRules.useQuery(
{
rulerConfig: grafanaRulerConfig,
- filter: { dashboardUID, panelId },
+ filter: { dashboardUID: dashboardUID ?? undefined, panelId },
},
{
pollingInterval: poll ? RULE_LIST_POLL_INTERVAL_MS : undefined,
+ skip: dashboardUID === null,
}
);
diff --git a/public/app/features/alerting/unified/hooks/useFilteredAmGroups.ts b/public/app/features/alerting/unified/hooks/useFilteredAmGroups.ts
index 3aa49042d93..3d88eafd006 100644
--- a/public/app/features/alerting/unified/hooks/useFilteredAmGroups.ts
+++ b/public/app/features/alerting/unified/hooks/useFilteredAmGroups.ts
@@ -3,19 +3,21 @@ import { useMemo } from 'react';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
-import { labelsMatchMatchers, parseMatchers } from '../utils/alertmanager';
+import { labelsMatchMatchers } from '../utils/alertmanager';
+import { parsePromQLStyleMatcherLooseSafe } from '../utils/matchers';
import { getFiltersFromUrlParams } from '../utils/misc';
export const useFilteredAmGroups = (groups: AlertmanagerGroup[]) => {
const [queryParams] = useQueryParams();
- const filters = getFiltersFromUrlParams(queryParams);
- const matchers = parseMatchers(filters.queryString || '');
+ const { queryString, alertState } = getFiltersFromUrlParams(queryParams);
return useMemo(() => {
+ const matchers = queryString ? parsePromQLStyleMatcherLooseSafe(queryString) : [];
+
return groups.reduce((filteredGroup: AlertmanagerGroup[], group) => {
const alerts = group.alerts.filter(({ labels, status }) => {
const labelsMatch = labelsMatchMatchers(labels, matchers);
- const filtersMatch = filters.alertState ? status.state === filters.alertState : true;
+ const filtersMatch = alertState ? status.state === alertState : true;
return labelsMatch && filtersMatch;
});
if (alerts.length > 0) {
@@ -28,5 +30,5 @@ export const useFilteredAmGroups = (groups: AlertmanagerGroup[]) => {
}
return filteredGroup;
}, []);
- }, [groups, filters, matchers]);
+ }, [queryString, groups, alertState]);
};
diff --git a/public/app/features/alerting/unified/hooks/useFilteredRules.ts b/public/app/features/alerting/unified/hooks/useFilteredRules.ts
index eaa13b43840..f7674c92500 100644
--- a/public/app/features/alerting/unified/hooks/useFilteredRules.ts
+++ b/public/app/features/alerting/unified/hooks/useFilteredRules.ts
@@ -9,10 +9,10 @@ import { CombinedRuleGroup, CombinedRuleNamespace, Rule } from 'app/types/unifie
import { isPromAlertingRuleState, PromRuleType, RulerGrafanaRuleDTO } from 'app/types/unified-alerting-dto';
import { applySearchFilterToQuery, getSearchFilterFromQuery, RulesFilter } from '../search/rulesSearchParser';
-import { labelsMatchMatchers, matcherToMatcherField, parseMatchers } from '../utils/alertmanager';
+import { labelsMatchMatchers, matcherToMatcherField } from '../utils/alertmanager';
import { Annotation } from '../utils/constants';
import { isCloudRulesSource } from '../utils/datasource';
-import { parseMatcher } from '../utils/matchers';
+import { parseMatcher, parsePromQLStyleMatcherLoose } from '../utils/matchers';
import {
getRuleHealth,
isAlertingRule,
@@ -71,7 +71,7 @@ export function useRulesFilter() {
dataSource: queryParams.get('dataSource') ?? undefined,
alertState: queryParams.get('alertState') ?? undefined,
ruleType: queryParams.get('ruleType') ?? undefined,
- labels: parseMatchers(queryParams.get('queryString') ?? '').map(matcherToMatcherField),
+ labels: parsePromQLStyleMatcherLoose(queryParams.get('queryString') ?? '').map(matcherToMatcherField),
};
const hasLegacyFilters = Object.values(legacyFilters).some((legacyFilter) => !isEmpty(legacyFilter));
diff --git a/public/app/features/alerting/unified/hooks/usePanelCombinedRules.ts b/public/app/features/alerting/unified/hooks/usePanelCombinedRules.ts
index 388c3ff2b1b..61b9323fbbd 100644
--- a/public/app/features/alerting/unified/hooks/usePanelCombinedRules.ts
+++ b/public/app/features/alerting/unified/hooks/usePanelCombinedRules.ts
@@ -3,7 +3,7 @@ import { CombinedRule } from 'app/types/unified-alerting';
import { useCombinedRules } from './useCombinedRuleNamespaces';
interface Options {
- dashboardUID: string;
+ dashboardUID: string | null;
panelId: number;
poll?: boolean;
diff --git a/public/app/features/alerting/unified/insights/DataSourcesInfo.tsx b/public/app/features/alerting/unified/insights/DataSourcesInfo.tsx
index bb3831c689b..81e5d619945 100644
--- a/public/app/features/alerting/unified/insights/DataSourcesInfo.tsx
+++ b/public/app/features/alerting/unified/insights/DataSourcesInfo.tsx
@@ -1,6 +1,6 @@
import { css } from '@emotion/css';
-import { GrafanaTheme2 } from '@grafana/data/src/themes';
+import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { DataSourceInformation } from '../home/Insights';
diff --git a/public/app/features/alerting/unified/insights/RatingModal.tsx b/public/app/features/alerting/unified/insights/RatingModal.tsx
index 0dd41eb7a61..8611d4602e5 100644
--- a/public/app/features/alerting/unified/insights/RatingModal.tsx
+++ b/public/app/features/alerting/unified/insights/RatingModal.tsx
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { useState } from 'react';
-import { GrafanaTheme2 } from '@grafana/data/src/themes';
+import { GrafanaTheme2 } from '@grafana/data';
import { Button, Dropdown, Icon, IconButton, Menu, Modal, useStyles2 } from '@grafana/ui';
import { trackInsightsFeedback } from '../Analytics';
diff --git a/public/app/features/alerting/unified/insights/SectionFooter.tsx b/public/app/features/alerting/unified/insights/SectionFooter.tsx
index 560b562abff..7006169da10 100644
--- a/public/app/features/alerting/unified/insights/SectionFooter.tsx
+++ b/public/app/features/alerting/unified/insights/SectionFooter.tsx
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import * as React from 'react';
-import { GrafanaTheme2 } from '@grafana/data/src/themes';
+import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
export function SectionFooter({ children }: React.PropsWithChildren<{}>) {
diff --git a/public/app/features/alerting/unified/insights/SectionSubheader.tsx b/public/app/features/alerting/unified/insights/SectionSubheader.tsx
index 302a391129d..a40e4c04117 100644
--- a/public/app/features/alerting/unified/insights/SectionSubheader.tsx
+++ b/public/app/features/alerting/unified/insights/SectionSubheader.tsx
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import * as React from 'react';
-import { GrafanaTheme2 } from '@grafana/data/src/themes';
+import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { DataSourceInformation } from '../home/Insights';
diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts
index 34c1a5ead12..8af7b141bff 100644
--- a/public/app/features/alerting/unified/mocks.ts
+++ b/public/app/features/alerting/unified/mocks.ts
@@ -21,7 +21,6 @@ import { DataSourceSrv, GetDataSourceListFilters, config } from '@grafana/runtim
import { defaultDashboard } from '@grafana/schema';
import { contextSrv } from 'app/core/services/context_srv';
import { MOCK_GRAFANA_ALERT_RULE_TITLE } from 'app/features/alerting/unified/mocks/server/handlers/alertRules';
-import { parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import {
AlertManagerCortexConfig,
@@ -64,6 +63,8 @@ import {
import { DashboardSearchItem, DashboardSearchItemType } from '../../search/types';
+import { parsePromQLStyleMatcherLooseSafe } from './utils/matchers';
+
let nextDataSourceId = 1;
export function mockDataSource
(
@@ -328,12 +329,12 @@ export const mockSilences = [
mockSilence({ id: MOCK_SILENCE_ID_EXISTING, comment: 'Happy path silence' }),
mockSilence({
id: 'ce031625-61c7-47cd-9beb-8760bccf0ed7',
- matchers: parseMatchers('foo!=bar'),
+ matchers: parsePromQLStyleMatcherLooseSafe('foo!=bar'),
comment: 'Silence with negated matcher',
}),
mockSilence({
id: MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID,
- matchers: parseMatchers(`__alert_rule_uid__=${MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID}`),
+ matchers: parsePromQLStyleMatcherLooseSafe(`__alert_rule_uid__=${MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID}`),
comment: 'Silence with alert rule UID matcher',
metadata: {
rule_title: MOCK_GRAFANA_ALERT_RULE_TITLE,
@@ -341,7 +342,7 @@ export const mockSilences = [
}),
mockSilence({
id: MOCK_SILENCE_ID_LACKING_PERMISSIONS,
- matchers: parseMatchers('something=else'),
+ matchers: parsePromQLStyleMatcherLooseSafe('something=else'),
comment: 'Silence without permissions to edit',
accessControl: {},
}),
diff --git a/public/app/features/alerting/unified/utils/alertmanager.test.ts b/public/app/features/alerting/unified/utils/alertmanager.test.ts
index d8f3cbe59c1..3c0293aef94 100644
--- a/public/app/features/alerting/unified/utils/alertmanager.test.ts
+++ b/public/app/features/alerting/unified/utils/alertmanager.test.ts
@@ -1,8 +1,8 @@
import { Matcher, MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types';
import { Labels } from 'app/types/unified-alerting-dto';
-import { parseMatchers, labelsMatchMatchers, removeMuteTimingFromRoute, matchersToString } from './alertmanager';
-import { parseMatcher } from './matchers';
+import { labelsMatchMatchers, removeMuteTimingFromRoute, matchersToString } from './alertmanager';
+import { parseMatcher, parsePromQLStyleMatcherLooseSafe } from './matchers';
describe('Alertmanager utils', () => {
describe('parseMatcher', () => {
@@ -64,57 +64,6 @@ describe('Alertmanager utils', () => {
});
});
- describe('parseMatchers', () => {
- it('should parse all operators', () => {
- expect(parseMatchers('foo=bar, bar=~ba.+, severity!=warning, email!~@grafana.com')).toEqual([
- { name: 'foo', value: 'bar', isRegex: false, isEqual: true },
- { name: 'bar', value: 'ba.+', isEqual: true, isRegex: true },
- { name: 'severity', value: 'warning', isRegex: false, isEqual: false },
- { name: 'email', value: '@grafana.com', isRegex: true, isEqual: false },
- ]);
- });
-
- it('should parse with spaces and brackets', () => {
- expect(parseMatchers('{ foo=bar }')).toEqual([
- {
- name: 'foo',
- value: 'bar',
- isRegex: false,
- isEqual: true,
- },
- ]);
- });
-
- it('should parse with spaces in the value', () => {
- expect(parseMatchers('foo=bar bazz')).toEqual([
- {
- name: 'foo',
- value: 'bar bazz',
- isRegex: false,
- isEqual: true,
- },
- ]);
- });
-
- it('should return nothing for invalid operator', () => {
- expect(parseMatchers('foo=!bar')).toEqual([]);
- });
-
- it('should parse matchers with or without quotes', () => {
- expect(parseMatchers('foo="bar",bar=bazz')).toEqual([
- { name: 'foo', value: 'bar', isRegex: false, isEqual: true },
- { name: 'bar', value: 'bazz', isEqual: true, isRegex: false },
- ]);
- });
-
- it('should parse matchers for key with special characters', () => {
- expect(parseMatchers('foo.bar-baz="bar",baz-bar.foo=bazz')).toEqual([
- { name: 'foo.bar-baz', value: 'bar', isRegex: false, isEqual: true },
- { name: 'baz-bar.foo', value: 'bazz', isEqual: true, isRegex: false },
- ]);
- });
- });
-
describe('labelsMatchMatchers', () => {
it('should return true for matching labels', () => {
const labels: Labels = {
@@ -123,7 +72,7 @@ describe('Alertmanager utils', () => {
bazz: 'buzz',
};
- const matchers = parseMatchers('foo=bar,bar=bazz');
+ const matchers = parsePromQLStyleMatcherLooseSafe('foo=bar,bar=bazz');
expect(labelsMatchMatchers(labels, matchers)).toBe(true);
});
it('should return false for no matching labels', () => {
@@ -131,7 +80,7 @@ describe('Alertmanager utils', () => {
foo: 'bar',
bar: 'bazz',
};
- const matchers = parseMatchers('foo=buzz');
+ const matchers = parsePromQLStyleMatcherLooseSafe('foo=buzz');
expect(labelsMatchMatchers(labels, matchers)).toBe(false);
});
it('should match with different operators', () => {
@@ -140,7 +89,7 @@ describe('Alertmanager utils', () => {
bar: 'bazz',
email: 'admin@grafana.com',
};
- const matchers = parseMatchers('foo!=bazz,bar=~ba.+');
+ const matchers = parsePromQLStyleMatcherLooseSafe('foo!=bazz,bar=~ba.+');
expect(labelsMatchMatchers(labels, matchers)).toBe(true);
});
});
@@ -198,7 +147,7 @@ describe('Alertmanager utils', () => {
const matchersString = matchersToString(matchers);
- expect(matchersString).toBe('{severity="critical",resource=~"cpu",rule_uid!="2Otf8canzz",cluster!~"prom"}');
+ expect(matchersString).toBe('{ severity="critical", resource=~"cpu", rule_uid!="2Otf8canzz", cluster!~"prom" }');
});
});
});
diff --git a/public/app/features/alerting/unified/utils/alertmanager.ts b/public/app/features/alerting/unified/utils/alertmanager.ts
index 2738b703844..8a73dd2685c 100644
--- a/public/app/features/alerting/unified/utils/alertmanager.ts
+++ b/public/app/features/alerting/unified/utils/alertmanager.ts
@@ -16,7 +16,7 @@ import { MatcherFieldValue } from '../types/silence-form';
import { getAllDataSources } from './config';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './datasource';
-import { MatcherFormatter, unquoteWithUnescape } from './matchers';
+import { MatcherFormatter, parsePromQLStyleMatcherLooseSafe, unquoteWithUnescape } from './matchers';
export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig {
// add default receiver if it does not exist
@@ -106,10 +106,10 @@ export function matchersToString(matchers: Matcher[]) {
const combinedMatchers = matcherFields.reduce((acc, current) => {
const currentMatcherString = `${current.name}${current.operator}"${current.value}"`;
- return acc ? `${acc},${currentMatcherString}` : currentMatcherString;
+ return acc ? `${acc}, ${currentMatcherString}` : currentMatcherString;
}, '');
- return `{${combinedMatchers}}`;
+ return `{ ${combinedMatchers} }`;
}
export const matcherFieldOptions: SelectableValue[] = [
@@ -124,35 +124,6 @@ export function matcherToObjectMatcher(matcher: Matcher): ObjectMatcher {
return [matcher.name, operator, matcher.value];
}
-export function parseMatchers(matcherQueryString: string): Matcher[] {
- const matcherRegExp = /\b([\w.-]+)(=~|!=|!~|=(?="?\w))"?([^"\n,}]*)"?/g;
- const matchers: Matcher[] = [];
-
- matcherQueryString.replace(matcherRegExp, (_, key, operator, value) => {
- const isEqual = operator === MatcherOperator.equal || operator === MatcherOperator.regex;
- const isRegex = operator === MatcherOperator.regex || operator === MatcherOperator.notRegex;
- matchers.push({
- name: key,
- value: isRegex ? getValidRegexString(value.trim()) : value.trim(),
- isEqual,
- isRegex,
- });
- return '';
- });
-
- return matchers;
-}
-
-function getValidRegexString(regex: string): string {
- // Regexes provided by users might be invalid, so we need to catch the error
- try {
- new RegExp(regex);
- return regex;
- } catch (error) {
- return '';
- }
-}
-
export function labelsMatchMatchers(labels: Labels, matchers: Matcher[]): boolean {
return matchers.every(({ name, value, isRegex, isEqual }) => {
return Object.entries(labels).some(([labelKey, labelValue]) => {
@@ -177,7 +148,7 @@ export function labelsMatchMatchers(labels: Labels, matchers: Matcher[]): boolea
}
export function combineMatcherStrings(...matcherStrings: string[]): string {
- const matchers = matcherStrings.map(parseMatchers).flat();
+ const matchers = matcherStrings.map(parsePromQLStyleMatcherLooseSafe).flat();
const uniqueMatchers = uniqWith(matchers, isEqual);
return matchersToString(uniqueMatchers);
}
diff --git a/public/app/features/alerting/unified/utils/amroutes.test.ts b/public/app/features/alerting/unified/utils/amroutes.test.ts
index 343e4587c4a..40441e890cc 100644
--- a/public/app/features/alerting/unified/utils/amroutes.test.ts
+++ b/public/app/features/alerting/unified/utils/amroutes.test.ts
@@ -72,10 +72,10 @@ describe('formAmRouteToAmRoute', () => {
// Assert
expect(amRoute.matchers).toStrictEqual([
- '"foo"="bar"',
- '"foo"="bar\\"baz"',
- '"foo"="bar\\\\baz"',
- '"foo"="\\\\bar\\\\baz\\"\\\\"',
+ 'foo="bar"',
+ 'foo="bar\\"baz"',
+ 'foo="bar\\\\baz"',
+ 'foo="\\\\bar\\\\baz\\"\\\\"',
]);
});
@@ -97,7 +97,7 @@ describe('formAmRouteToAmRoute', () => {
// Assert
expect(amRoute.matchers).toStrictEqual([
- '"foo"="bar"',
+ 'foo="bar"',
'"foo with spaces"="bar"',
'"foo\\\\slash"="bar"',
'"foo\\"quote"="bar"',
@@ -116,7 +116,7 @@ describe('formAmRouteToAmRoute', () => {
const amRoute = formAmRouteToAmRoute('mimir-am', route, { id: 'root' });
// Assert
- expect(amRoute.matchers).toStrictEqual(['"foo"=""']);
+ expect(amRoute.matchers).toStrictEqual(['foo=""']);
});
it('should allow matchers with empty values for Grafana AM', () => {
diff --git a/public/app/features/alerting/unified/utils/amroutes.ts b/public/app/features/alerting/unified/utils/amroutes.ts
index fd59aac2a84..f186a2e3aea 100644
--- a/public/app/features/alerting/unified/utils/amroutes.ts
+++ b/public/app/features/alerting/unified/utils/amroutes.ts
@@ -8,7 +8,7 @@ import { MatcherFieldValue } from '../types/silence-form';
import { matcherToMatcherField } from './alertmanager';
import { GRAFANA_RULES_SOURCE_NAME } from './datasource';
-import { normalizeMatchers, parseMatcherToArray, quoteWithEscape, unquoteWithUnescape } from './matchers';
+import { encodeMatcher, normalizeMatchers, parseMatcherToArray, unquoteWithUnescape } from './matchers';
import { findExistingRoute } from './routeTree';
import { isValidPrometheusDuration, safeParsePrometheusDuration } from './time';
@@ -189,9 +189,8 @@ export const formAmRouteToAmRoute = (
// Grafana maintains a fork of AM to support all utf-8 characters in the "object_matchers" property values but this
// does not exist in upstream AlertManager
if (alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME) {
- amRoute.matchers = formAmRoute.object_matchers?.map(
- ({ name, operator, value }) => `${quoteWithEscape(name)}${operator}${quoteWithEscape(value)}`
- );
+ // to support UTF-8 characters we must wrap label keys and values with double quotes if they contain reserved characters.
+ amRoute.matchers = formAmRoute.object_matchers?.map(encodeMatcher);
amRoute.object_matchers = undefined;
} else {
amRoute.object_matchers = normalizeMatchers(amRoute);
diff --git a/public/app/features/alerting/unified/utils/matchers.test.ts b/public/app/features/alerting/unified/utils/matchers.test.ts
index c10b03749b6..69487604a8c 100644
--- a/public/app/features/alerting/unified/utils/matchers.test.ts
+++ b/public/app/features/alerting/unified/utils/matchers.test.ts
@@ -1,14 +1,18 @@
-import { MatcherOperator, Route } from '../../../../plugins/datasource/alertmanager/types';
+import { Matcher, MatcherOperator, Route } from '../../../../plugins/datasource/alertmanager/types';
import {
+ encodeMatcher,
getMatcherQueryParams,
isPromQLStyleMatcher,
matcherToObjectMatcher,
normalizeMatchers,
parseMatcher,
parsePromQLStyleMatcher,
+ parsePromQLStyleMatcherLoose,
+ parsePromQLStyleMatcherLooseSafe,
parseQueryParamMatchers,
quoteWithEscape,
+ quoteWithEscapeIfRequired,
unquoteWithUnescape,
} from './matchers';
@@ -175,4 +179,102 @@ describe('parsePromQLStyleMatcher', () => {
it('should throw when not using correct syntax', () => {
expect(() => parsePromQLStyleMatcher('foo="bar"')).toThrow();
});
+
+ it('should only encode matchers if the label key contains reserved characters', () => {
+ expect(quoteWithEscapeIfRequired('foo')).toBe('foo');
+ expect(quoteWithEscapeIfRequired('foo bar')).toBe('"foo bar"');
+ expect(quoteWithEscapeIfRequired('foo{}bar')).toBe('"foo{}bar"');
+ expect(quoteWithEscapeIfRequired('foo\\bar')).toBe('"foo\\\\bar"');
+ });
+
+ it('should properly encode a matcher field', () => {
+ expect(encodeMatcher({ name: 'foo', operator: MatcherOperator.equal, value: 'baz' })).toBe('foo="baz"');
+ expect(encodeMatcher({ name: 'foo bar', operator: MatcherOperator.equal, value: 'baz' })).toBe('"foo bar"="baz"');
+ expect(encodeMatcher({ name: 'foo{}bar', operator: MatcherOperator.equal, value: 'baz qux' })).toBe(
+ '"foo{}bar"="baz qux"'
+ );
+ });
+});
+
+describe('parsePromQLStyleMatcherLooseSafe', () => {
+ it('should parse all operators', () => {
+ expect(parsePromQLStyleMatcherLooseSafe('foo=bar, bar=~ba.+, severity!=warning, email!~@grafana.com')).toEqual<
+ Matcher[]
+ >([
+ { name: 'foo', value: 'bar', isRegex: false, isEqual: true },
+ { name: 'bar', value: 'ba.+', isEqual: true, isRegex: true },
+ { name: 'severity', value: 'warning', isRegex: false, isEqual: false },
+ { name: 'email', value: '@grafana.com', isRegex: true, isEqual: false },
+ ]);
+ });
+
+ it('should parse with spaces and brackets', () => {
+ expect(parsePromQLStyleMatcherLooseSafe('{ foo=bar }')).toEqual([
+ {
+ name: 'foo',
+ value: 'bar',
+ isRegex: false,
+ isEqual: true,
+ },
+ ]);
+ });
+
+ it('should parse with spaces in the value', () => {
+ expect(parsePromQLStyleMatcherLooseSafe('foo=bar bazz')).toEqual([
+ {
+ name: 'foo',
+ value: 'bar bazz',
+ isRegex: false,
+ isEqual: true,
+ },
+ ]);
+ });
+
+ it('should return nothing for invalid operator', () => {
+ expect(parsePromQLStyleMatcherLooseSafe('foo=!bar')).toEqual([
+ {
+ name: 'foo',
+ value: '!bar',
+ isRegex: false,
+ isEqual: true,
+ },
+ ]);
+ });
+
+ it('should parse matchers with or without quotes', () => {
+ expect(parsePromQLStyleMatcherLooseSafe('foo="bar",bar=bazz')).toEqual([
+ { name: 'foo', value: 'bar', isRegex: false, isEqual: true },
+ { name: 'bar', value: 'bazz', isEqual: true, isRegex: false },
+ ]);
+ });
+
+ it('should parse matchers for key with special characters', () => {
+ expect(parsePromQLStyleMatcherLooseSafe('foo.bar-baz="bar",baz-bar.foo=bazz')).toEqual([
+ { name: 'foo.bar-baz', value: 'bar', isRegex: false, isEqual: true },
+ { name: 'baz-bar.foo', value: 'bazz', isEqual: true, isRegex: false },
+ ]);
+ });
+});
+
+describe('parsePromQLStyleMatcherLoose', () => {
+ it('should throw on invalid matcher', () => {
+ expect(() => {
+ parsePromQLStyleMatcherLoose('foo');
+ }).toThrow();
+
+ expect(() => {
+ parsePromQLStyleMatcherLoose('foo;bar');
+ }).toThrow();
+ });
+
+ it('should return empty array for empty input', () => {
+ expect(parsePromQLStyleMatcherLoose('')).toStrictEqual([]);
+ });
+
+ it('should also accept { } syntax', () => {
+ expect(parsePromQLStyleMatcherLoose('{ foo=bar, bar=baz }')).toStrictEqual([
+ { isEqual: true, isRegex: false, name: 'foo', value: 'bar' },
+ { isEqual: true, isRegex: false, name: 'bar', value: 'baz' },
+ ]);
+ });
});
diff --git a/public/app/features/alerting/unified/utils/matchers.ts b/public/app/features/alerting/unified/utils/matchers.ts
index a596a318da0..917e055e3bf 100644
--- a/public/app/features/alerting/unified/utils/matchers.ts
+++ b/public/app/features/alerting/unified/utils/matchers.ts
@@ -10,6 +10,7 @@ import { compact, uniqBy } from 'lodash';
import { Matcher, MatcherOperator, ObjectMatcher, Route } from 'app/plugins/datasource/alertmanager/types';
import { Labels } from '../../../../types/unified-alerting-dto';
+import { MatcherFieldValue } from '../types/silence-form';
import { isPrivateLabelKey } from './labels';
@@ -57,6 +58,8 @@ export function parseMatcher(matcher: string): Matcher {
/**
* This function combines parseMatcher and parsePromQLStyleMatcher, always returning an array of Matcher[] regardless of input syntax
+ * 1. { foo=bar, bar=baz }
+ * 2. foo=bar
*/
export function parseMatcherToArray(matcher: string): Matcher[] {
return isPromQLStyleMatcher(matcher) ? parsePromQLStyleMatcher(matcher) : [parseMatcher(matcher)];
@@ -70,6 +73,15 @@ export function parsePromQLStyleMatcher(matcher: string): Matcher[] {
throw new Error('not a PromQL style matcher');
}
+ return parsePromQLStyleMatcherLoose(matcher);
+}
+
+/**
+ * This function behaves the same as "parsePromQLStyleMatcher" but does not check if the matcher is formatted with { }
+ * In other words; it accepts both "{ foo=bar, bar=baz }" and "foo=bar,bar=baz"
+ * @throws
+ */
+export function parsePromQLStyleMatcherLoose(matcher: string): Matcher[] {
// split by `,` but not when it's used as a label value
const commaUnlessQuoted = /,(?=(?:[^"]*"[^"]*")*[^"]*$)/;
const parts = matcher.replace(/^\{/, '').replace(/\}$/, '').trim().split(commaUnlessQuoted);
@@ -83,6 +95,18 @@ export function parsePromQLStyleMatcher(matcher: string): Matcher[] {
}));
}
+/**
+ * This function behaves the same as "parsePromQLStyleMatcherLoose" but instead of throwing an error for incorrect syntax
+ * it returns an empty Array of matchers instead.
+ */
+export function parsePromQLStyleMatcherLooseSafe(matcher: string): Matcher[] {
+ try {
+ return parsePromQLStyleMatcherLoose(matcher);
+ } catch {
+ return [];
+ }
+}
+
// Parses a list of entries like like "['foo=bar', 'baz=~bad*']" into SilenceMatcher[]
export function parseQueryParamMatchers(matcherPairs: string[]): Matcher[] {
const parsedMatchers = matcherPairs.filter((x) => !!x.trim()).map((x) => parseMatcher(x));
@@ -144,6 +168,27 @@ export function quoteWithEscape(input: string) {
return `"${escaped}"`;
}
+// The list of reserved characters that indicate we should be escaping the label key / value are
+// { } ! = ~ , \ " ' ` and any whitespace (\s), encoded in the regular expression below
+//
+// See Alertmanager PR: https://github.com/prometheus/alertmanager/pull/3453
+const RESERVED_CHARACTERS = /[\{\}\!\=\~\,\\\"\'\`\s]+/;
+
+/**
+ * Quotes string only when reserved characters are used
+ */
+export function quoteWithEscapeIfRequired(input: string) {
+ const shouldQuote = RESERVED_CHARACTERS.test(input);
+ return shouldQuote ? quoteWithEscape(input) : input;
+}
+
+export const encodeMatcher = ({ name, operator, value }: MatcherFieldValue) => {
+ const encodedLabelName = quoteWithEscapeIfRequired(name);
+ const encodedLabelValue = quoteWithEscape(value);
+
+ return `${encodedLabelName}${operator}${encodedLabelValue}`;
+};
+
/**
* Unquotes and unescapes a string **if it has been quoted**
*/
diff --git a/public/app/features/apiserver/client.test.ts b/public/app/features/apiserver/client.test.ts
new file mode 100644
index 00000000000..dd233cd6d98
--- /dev/null
+++ b/public/app/features/apiserver/client.test.ts
@@ -0,0 +1,35 @@
+import { getBackendSrv } from '@grafana/runtime';
+
+import { DatasourceAPIVersions } from './client';
+
+jest.mock('@grafana/runtime', () => ({
+ getBackendSrv: jest.fn().mockReturnValue({
+ get: jest.fn(),
+ }),
+ config: {},
+}));
+
+describe('DatasourceAPIVersions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('get', async () => {
+ const getMock = jest.fn().mockResolvedValue({
+ groups: [
+ { name: 'testdata.datasource.grafana.app', preferredVersion: { version: 'v1' } },
+ { name: 'prometheus.datasource.grafana.app', preferredVersion: { version: 'v2' } },
+ { name: 'myorg-myplugin.datasource.grafana.app', preferredVersion: { version: 'v3' } },
+ ],
+ });
+ getBackendSrv().get = getMock;
+ const apiVersions = new DatasourceAPIVersions();
+ expect(await apiVersions.get('testdata')).toBe('v1');
+ expect(await apiVersions.get('grafana-testdata-datasource')).toBe('v1');
+ expect(await apiVersions.get('prometheus')).toBe('v2');
+ expect(await apiVersions.get('graphite')).toBeUndefined();
+ expect(await apiVersions.get('myorg-myplugin-datasource')).toBe('v3');
+ expect(getMock).toHaveBeenCalledTimes(1);
+ expect(getMock).toHaveBeenCalledWith('/apis');
+ });
+});
diff --git a/public/app/features/apiserver/client.ts b/public/app/features/apiserver/client.ts
index 6a23c3dce50..609d6a4c9ec 100644
--- a/public/app/features/apiserver/client.ts
+++ b/public/app/features/apiserver/client.ts
@@ -14,6 +14,7 @@ import {
AnnoKeyOriginPath,
AnnoKeyOriginHash,
AnnoKeyOriginName,
+ K8sAPIGroupList,
} from './types';
export interface GroupVersionResource {
@@ -110,3 +111,34 @@ function setOriginAsUI(meta: Partial) {
meta.annotations[AnnoKeyOriginPath] = window.location.pathname;
meta.annotations[AnnoKeyOriginHash] = config.buildInfo.versionString;
}
+
+export class DatasourceAPIVersions {
+ private apiVersions?: { [pluginID: string]: string };
+
+ async get(pluginID: string): Promise {
+ if (this.apiVersions) {
+ return this.apiVersions[pluginID];
+ }
+ const apis = await getBackendSrv().get('/apis');
+ const apiVersions: { [pluginID: string]: string } = {};
+ apis.groups.forEach((group) => {
+ if (group.name.includes('datasource.grafana.app')) {
+ const id = group.name.split('.')[0];
+ apiVersions[id] = group.preferredVersion.version;
+ // workaround for plugins that don't append '-datasource' for the group name
+ // e.g. org-plugin-datasource uses org-plugin.datasource.grafana.app
+ if (!id.endsWith('-datasource')) {
+ if (!id.includes('-')) {
+ // workaroud for Grafana plugins that don't include the org either
+ // e.g. testdata uses testdata.datasource.grafana.app
+ apiVersions[`grafana-${id}-datasource`] = group.preferredVersion.version;
+ } else {
+ apiVersions[`${id}-datasource`] = group.preferredVersion.version;
+ }
+ }
+ }
+ });
+ this.apiVersions = apiVersions;
+ return apiVersions[pluginID];
+ }
+}
diff --git a/public/app/features/apiserver/types.ts b/public/app/features/apiserver/types.ts
index 9065f67f668..e3ee1ded77c 100644
--- a/public/app/features/apiserver/types.ts
+++ b/public/app/features/apiserver/types.ts
@@ -150,3 +150,13 @@ export interface ResourceClient {
update(obj: ResourceForCreate): Promise>;
delete(name: string): Promise;
}
+
+export interface K8sAPIGroup {
+ name: string;
+ versions: Array<{ groupVersion: string; version: string }>;
+ preferredVersion: { groupVersion: string; version: string };
+}
+export interface K8sAPIGroupList {
+ kind: 'APIGroupList';
+ groups: K8sAPIGroup[];
+}
diff --git a/public/app/features/auth-config/ProviderConfigForm.test.tsx b/public/app/features/auth-config/ProviderConfigForm.test.tsx
index ad1b87ec39a..2ae4728227a 100644
--- a/public/app/features/auth-config/ProviderConfigForm.test.tsx
+++ b/public/app/features/auth-config/ProviderConfigForm.test.tsx
@@ -57,9 +57,18 @@ const testConfig: SSOProvider = {
allowedDomains: '',
allowedGroups: '',
scopes: '',
+ orgMapping: '',
},
};
+jest.mock('app/core/core', () => {
+ return {
+ contextSrv: {
+ isGrafanaAdmin: true,
+ },
+ };
+});
+
const emptyConfig = {
...testConfig,
settings: { ...testConfig.settings, enabled: false, clientId: '', clientSecret: '' },
@@ -120,6 +129,8 @@ describe('ProviderConfigForm', () => {
await user.click(screen.getByText('User mapping'));
await user.type(screen.getByRole('textbox', { name: /Role attribute path/i }), 'new-attribute-path');
await user.click(screen.getByRole('checkbox', { name: /Role attribute strict mode/i }));
+ await user.type(screen.getByRole('combobox', { name: /Organization mapping/i }), 'Group A:1:Editor{enter}');
+ await user.type(screen.getByRole('combobox', { name: /Organization mapping/i }), 'Group B:2:Admin{enter}');
await user.click(screen.getByText('Extra security measures'));
await user.type(screen.getByRole('combobox', { name: /Allowed domains/i }), 'grafana.com{enter}');
@@ -143,6 +154,7 @@ describe('ProviderConfigForm', () => {
clientSecret: 'test-client-secret',
enabled: true,
name: 'GitHub',
+ orgMapping: '["Group A:1:Editor","Group B:2:Admin"]',
roleAttributePath: 'new-attribute-path',
roleAttributeStrict: true,
scopes: 'user:email',
@@ -203,6 +215,7 @@ describe('ProviderConfigForm', () => {
tlsClientKey: '',
usePkce: false,
useRefreshToken: false,
+ orgMapping: '',
},
},
{ showErrorAlert: false }
diff --git a/public/app/features/auth-config/fields.tsx b/public/app/features/auth-config/fields.tsx
index 6b34468b22b..171f3df0243 100644
--- a/public/app/features/auth-config/fields.tsx
+++ b/public/app/features/auth-config/fields.tsx
@@ -38,7 +38,7 @@ export const sectionFields: Section = {
{
name: 'User mapping',
id: 'user',
- fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
+ fields: ['roleAttributePath', 'roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
},
{
name: 'Extra security measures',
@@ -86,6 +86,8 @@ export const sectionFields: Section = {
'idTokenAttributeName',
'roleAttributePath',
'roleAttributeStrict',
+ 'orgMapping',
+ 'orgAttributePath',
'allowAssignGrafanaAdmin',
'skipOrgRoleSync',
],
@@ -121,7 +123,7 @@ export const sectionFields: Section = {
{
name: 'User mapping',
id: 'user',
- fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
+ fields: ['roleAttributePath', 'roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
},
{
name: 'Extra security measures',
@@ -149,7 +151,7 @@ export const sectionFields: Section = {
{
name: 'User mapping',
id: 'user',
- fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
+ fields: ['roleAttributePath', 'roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
},
{
name: 'Extra security measures',
@@ -176,7 +178,7 @@ export const sectionFields: Section = {
{
name: 'User mapping',
id: 'user',
- fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
+ fields: ['roleAttributePath', 'roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
},
{
name: 'Extra security measures',
@@ -213,7 +215,14 @@ export const sectionFields: Section = {
{
name: 'User mapping',
id: 'user',
- fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
+ fields: [
+ 'roleAttributePath',
+ 'roleAttributeStrict',
+ 'orgMapping',
+ 'orgAttributePath',
+ 'allowAssignGrafanaAdmin',
+ 'skipOrgRoleSync',
+ ],
},
{
name: 'Extra security measures',
@@ -448,6 +457,22 @@ export function fieldMap(provider: string): Record {
description: 'Prevent synchronizing users’ organization roles from your IdP.',
type: 'switch',
},
+ orgMapping: {
+ label: 'Organization mapping',
+ description: orgMappingDescription(provider),
+ type: 'select',
+ hidden: !contextSrv.isGrafanaAdmin,
+ multi: true,
+ allowCustomValue: true,
+ options: [],
+ placeholder: 'Enter mappings (my-team:1:Viewer...) and press Enter to add',
+ },
+ orgAttributePath: {
+ label: 'Organization attribute path',
+ description: 'JMESPath expression to use for organization lookup.',
+ type: 'text',
+ hidden: !['generic_oauth', 'okta'].includes(provider),
+ },
defineAllowedGroups: {
label: 'Define allowed groups',
type: 'switch',
@@ -602,3 +627,19 @@ export function fieldMap(provider: string): Record {
function isNumeric(value: string) {
return /^-?\d+$/.test(value);
}
+
+function orgMappingDescription(provider: string): string {
+ switch (provider) {
+ case 'azuread':
+ return 'List of "::" mappings.';
+ case 'github':
+ return 'List of "::" mappings.';
+ case 'gitlab':
+ return 'List of "::';
+ case 'google':
+ return 'List of "::';
+ default:
+ // Generic OAuth, Okta
+ return 'List of "::" mappings.';
+ }
+}
diff --git a/public/app/features/auth-config/types.ts b/public/app/features/auth-config/types.ts
index b139efda4fb..19c827fd356 100644
--- a/public/app/features/auth-config/types.ts
+++ b/public/app/features/auth-config/types.ts
@@ -36,6 +36,7 @@ export type SSOProviderSettingsBase = {
roleAttributeStrict?: boolean;
signoutRedirectUrl?: string;
skipOrgRoleSync?: boolean;
+ orgAttributePath?: string;
teamIdsAttributePath?: string;
teamsUrl?: string;
tlsClientCa?: string;
@@ -70,6 +71,7 @@ export type SSOProvider = {
allowedDomains?: string;
allowedGroups?: string;
scopes?: string;
+ orgMapping?: string;
};
};
@@ -80,6 +82,7 @@ export type SSOProviderDTO = Partial & {
allowedDomains?: Array>;
allowedGroups?: Array>;
scopes?: Array>;
+ orgMapping?: Array>;
};
export interface AuthConfigState {
diff --git a/public/app/features/auth-config/utils/data.ts b/public/app/features/auth-config/utils/data.ts
index fa7d425939c..9b610a3cc82 100644
--- a/public/app/features/auth-config/utils/data.ts
+++ b/public/app/features/auth-config/utils/data.ts
@@ -51,6 +51,11 @@ const strToValue = (val: string | string[]): SelectableValue[] => {
if (Array.isArray(val)) {
return val.map((v) => ({ label: v, value: v }));
}
+ // Stored as JSON Array
+ if (val.startsWith('[') && val.endsWith(']')) {
+ return JSON.parse(val).map((v: string) => ({ label: v, value: v }));
+ }
+
return val.split(/[\s,]/).map((s) => ({ label: s, value: s }));
};
@@ -70,7 +75,11 @@ export function dataToDTO(data?: SSOProvider): SSOProviderDTO {
}
const valuesToString = (values: Array>) => {
- return values.map(({ value }) => value).join(',');
+ if (values.length <= 1) {
+ return values.map(({ value }) => value).join(',');
+ }
+ // Store as JSON array if there are multiple values
+ return JSON.stringify(values.map(({ value }) => value));
};
const getFieldsForProvider = (provider: string) => {
diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
index cac9d26c593..1aabecafc40 100644
--- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
+++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
@@ -1,9 +1,10 @@
import { css } from '@emotion/css';
import { memo, useEffect, useMemo } from 'react';
+import { useLocation } from 'react-router-dom';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
-import { locationService, reportInteraction } from '@grafana/runtime';
+import { reportInteraction } from '@grafana/runtime';
import { FilterInput, useStyles2 } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
@@ -39,7 +40,8 @@ const BrowseDashboardsPage = memo(({ match }: Props) => {
const styles = useStyles2(getStyles);
const [searchState, stateManager] = useSearchStateManager();
const isSearching = stateManager.hasSearchFilters();
- const search = locationService.getSearch();
+ const location = useLocation();
+ const search = useMemo(() => new URLSearchParams(location.search), [location.search]);
useEffect(() => {
stateManager.initStateFromUrl(folderUID);
diff --git a/public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts b/public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts
index b49c7663770..b4ad0c41b37 100644
--- a/public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts
+++ b/public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts
@@ -15,6 +15,7 @@ import { VizPanel } from '@grafana/scenes';
import { GrafanaQueryType } from 'app/plugins/datasource/grafana/types';
import { DashboardGridItem } from '../../scene/DashboardGridItem';
+import { DashboardScene } from '../../scene/DashboardScene';
import { LibraryVizPanel } from '../../scene/LibraryVizPanel';
import { gridItemToPanel, vizPanelToPanel } from '../../serialization/transformSceneToSaveModel';
import { getQueryRunnerFor } from '../../utils/utils';
@@ -62,15 +63,28 @@ export function getGithubMarkdown(panel: VizPanel, snapshot: string): string {
}
export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRange: TimeRange) {
- let saveModel;
+ let saveModel: ReturnType = { type: '' };
const isLibraryPanel = panel.parent instanceof LibraryVizPanel;
const gridItem = (isLibraryPanel ? panel.parent.parent : panel.parent) as DashboardGridItem;
+ const scene = panel.getRoot() as DashboardScene;
if (isLibraryPanel) {
saveModel = {
...gridItemToPanel(gridItem),
...vizPanelToPanel(panel),
};
+ } else if (scene.state.editPanel) {
+ // If panel edit mode is open when the user chooses the "get help" panel menu option
+ // we want the debug dashboard to include the panel with any changes that were made while
+ // in panel edit mode.
+ const sourcePanel = scene.state.editPanel.state.vizManager.state.sourcePanel.resolve();
+ const dashGridItem = sourcePanel.parent instanceof LibraryVizPanel ? sourcePanel.parent.parent : sourcePanel.parent;
+ if (dashGridItem instanceof DashboardGridItem) {
+ saveModel = {
+ ...gridItemToPanel(dashGridItem),
+ ...vizPanelToPanel(scene.state.editPanel.state.vizManager.state.panel.clone()),
+ };
+ }
} else {
saveModel = gridItemToPanel(gridItem);
}
diff --git a/public/app/features/dashboard-scene/inspect/InspectDataTab.tsx b/public/app/features/dashboard-scene/inspect/InspectDataTab.tsx
index 3e835a1c0af..9b6805153b4 100644
--- a/public/app/features/dashboard-scene/inspect/InspectDataTab.tsx
+++ b/public/app/features/dashboard-scene/inspect/InspectDataTab.tsx
@@ -25,7 +25,7 @@ export class InspectDataTab extends SceneObjectBase {
super({
...state,
options: {
- withTransforms: true,
+ withTransforms: false,
withFieldConfig: true,
},
});
diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx
index cc0470c1bf2..94d59e73eb4 100644
--- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx
+++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.tsx
@@ -1,6 +1,6 @@
import * as React from 'react';
-import { CoreApp, DataSourceApi, DataSourceInstanceSettings, IconName } from '@grafana/data';
+import { CoreApp, DataSourceApi, DataSourceInstanceSettings, IconName, getDataSourceRef } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, getDataSourceSrv } from '@grafana/runtime';
import { SceneObjectBase, SceneComponentProps, sceneGraph, SceneQueryRunner } from '@grafana/scenes';
@@ -81,8 +81,9 @@ export class PanelDataQueriesTab extends SceneObjectBase) => {
const queries = this.getQueries();
const dsSettings = this._panelManager.state.dsSettings;
- this.onQueriesChange(addQuery(queries, query, { type: dsSettings?.type, uid: dsSettings?.uid }));
+ this.onQueriesChange(
+ addQuery(queries, query, dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined })
+ );
};
isExpressionsSupported(dsSettings: DataSourceInstanceSettings): boolean {
diff --git a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx
index 40ae36a0958..33e36101985 100644
--- a/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx
+++ b/public/app/features/dashboard-scene/panel-edit/VizPanelManager.tsx
@@ -9,6 +9,7 @@ import {
GrafanaTheme2,
PanelModel,
filterFieldConfigOverrides,
+ getDataSourceRef,
isStandardFieldProp,
restoreCustomOverrideRules,
} from '@grafana/data';
@@ -159,8 +160,8 @@ export class VizPanelManager extends SceneObjectBase {
this.queryRunner.setState({
datasource: {
+ ...getDataSourceRef(dsSettings),
uid: lastUsedDatasource?.datasourceUid,
- type: dsSettings.type,
},
});
}
@@ -173,12 +174,7 @@ export class VizPanelManager extends SceneObjectBase {
if (datasource && dsSettings) {
this.setState({ datasource, dsSettings });
- storeLastUsedDataSourceInLocalStorage(
- {
- type: dsSettings.type,
- uid: dsSettings.uid,
- } || { default: true }
- );
+ storeLastUsedDataSourceInLocalStorage(getDataSourceRef(dsSettings) || { default: true });
}
} catch (err) {
//set default datasource if we fail to load the datasource
@@ -192,10 +188,7 @@ export class VizPanelManager extends SceneObjectBase {
});
this.queryRunner.setState({
- datasource: {
- uid: dsSettings.uid,
- type: dsSettings.type,
- },
+ datasource: getDataSourceRef(dsSettings),
});
}
@@ -296,10 +289,7 @@ export class VizPanelManager extends SceneObjectBase {
const queries = defaultQueries || (await updateQueries(nextDS, newSettings.uid, currentQueries, currentDS));
queryRunner.setState({
- datasource: {
- type: newSettings.type,
- uid: newSettings.uid,
- },
+ datasource: getDataSourceRef(newSettings),
queries,
});
if (defaultQueries) {
diff --git a/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx b/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx
index fc045edfa16..6a710357cdf 100644
--- a/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx
+++ b/public/app/features/dashboard-scene/scene/DashboardGridItem.test.tsx
@@ -1,6 +1,7 @@
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { setPluginImportUtils } from '@grafana/runtime';
-import { SceneGridLayout, VizPanel } from '@grafana/scenes';
+import { SceneGridLayout, TestVariable, VizPanel } from '@grafana/scenes';
+import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
import { activateFullSceneTree, buildPanelRepeaterScene } from '../utils/test-utils';
@@ -40,7 +41,7 @@ describe('PanelRepeaterGridItem', () => {
expect(repeater.state.repeatedPanels?.length).toBe(5);
});
- it('Should adjust container height to fit panels direction is horizontal', async () => {
+ it('Should adjust container height to fit panels if direction is horizontal', async () => {
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0, maxPerRow: 2, itemHeight: 10 });
const layoutForceRender = jest.fn();
@@ -144,4 +145,56 @@ describe('PanelRepeaterGridItem', () => {
expect(gridItem.getClassName()).toBe('');
});
+
+ it('should have correct height after repeat is performed', () => {
+ const { scene, repeater } = buildPanelRepeaterScene({
+ variableQueryTime: 0,
+ height: 4,
+ maxPerRow: 4,
+ repeatDirection: 'h',
+ numberOfOptions: 5,
+ });
+
+ activateFullSceneTree(scene);
+
+ expect(repeater.state.height).toBe(4);
+ });
+
+ it('should have same item height if number of repititions changes', async () => {
+ const { scene, repeater } = buildPanelRepeaterScene({
+ variableQueryTime: 0,
+ height: 4,
+ maxPerRow: 4,
+ repeatDirection: 'h',
+ numberOfOptions: 5,
+ });
+ activateFullSceneTree(scene);
+
+ scene.state.$variables!.setState({
+ variables: [
+ new TestVariable({
+ name: 'server',
+ query: 'A.*',
+ value: ALL_VARIABLE_VALUE,
+ text: ALL_VARIABLE_TEXT,
+ isMulti: true,
+ includeAll: true,
+ delayMs: 0,
+ optionsToReturn: [
+ { label: 'A', value: '1' },
+ { label: 'B', value: '2' },
+ { label: 'C', value: '3' },
+ { label: 'D', value: '4' },
+ { label: 'E', value: '5' },
+ { label: 'F', value: '6' },
+ { label: 'G', value: '7' },
+ { label: 'H', value: '8' },
+ { label: 'I', value: '9' },
+ { label: 'J', value: '10' },
+ ],
+ }),
+ ],
+ });
+ expect(repeater.state.height).toBe(6);
+ });
});
diff --git a/public/app/features/dashboard-scene/scene/DashboardGridItem.tsx b/public/app/features/dashboard-scene/scene/DashboardGridItem.tsx
index 0a48d944190..1c7e76adf45 100644
--- a/public/app/features/dashboard-scene/scene/DashboardGridItem.tsx
+++ b/public/app/features/dashboard-scene/scene/DashboardGridItem.tsx
@@ -163,6 +163,8 @@ export class DashboardGridItem extends SceneObjectBase i
return;
}
+ // Needed to calculate item height
+ const prevRepeatCount = this._prevRepeatValues?.length ?? values.length;
this._prevRepeatValues = values;
const panelToRepeat = this.state.body instanceof LibraryVizPanel ? this.state.body.state.panel! : this.state.body;
const repeatedPanels: VizPanel[] = [];
@@ -188,16 +190,15 @@ export class DashboardGridItem extends SceneObjectBase i
const direction = this.getRepeatDirection();
const stateChange: Partial = { repeatedPanels: repeatedPanels };
- const itemHeight = this.state.itemHeight ?? 10;
- const prevHeight = this.state.height;
- const maxPerRow = this.getMaxPerRow();
+ const prevHeight = this.state.height ?? 0;
+ const maxPerRow = direction === 'h' ? this.getMaxPerRow() : 1;
+ const prevRowCount = Math.ceil(prevRepeatCount / maxPerRow);
+ const newRowCount = Math.ceil(repeatedPanels.length / maxPerRow);
- if (direction === 'h') {
- const rowCount = Math.ceil(repeatedPanels.length / maxPerRow);
- stateChange.height = rowCount * itemHeight;
- } else {
- stateChange.height = repeatedPanels.length * itemHeight;
- }
+ // If item height is not defined, calculate based on total height and row count
+ const itemHeight = this.state.itemHeight ?? prevHeight / prevRowCount;
+ stateChange.itemHeight = itemHeight;
+ stateChange.height = Math.ceil(newRowCount * itemHeight);
this.setState(stateChange);
diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx
index 1f8a113de7e..9e2ca410700 100644
--- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx
+++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx
@@ -1,4 +1,4 @@
-import { CoreApp, LoadingState, getDefaultTimeRange } from '@grafana/data';
+import { CoreApp, LoadingState, getDefaultTimeRange, store } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import {
sceneGraph,
@@ -14,6 +14,7 @@ import {
} from '@grafana/scenes';
import { Dashboard, DashboardCursorSync, LibraryPanel } from '@grafana/schema';
import appEvents from 'app/core/app_events';
+import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { VariablesChanged } from 'app/features/variables/types';
@@ -25,7 +26,7 @@ import {
transformSaveModelToScene,
} from '../serialization/transformSaveModelToScene';
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
-import { getHistorySrv } from '../settings/version-history/HistorySrv';
+import { historySrv } from '../settings/version-history/HistorySrv';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { djb2Hash } from '../utils/djb2Hash';
import { findVizPanelByKey } from '../utils/utils';
@@ -620,7 +621,7 @@ describe('DashboardScene', () => {
scene.copyPanel(vizPanel);
- expect(scene.state.hasCopiedPanel).toBe(false);
+ expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false);
});
it('Should fail to copy a library panel if it does not have a grid item parent', () => {
@@ -638,14 +639,14 @@ describe('DashboardScene', () => {
scene.copyPanel(libVizPanel.state.panel as VizPanel);
- expect(scene.state.hasCopiedPanel).toBe(false);
+ expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false);
});
it('Should copy a panel', () => {
const vizPanel = ((scene.state.body as SceneGridLayout).state.children[0] as DashboardGridItem).state.body;
scene.copyPanel(vizPanel as VizPanel);
- expect(scene.state.hasCopiedPanel).toBe(true);
+ expect(store.exists(LS_PANEL_COPY_KEY)).toBe(true);
});
it('Should copy a library viz panel', () => {
@@ -654,11 +655,11 @@ describe('DashboardScene', () => {
scene.copyPanel(libVizPanel.state.panel as VizPanel);
- expect(scene.state.hasCopiedPanel).toBe(true);
+ expect(store.exists(LS_PANEL_COPY_KEY)).toBe(true);
});
it('Should paste a panel', () => {
- scene.setState({ hasCopiedPanel: true });
+ store.set(LS_PANEL_COPY_KEY, JSON.stringify({ key: 'panel-7' }));
jest.spyOn(JSON, 'parse').mockReturnThis();
jest.mocked(buildGridItemForPanel).mockReturnValue(
new DashboardGridItem({
@@ -680,11 +681,11 @@ describe('DashboardScene', () => {
expect(body.state.children.length).toBe(6);
expect(gridItem.state.body!.state.key).toBe('panel-7');
expect(gridItem.state.y).toBe(0);
- expect(scene.state.hasCopiedPanel).toBe(false);
+ expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false);
});
it('Should paste a library viz panel', () => {
- scene.setState({ hasCopiedPanel: true });
+ store.set(LS_PANEL_COPY_KEY, JSON.stringify({ key: 'panel-7' }));
jest.spyOn(JSON, 'parse').mockReturnValue({ libraryPanel: { uid: 'uid', name: 'libraryPanel' } });
jest.mocked(buildGridItemForLibPanel).mockReturnValue(
new DashboardGridItem({
@@ -709,7 +710,7 @@ describe('DashboardScene', () => {
expect(libVizPanel.state.panelKey).toBe('panel-7');
expect(libVizPanel.state.panel?.state.key).toBe('panel-7');
expect(gridItem.state.y).toBe(0);
- expect(scene.state.hasCopiedPanel).toBe(false);
+ expect(store.exists(LS_PANEL_COPY_KEY)).toBe(false);
});
it('Should remove a panel', () => {
@@ -1137,7 +1138,7 @@ describe('DashboardScene', () => {
version: 4,
});
- jest.mocked(getHistorySrv().restoreDashboard).mockResolvedValue({ version: newVersion });
+ jest.mocked(historySrv.restoreDashboard).mockResolvedValue({ version: newVersion });
jest.mocked(transformSaveModelToScene).mockReturnValue(mockScene);
return scene.onRestore(getVersionMock()).then((res) => {
@@ -1150,7 +1151,7 @@ describe('DashboardScene', () => {
it('should return early if historySrv does not return a valid version number', () => {
jest
- .mocked(getHistorySrv().restoreDashboard)
+ .mocked(historySrv.restoreDashboard)
.mockResolvedValueOnce({ version: null })
.mockResolvedValueOnce({ version: undefined })
.mockResolvedValueOnce({ version: Infinity })
diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx
index 1b5e2691591..8c66b347edd 100644
--- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx
+++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx
@@ -49,7 +49,7 @@ import {
import { gridItemToPanel } from '../serialization/transformSceneToSaveModel';
import { DecoratedRevisionModel } from '../settings/VersionsEditView';
import { DashboardEditView } from '../settings/utils';
-import { getHistorySrv } from '../settings/version-history';
+import { historySrv } from '../settings/version-history';
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
import { dashboardSceneGraph, getLibraryVizPanelFromVizPanel } from '../utils/dashboardSceneGraph';
import { djb2Hash } from '../utils/djb2Hash';
@@ -121,8 +121,6 @@ export interface DashboardSceneState extends SceneObjectState {
editPanel?: PanelEditor;
/** Scene object that handles the current drawer or modal */
overlay?: SceneObject;
- /** True when a user copies a panel in the dashboard */
- hasCopiedPanel?: boolean;
/** The dashboard doesn't have panels */
isEmpty?: boolean;
/** Scene object that handles the scopes selector */
@@ -172,7 +170,6 @@ export class DashboardScene extends SceneObjectBase {
editable: true,
body: state.body ?? new SceneFlexLayout({ children: [] }),
links: state.links ?? [],
- hasCopiedPanel: store.exists(LS_PANEL_COPY_KEY),
scopes: state.uid && config.featureToggles.scopeFilters ? new ScopesScene() : undefined,
...state,
});
@@ -357,20 +354,19 @@ export class DashboardScene extends SceneObjectBase {
}
public onRestore = async (version: DecoratedRevisionModel): Promise => {
- const versionRsp = await getHistorySrv().restoreDashboard(version.uid, version.version);
+ const versionRsp = await historySrv.restoreDashboard(version.uid, version.version);
- const rev = (versionRsp as SaveDashboardResponseDTO).version;
- if (!Number.isInteger(version)) {
+ if (!Number.isInteger(versionRsp.version)) {
return false;
}
const dashboardDTO: DashboardDTO = {
- dashboard: new DashboardModel(version.data!),
+ dashboard: new DashboardModel(version.data),
meta: this.state.meta,
};
const dashScene = transformSaveModelToScene(dashboardDTO);
const newState = sceneUtils.cloneSceneObjectState(dashScene.state);
- newState.version = rev;
+ newState.version = versionRsp.version;
this.setState(newState);
this.exitEditMode({ skipConfirm: true, restoreInitialState: false });
@@ -649,7 +645,6 @@ export class DashboardScene extends SceneObjectBase {
store.set(LS_PANEL_COPY_KEY, JSON.stringify(jsonData));
appEvents.emit(AppEvents.alertSuccess, ['Panel copied. Use **Paste panel** toolbar action to paste.']);
- this.setState({ hasCopiedPanel: true });
}
public pastePanel() {
@@ -704,7 +699,6 @@ export class DashboardScene extends SceneObjectBase {
children: [gridItem, ...sceneGridLayout.state.children],
});
- this.setState({ hasCopiedPanel: false });
store.delete(LS_PANEL_COPY_KEY);
}
diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
index e851c1ea4cc..c40f3e08697 100644
--- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
+++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { useEffect, useId, useState } from 'react';
import * as React from 'react';
-import { GrafanaTheme2 } from '@grafana/data';
+import { GrafanaTheme2, store } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, locationService } from '@grafana/runtime';
import {
@@ -18,6 +18,7 @@ import {
} from '@grafana/ui';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
+import { LS_PANEL_COPY_KEY } from 'app/core/constants';
import { contextSrv } from 'app/core/core';
import { Trans, t } from 'app/core/internationalization';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
@@ -51,17 +52,7 @@ NavToolbarActions.displayName = 'NavToolbarActions';
* This part is split into a separate component to help test this
*/
export function ToolbarActions({ dashboard }: Props) {
- const {
- isEditing,
- viewPanelScene,
- isDirty,
- uid,
- meta,
- editview,
- editPanel,
- editable,
- hasCopiedPanel: copiedPanel,
- } = dashboard.useState();
+ const { isEditing, viewPanelScene, isDirty, uid, meta, editview, editPanel, editable } = dashboard.useState();
const { isPlaying } = playlistSrv.useState();
const [isAddPanelMenuOpen, setIsAddPanelMenuOpen] = useState(false);
@@ -72,7 +63,7 @@ export function ToolbarActions({ dashboard }: Props) {
const isViewingPanel = Boolean(viewPanelScene);
const isEditedPanelDirty = useVizManagerDirty(editPanel);
const isEditingLibraryPanel = useEditingLibraryPanel(editPanel);
- const hasCopiedPanel = Boolean(copiedPanel);
+ const hasCopiedPanel = store.exists(LS_PANEL_COPY_KEY);
// Means we are not in settings view, fullscreen panel or edit panel
const isShowingDashboard = !editview && !isViewingPanel && !isEditingPanel;
const isEditingAndShowingDashboard = isEditing && isShowingDashboard;
diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx
index ec36dafc8e2..eb6d5fb73e5 100644
--- a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx
+++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.test.tsx
@@ -94,6 +94,24 @@ describe('RowRepeaterBehavior', () => {
});
});
+ describe('Should not repeat row', () => {
+ it('Should ignore repeat process if the variable is not a multi select variable', async () => {
+ const { scene, grid, repeatBehavior } = buildScene({ variableQueryTime: 0 }, undefined, { isMulti: false });
+ const gridStateUpdates = [];
+ grid.subscribeToState((state) => gridStateUpdates.push(state));
+
+ activateFullSceneTree(scene);
+ await new Promise((r) => setTimeout(r, 1));
+
+ // trigger another repeat cycle by changing the variable
+ repeatBehavior.performRepeat();
+
+ await new Promise((r) => setTimeout(r, 1));
+
+ expect(gridStateUpdates.length).toBe(0);
+ });
+ });
+
describe('Given scene empty row', () => {
let scene: DashboardScene;
let grid: SceneGridLayout;
@@ -139,7 +157,11 @@ interface SceneOptions {
repeatDirection?: RepeatDirection;
}
-function buildScene(options: SceneOptions, variableOptions?: VariableValueOption[]) {
+function buildScene(
+ options: SceneOptions,
+ variableOptions?: VariableValueOption[],
+ variableStateOverrides?: { isMulti: boolean }
+) {
const repeatBehavior = new RowRepeaterBehavior({ variableName: 'server' });
const grid = new SceneGridLayout({
@@ -223,6 +245,7 @@ function buildScene(options: SceneOptions, variableOptions?: VariableValueOption
{ label: 'D', value: 'D1' },
{ label: 'E', value: 'E1' },
],
+ ...variableStateOverrides,
}),
],
}),
diff --git a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts
index 5ef6c6137ec..9b96f31c1d6 100644
--- a/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts
+++ b/public/app/features/dashboard-scene/scene/RowRepeaterBehavior.ts
@@ -77,7 +77,12 @@ export class RowRepeaterBehavior extends SceneObjectBase
currentNode.isExpanded = isExpanded;
currentNode.query = query;
- this.setState({ nodes, loadingNodeName: undefined });
-
if (isExpanded || isDifferentQuery) {
- this.setState({ loadingNodeName: name });
+ this.setState({ nodes, loadingNodeName: name });
this.nodesFetchingSub = from(fetchNodes(name, query))
.pipe(
@@ -138,6 +136,8 @@ export class ScopesFiltersScene extends SceneObjectBase
this.nodesFetchingSub?.unsubscribe();
});
+ } else {
+ this.setState({ nodes, loadingNodeName: undefined });
}
}
diff --git a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx
index cc9f4a61d8c..e0962914f71 100644
--- a/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx
+++ b/public/app/features/dashboard-scene/scene/Scopes/ScopesTreeSearch.tsx
@@ -1,6 +1,6 @@
import { css } from '@emotion/css';
-import { debounce } from 'lodash';
-import { useEffect, useMemo, useState } from 'react';
+import { useEffect, useState } from 'react';
+import { useDebounce } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { FilterInput, useStyles2 } from '@grafana/ui';
@@ -18,13 +18,23 @@ export interface ScopesTreeSearchProps {
export function ScopesTreeSearch({ anyChildExpanded, nodePath, query, onNodeUpdate }: ScopesTreeSearchProps) {
const styles = useStyles2(getStyles);
- const [queryValue, setQueryValue] = useState(query);
+ const [inputState, setInputState] = useState<{ value: string; isDirty: boolean }>({ value: query, isDirty: false });
useEffect(() => {
- setQueryValue(query);
- }, [query]);
+ if (!inputState.isDirty && inputState.value !== query) {
+ setInputState({ value: query, isDirty: false });
+ }
+ }, [inputState, query]);
- const onQueryUpdate = useMemo(() => debounce(onNodeUpdate, 500), [onNodeUpdate]);
+ useDebounce(
+ () => {
+ if (inputState.isDirty) {
+ onNodeUpdate(nodePath, true, inputState.value);
+ }
+ },
+ 500,
+ [inputState.isDirty, inputState.value]
+ );
if (anyChildExpanded) {
return null;
@@ -33,12 +43,11 @@ export function ScopesTreeSearch({ anyChildExpanded, nodePath, query, onNodeUpda
return (
{
- setQueryValue(value);
- onQueryUpdate(nodePath, true, value);
+ setInputState({ value, isDirty: true });
}}
/>
);
diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
index 1e11bc51bde..7f312c60208 100644
--- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
+++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
@@ -438,7 +438,6 @@ export function buildGridItemForLibPanel(panel: PanelModel) {
x: panel.gridPos.x,
width: panel.gridPos.w,
height: panel.gridPos.h,
- itemHeight: panel.gridPos.h,
body,
});
}
@@ -503,7 +502,6 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
y: panel.gridPos.y,
width: repeatOptions.repeatDirection === 'h' ? 24 : panel.gridPos.w,
height: panel.gridPos.h,
- itemHeight: panel.gridPos.h,
body,
maxPerRow: panel.maxPerRow,
...repeatOptions,
diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx
index b1256e997b5..f9024d2b8ed 100644
--- a/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx
+++ b/public/app/features/dashboard-scene/settings/VersionsEditView.test.tsx
@@ -4,7 +4,7 @@ import { DashboardScene } from '../scene/DashboardScene';
import { activateFullSceneTree } from '../utils/test-utils';
import { VERSIONS_FETCH_LIMIT, VersionsEditView } from './VersionsEditView';
-import { getHistorySrv } from './version-history';
+import { historySrv } from './version-history';
jest.mock('./version-history/HistorySrv');
@@ -20,7 +20,7 @@ describe('VersionsEditView', () => {
} as unknown as React.FormEvent;
beforeEach(async () => {
- jest.mocked(getHistorySrv().getHistoryList).mockResolvedValue(getVersions());
+ jest.mocked(historySrv.getHistoryList).mockResolvedValue(getVersions());
const result = await buildTestScene();
dashboard = result.dashboard;
@@ -81,7 +81,7 @@ describe('VersionsEditView', () => {
versionsView.onCheck(mockEvent, 4);
jest
- .mocked(getHistorySrv().getDashboardVersion)
+ .mocked(historySrv.getDashboardVersion)
.mockResolvedValueOnce({ data: 'lhs' })
.mockResolvedValue({ data: 'rhs' });
@@ -100,7 +100,7 @@ describe('VersionsEditView', () => {
versionsView.onCheck(mockEvent, 2);
jest
- .mocked(getHistorySrv().getDashboardVersion)
+ .mocked(historySrv.getDashboardVersion)
.mockResolvedValueOnce({ data: 'lhs' })
.mockResolvedValue({ data: 'rhs' });
diff --git a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx
index 3ed616d8095..c2acbb9b7af 100644
--- a/public/app/features/dashboard-scene/settings/VersionsEditView.tsx
+++ b/public/app/features/dashboard-scene/settings/VersionsEditView.tsx
@@ -11,15 +11,21 @@ import { getDashboardSceneFor } from '../utils/utils';
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
import {
+ RevisionsModel,
VersionHistoryComparison,
VersionHistoryHeader,
VersionHistoryTable,
VersionsHistoryButtons,
+ historySrv,
} from './version-history';
-import { DecoratedRevisionModel, VersionModel, getHistorySrv } from './version-history/HistorySrv';
export const VERSIONS_FETCH_LIMIT = 10;
+export type DecoratedRevisionModel = RevisionsModel & {
+ createdDateString: string;
+ ageString: string;
+};
+
export interface VersionsEditViewState extends DashboardEditViewState {
versions?: DecoratedRevisionModel[];
isLoading?: boolean;
@@ -96,7 +102,7 @@ export class VersionsEditView extends SceneObjectBase imp
this.setState({ isAppending: append });
- getHistorySrv()
+ historySrv
.getHistoryList(uid, { limit: this._limit, start: this._start })
.then((result) => {
this.setState({
@@ -122,8 +128,8 @@ export class VersionsEditView extends SceneObjectBase imp
return;
}
- const lhs = await getHistorySrv().getDashboardVersion(this._dashboard.state.uid, baseInfo.version);
- const rhs = await getHistorySrv().getDashboardVersion(this._dashboard.state.uid, newInfo.version);
+ const lhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, baseInfo.version);
+ const rhs = await historySrv.getDashboardVersion(this._dashboard.state.uid, newInfo.version);
this.setState({
baseInfo,
@@ -132,8 +138,8 @@ export class VersionsEditView extends SceneObjectBase imp
newInfo,
viewMode: 'compare',
diffData: {
- lhs: JSON.stringify(lhs),
- rhs: JSON.stringify(rhs),
+ lhs: lhs.data,
+ rhs: rhs.data,
},
});
};
@@ -152,15 +158,15 @@ export class VersionsEditView extends SceneObjectBase imp
});
};
- public onCheck = (ev: React.FormEvent, versionId: number | string) => {
+ public onCheck = (ev: React.FormEvent, versionId: number) => {
this.setState({
versions: this.versions.map((version) =>
- version.version === versionId ? { ...version, checked: ev.currentTarget.checked } : version
+ version.id === versionId ? { ...version, checked: ev.currentTarget.checked } : version
),
});
};
- private decorateVersions(versions: VersionModel[]): DecoratedRevisionModel[] {
+ private decorateVersions(versions: RevisionsModel[]): DecoratedRevisionModel[] {
const timeZone = this.getTimeRange().getTimeZone();
return versions.map((version) => {
diff --git a/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx
index dc03d1b347b..5e5ff504462 100644
--- a/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx
+++ b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx
@@ -1,9 +1,8 @@
import { useAsync } from 'react-use';
-import { DataSourceInstanceSettings, MetricFindValue } from '@grafana/data';
+import { DataSourceInstanceSettings, MetricFindValue, getDataSourceRef } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { AdHocFiltersVariable } from '@grafana/scenes';
-import { DataSourceRef } from '@grafana/schema';
import { AdHocVariableForm } from '../components/AdHocVariableForm';
@@ -25,10 +24,7 @@ export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProp
: 'This data source does not support ad hoc filters yet.';
const onDataSourceChange = (ds: DataSourceInstanceSettings) => {
- const dsRef: DataSourceRef = {
- uid: ds.uid,
- type: ds.type,
- };
+ const dsRef = getDataSourceRef(ds);
variable.setState({
datasource: dsRef,
diff --git a/public/app/features/dashboard-scene/settings/variables/editors/GroupByVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/GroupByVariableEditor.tsx
index 808dee6dad2..2663ab7c47c 100644
--- a/public/app/features/dashboard-scene/settings/variables/editors/GroupByVariableEditor.tsx
+++ b/public/app/features/dashboard-scene/settings/variables/editors/GroupByVariableEditor.tsx
@@ -1,6 +1,6 @@
import { useAsync } from 'react-use';
-import { DataSourceInstanceSettings, DataSourceRef, MetricFindValue } from '@grafana/data';
+import { DataSourceInstanceSettings, MetricFindValue, getDataSourceRef } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
import { GroupByVariable } from '@grafana/scenes';
@@ -24,10 +24,7 @@ export function GroupByVariableEditor(props: GroupByVariableEditorProps) {
: 'This data source does not support group by variable yet.';
const onDataSourceChange = async (ds: DataSourceInstanceSettings) => {
- const dsRef: DataSourceRef = {
- uid: ds.uid,
- type: ds.type,
- };
+ const dsRef = getDataSourceRef(ds);
variable.setState({ datasource: dsRef });
onRunQuery();
diff --git a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx
index 539290d26e9..05ea0873e91 100644
--- a/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx
+++ b/public/app/features/dashboard-scene/settings/variables/editors/QueryVariableEditor.tsx
@@ -1,9 +1,9 @@
import { FormEvent } from 'react';
import * as React from 'react';
-import { SelectableValue, DataSourceInstanceSettings } from '@grafana/data';
+import { SelectableValue, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data';
import { QueryVariable, sceneGraph } from '@grafana/scenes';
-import { DataSourceRef, VariableRefresh, VariableSort } from '@grafana/schema';
+import { VariableRefresh, VariableSort } from '@grafana/schema';
import { QueryVariableEditorForm } from '../components/QueryVariableForm';
@@ -36,7 +36,7 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito
variable.setState({ allValue: event.currentTarget.value });
};
const onDataSourceChange = (dsInstanceSettings: DataSourceInstanceSettings) => {
- const datasource: DataSourceRef = { uid: dsInstanceSettings.uid, type: dsInstanceSettings.type };
+ const datasource = getDataSourceRef(dsInstanceSettings);
if (variable.state.datasource && variable.state.datasource.type !== datasource.type) {
variable.setState({ datasource, query: '', definition: '' });
diff --git a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts
index e7bc09af7d9..c2caee31a0e 100644
--- a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts
+++ b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.test.ts
@@ -1,6 +1,6 @@
import { createDashboardModelFixture } from 'app/features/dashboard/state/__fixtures__/dashboardFixtures';
-import { getHistorySrv } from './HistorySrv';
+import { HistorySrv } from './HistorySrv';
import { restore, versions } from './__mocks__/dashboardHistoryMocks';
const getMock = jest.fn().mockResolvedValue({});
@@ -27,7 +27,7 @@ describe('historySrv', () => {
const versionsResponse = versions();
const restoreResponse = restore;
- let historySrv = getHistorySrv();
+ let historySrv = new HistorySrv();
const dash = createDashboardModelFixture({ uid: '_U4zObQMz' });
const emptyDash = createDashboardModelFixture();
@@ -40,7 +40,7 @@ describe('historySrv', () => {
describe('getHistoryList', () => {
it('should return a versions array for the given dashboard id', () => {
getMock.mockImplementation(() => Promise.resolve(versionsResponse));
- historySrv = getHistorySrv();
+ historySrv = new HistorySrv();
return historySrv.getHistoryList(dash.uid, historyListOpts).then((versions) => {
expect(versions).toEqual(versionsResponse);
@@ -63,7 +63,7 @@ describe('historySrv', () => {
describe('getDashboardVersion', () => {
it('should return a version object for the given dashboard id and version', () => {
getMock.mockImplementation(() => Promise.resolve(versionsResponse[0]));
- historySrv = getHistorySrv();
+ historySrv = new HistorySrv();
return historySrv.getDashboardVersion(dash.uid, 4).then((version) => {
expect(version).toEqual(versionsResponse[0]);
@@ -71,7 +71,7 @@ describe('historySrv', () => {
});
it('should return an empty object when not given an id', async () => {
- historySrv = getHistorySrv();
+ historySrv = new HistorySrv();
const rsp = await historySrv.getDashboardVersion(emptyDash.uid, 6);
expect(rsp).toEqual({});
@@ -82,14 +82,14 @@ describe('historySrv', () => {
it('should return a success response given valid parameters', () => {
const version = 6;
postMock.mockImplementation(() => Promise.resolve(restoreResponse(version)));
- historySrv = getHistorySrv();
+ historySrv = new HistorySrv();
return historySrv.restoreDashboard(dash.uid, version).then((response) => {
expect(response).toEqual(restoreResponse(version));
});
});
it('should return an empty object when not given an id', async () => {
- historySrv = getHistorySrv();
+ historySrv = new HistorySrv();
const rsp = await historySrv.restoreDashboard(emptyDash.uid, 6);
expect(rsp).toEqual({});
});
diff --git a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.ts b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.ts
index 1721a2dcb5d..61312b8ad45 100644
--- a/public/app/features/dashboard-scene/settings/version-history/HistorySrv.ts
+++ b/public/app/features/dashboard-scene/settings/version-history/HistorySrv.ts
@@ -1,56 +1,43 @@
import { getBackendSrv } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
-import { SaveDashboardResponseDTO } from 'app/types';
export interface HistoryListOpts {
limit: number;
start: number;
}
-// The raw version returned from api
-export interface VersionModel {
+export interface RevisionsModel {
+ id: number;
+ checked: boolean;
uid: string;
- version: number; // resourceVersion in k8s must be numeric
- created: string;
+ parentVersion: number;
+ version: number;
+ created: Date;
createdBy: string;
message: string;
+ data: Dashboard;
}
-// The version used in UI components
-export type DecoratedRevisionModel = VersionModel & {
- checked: boolean;
- createdDateString: string;
- ageString: string;
- data?: Dashboard;
-};
-
-export interface HistorySrv {
- getHistoryList(dashboardUID: string, options: HistoryListOpts): Promise;
- getDashboardVersion(dashboardUID: string, version: number | string): Promise; // Just the spec (for now)
- restoreDashboard(dashboardUID: string, version: number | string): Promise;
-}
-
-class LegacyHistorySrv implements HistorySrv {
+export class HistorySrv {
getHistoryList(dashboardUID: string, options: HistoryListOpts) {
if (typeof dashboardUID !== 'string') {
return Promise.resolve([]);
}
- return getBackendSrv().get(`api/dashboards/uid/${dashboardUID}/versions`, options);
+ return getBackendSrv().get(`api/dashboards/uid/${dashboardUID}/versions`, options);
}
- async getDashboardVersion(dashboardUID: string, version: number): Promise {
+ getDashboardVersion(dashboardUID: string, version: number) {
if (typeof dashboardUID !== 'string') {
return Promise.resolve({});
}
- const info = await getBackendSrv().get(`api/dashboards/uid/${dashboardUID}/versions/${version}`);
- return info.data; // the dashboard body
+ return getBackendSrv().get(`api/dashboards/uid/${dashboardUID}/versions/${version}`);
}
- restoreDashboard(dashboardUID: string, version: number): Promise {
+ restoreDashboard(dashboardUID: string, version: number) {
if (typeof dashboardUID !== 'string') {
- return Promise.resolve({} as unknown as SaveDashboardResponseDTO);
+ return Promise.resolve({});
}
const url = `api/dashboards/uid/${dashboardUID}/restore`;
@@ -59,11 +46,5 @@ class LegacyHistorySrv implements HistorySrv {
}
}
-let historySrv: HistorySrv | undefined = undefined;
-
-export function getHistorySrv(): HistorySrv {
- if (!historySrv) {
- historySrv = new LegacyHistorySrv();
- }
- return historySrv;
-}
+const historySrv = new HistorySrv();
+export { historySrv };
diff --git a/public/app/features/dashboard-scene/settings/version-history/RevertDashboardModal.tsx b/public/app/features/dashboard-scene/settings/version-history/RevertDashboardModal.tsx
index 87d96754e27..c3e44c132d1 100644
--- a/public/app/features/dashboard-scene/settings/version-history/RevertDashboardModal.tsx
+++ b/public/app/features/dashboard-scene/settings/version-history/RevertDashboardModal.tsx
@@ -1,7 +1,7 @@
import { ConfirmModal } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
-import { DecoratedRevisionModel } from './HistorySrv';
+import { DecoratedRevisionModel } from '../VersionsEditView';
export interface RevertDashboardModalProps {
hideModal: () => void;
diff --git a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryHeader.tsx b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryHeader.tsx
index 73a7e1f6af6..05987ee2838 100644
--- a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryHeader.tsx
+++ b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryHeader.tsx
@@ -6,8 +6,8 @@ import { Icon, IconButton, useStyles2 } from '@grafana/ui';
type VersionHistoryHeaderProps = {
onClick?: () => void;
- baseVersion?: number | string;
- newVersion?: number | string;
+ baseVersion?: number;
+ newVersion?: number;
isNewLatest?: boolean;
};
diff --git a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx
index 264c7c1e313..5ae97bc63e7 100644
--- a/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx
+++ b/public/app/features/dashboard-scene/settings/version-history/VersionHistoryTable.tsx
@@ -4,13 +4,14 @@ import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Checkbox, Button, Tag, ModalsController, useStyles2 } from '@grafana/ui';
-import { DecoratedRevisionModel } from './HistorySrv';
+import { DecoratedRevisionModel } from '../VersionsEditView';
+
import { RevertDashboardModal } from './RevertDashboardModal';
type VersionsTableProps = {
versions: DecoratedRevisionModel[];
canCompare: boolean;
- onCheck: (ev: React.FormEvent, versionId: number | string) => void;
+ onCheck: (ev: React.FormEvent, versionId: number) => void;
onRestore: (version: DecoratedRevisionModel) => Promise;
};
@@ -32,7 +33,7 @@ export const VersionHistoryTable = ({ versions, canCompare, onCheck, onRestore }
{versions.map((version, idx) => (
-
+
onCheck(ev, version.version)}
+ onChange={(ev) => onCheck(ev, version.id)}
disabled={!version.checked && canCompare}
/>
diff --git a/public/app/features/dashboard-scene/settings/version-history/index.ts b/public/app/features/dashboard-scene/settings/version-history/index.ts
index b9c4201ba43..c87d2d0b9b7 100644
--- a/public/app/features/dashboard-scene/settings/version-history/index.ts
+++ b/public/app/features/dashboard-scene/settings/version-history/index.ts
@@ -1,4 +1,4 @@
-export { HistorySrv, getHistorySrv } from './HistorySrv';
+export { HistorySrv, historySrv, RevisionsModel } from './HistorySrv';
export { VersionHistoryTable } from './VersionHistoryTable';
export { VersionHistoryHeader } from './VersionHistoryHeader';
export { VersionsHistoryButtons } from './VersionHistoryButtons';
diff --git a/public/app/features/dashboard-scene/utils/test-utils.ts b/public/app/features/dashboard-scene/utils/test-utils.ts
index 235dae85035..b8f284622d0 100644
--- a/public/app/features/dashboard-scene/utils/test-utils.ts
+++ b/public/app/features/dashboard-scene/utils/test-utils.ts
@@ -96,6 +96,7 @@ interface SceneOptions {
variableQueryTime: number;
maxPerRow?: number;
itemHeight?: number;
+ height?: number;
repeatDirection?: RepeatDirection;
x?: number;
y?: number;
@@ -113,6 +114,7 @@ export function buildPanelRepeaterScene(options: SceneOptions, source?: VizPanel
repeatDirection: options.repeatDirection,
maxPerRow: options.maxPerRow,
itemHeight: options.itemHeight,
+ height: options.height,
body:
source ??
new VizPanel({
diff --git a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx
index 0ee9b2809ba..77983e110fb 100644
--- a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx
+++ b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.test.tsx
@@ -5,7 +5,7 @@ import { BrowserRouter } from 'react-router-dom';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { GrafanaContext } from 'app/core/context/GrafanaContext';
-import { getHistorySrv } from 'app/features/dashboard-scene/settings/version-history/HistorySrv';
+import { historySrv } from 'app/features/dashboard-scene/settings/version-history/HistorySrv';
import { configureStore } from '../../../../store/configureStore';
import { createDashboardModelFixture } from '../../state/__fixtures__/dashboardFixtures';
@@ -135,8 +135,8 @@ describe('VersionSettings', () => {
});
test('clicking show more appends results to the table', async () => {
- getHistorySrv()
- .getHistoryList // @ts-ignore
+ historySrv.getHistoryList
+ // @ts-ignore
.mockImplementationOnce(() => Promise.resolve(versions.slice(0, VERSIONS_FETCH_LIMIT)))
.mockImplementationOnce(
() => new Promise((resolve) => setTimeout(() => resolve(versions.slice(VERSIONS_FETCH_LIMIT)), 1000))
@@ -144,7 +144,7 @@ describe('VersionSettings', () => {
setup();
- expect(getHistorySrv().getHistoryList).toBeCalledTimes(1);
+ expect(historySrv.getHistoryList).toBeCalledTimes(1);
await waitFor(() => expect(screen.getByRole('table')).toBeInTheDocument());
@@ -153,7 +153,7 @@ describe('VersionSettings', () => {
const showMoreButton = screen.getByRole('button', { name: /show more versions/i });
await user.click(showMoreButton);
- expect(getHistorySrv().getHistoryList).toBeCalledTimes(2);
+ expect(historySrv.getHistoryList).toBeCalledTimes(2);
expect(screen.getByText(/Fetching more entries/i)).toBeInTheDocument();
jest.advanceTimersByTime(1000);
@@ -166,14 +166,14 @@ describe('VersionSettings', () => {
test('selecting two versions and clicking compare button should render compare view', async () => {
// @ts-ignore
historySrv.getHistoryList.mockResolvedValue(versions.slice(0, VERSIONS_FETCH_LIMIT));
- getHistorySrv()
- .getDashboardVersion // @ts-ignore
+ historySrv.getDashboardVersion
+ // @ts-ignore
.mockImplementationOnce(() => Promise.resolve(diffs.lhs))
.mockImplementationOnce(() => Promise.resolve(diffs.rhs));
setup();
- expect(getHistorySrv().getHistoryList).toBeCalledTimes(1);
+ expect(historySrv.getHistoryList).toBeCalledTimes(1);
await waitFor(() => expect(screen.getByRole('table')).toBeInTheDocument());
diff --git a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx
index c61c131902d..c22f31c9437 100644
--- a/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx
+++ b/public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx
@@ -3,12 +3,12 @@ import * as React from 'react';
import { Spinner, HorizontalGroup } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
-import { VersionHistoryHeader, VersionsHistoryButtons } from 'app/features/dashboard-scene/settings/version-history';
import {
- DecoratedRevisionModel,
- VersionModel,
- getHistorySrv,
-} from 'app/features/dashboard-scene/settings/version-history/HistorySrv';
+ historySrv,
+ RevisionsModel,
+ VersionHistoryHeader,
+ VersionsHistoryButtons,
+} from 'app/features/dashboard-scene/settings/version-history';
import { VersionHistoryComparison } from '../VersionHistory/VersionHistoryComparison';
import { VersionHistoryTable } from '../VersionHistory/VersionHistoryTable';
@@ -28,6 +28,11 @@ type State = {
isNewLatest: boolean;
};
+export type DecoratedRevisionModel = RevisionsModel & {
+ createdDateString: string;
+ ageString: string;
+};
+
export const VERSIONS_FETCH_LIMIT = 10;
export class VersionsSettings extends PureComponent {
@@ -57,7 +62,7 @@ export class VersionsSettings extends PureComponent {
getVersions = (append = false) => {
this.setState({ isAppending: append });
- getHistorySrv()
+ historySrv
.getHistoryList(this.props.dashboard.uid, { limit: this.limit, start: this.start })
.then((res) => {
this.setState({
@@ -79,8 +84,8 @@ export class VersionsSettings extends PureComponent {
isLoading: true,
});
- const lhs = await getHistorySrv().getDashboardVersion(this.props.dashboard.uid, baseInfo.version);
- const rhs = await getHistorySrv().getDashboardVersion(this.props.dashboard.uid, newInfo.version);
+ const lhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, baseInfo.version);
+ const rhs = await historySrv.getDashboardVersion(this.props.dashboard.uid, newInfo.version);
this.setState({
baseInfo,
@@ -89,13 +94,13 @@ export class VersionsSettings extends PureComponent {
newInfo,
viewMode: 'compare',
diffData: {
- lhs: JSON.stringify(lhs),
- rhs: JSON.stringify(rhs),
+ lhs: lhs.data,
+ rhs: rhs.data,
},
});
};
- decorateVersions = (versions: VersionModel[]) =>
+ decorateVersions = (versions: RevisionsModel[]) =>
versions.map((version) => ({
...version,
createdDateString: this.props.dashboard.formatDate(version.created),
@@ -110,7 +115,7 @@ export class VersionsSettings extends PureComponent {
onCheck = (ev: React.FormEvent, versionId: number) => {
this.setState({
versions: this.state.versions.map((version) =>
- version.version === versionId ? { ...version, checked: ev.currentTarget.checked } : version
+ version.id === versionId ? { ...version, checked: ev.currentTarget.checked } : version
),
});
};
diff --git a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx
index 7f87d410e20..0c81aafeea8 100644
--- a/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx
+++ b/public/app/features/dashboard/components/PanelEditor/PanelEditorQueries.tsx
@@ -40,8 +40,7 @@ export class PanelEditorQueries extends PureComponent {
cacheTimeout: datasourceSettings?.meta.queryOptions?.cacheTimeout ? panel.cacheTimeout : undefined,
dataSource: {
default: datasourceSettings?.isDefault,
- type: datasourceSettings?.type,
- uid: datasourceSettings?.uid,
+ ...(datasourceSettings ? getDataSourceRef(datasourceSettings) : { type: undefined, uid: undefined }),
},
queryCachingTTL: datasourceSettings?.cachingConfig?.enabled ? panel.queryCachingTTL : undefined,
queries: panel.targets,
diff --git a/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx b/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx
index f44443a53f4..2f23cd32e17 100644
--- a/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx
+++ b/public/app/features/dashboard/components/VersionHistory/VersionHistoryComparison.tsx
@@ -4,9 +4,10 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Button, ModalsController, CollapsableSection, useStyles2, Stack, Icon, Box } from '@grafana/ui';
import { DiffGroup } from 'app/features/dashboard-scene/settings/version-history/DiffGroup';
import { DiffViewer } from 'app/features/dashboard-scene/settings/version-history/DiffViewer';
-import { DecoratedRevisionModel } from 'app/features/dashboard-scene/settings/version-history/HistorySrv';
import { jsonDiff } from 'app/features/dashboard-scene/settings/version-history/utils';
+import { DecoratedRevisionModel } from '../DashboardSettings/VersionsSettings';
+
import { RevertDashboardModal } from './RevertDashboardModal';
type DiffViewProps = {
diff --git a/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx b/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx
index 504db7cbb35..2cd8514992a 100644
--- a/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx
+++ b/public/app/features/dashboard/components/VersionHistory/VersionHistoryTable.tsx
@@ -3,7 +3,8 @@ import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Checkbox, Button, Tag, ModalsController, useStyles2 } from '@grafana/ui';
-import { DecoratedRevisionModel } from 'app/features/dashboard-scene/settings/version-history/HistorySrv';
+
+import { DecoratedRevisionModel } from '../DashboardSettings/VersionsSettings';
import { RevertDashboardModal } from './RevertDashboardModal';
@@ -31,7 +32,7 @@ export const VersionHistoryTable = ({ versions, canCompare, onCheck }: VersionsT
{versions.map((version, idx) => (
-
+
onCheck(ev, version.version)}
+ onChange={(ev) => onCheck(ev, version.id)}
disabled={!version.checked && canCompare}
/>
diff --git a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx
index b92b77d1a1c..af1f894d3f2 100644
--- a/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx
+++ b/public/app/features/dashboard/components/VersionHistory/useDashboardRestore.tsx
@@ -4,7 +4,7 @@ import { useAsyncFn } from 'react-use';
import { locationUtil } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { useAppNotification } from 'app/core/copy/appNotification';
-import { getHistorySrv } from 'app/features/dashboard-scene/settings/version-history/HistorySrv';
+import { historySrv } from 'app/features/dashboard-scene/settings/version-history';
import { useSelector } from 'app/types';
import { dashboardWatcher } from '../../../live/dashboard/dashboardWatcher';
@@ -13,7 +13,7 @@ import { DashboardModel } from '../../state';
const restoreDashboard = async (version: number, dashboard: DashboardModel) => {
// Skip the watcher logic for this save since it's handled by the hook
dashboardWatcher.ignoreNextSave();
- return await getHistorySrv().restoreDashboard(dashboard.uid, version);
+ return await historySrv.restoreDashboard(dashboard.uid, version);
};
export const useDashboardRestore = (version: number) => {
diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts
index 56d76acd5ef..b8ca782906c 100644
--- a/public/app/features/dashboard/state/PanelModel.ts
+++ b/public/app/features/dashboard/state/PanelModel.ts
@@ -544,10 +544,7 @@ export class PanelModel implements DataConfigSource, IPanelModel {
updateQueries(options: QueryGroupOptions) {
const { dataSource } = options;
- this.datasource = {
- uid: dataSource.uid,
- type: dataSource.type,
- };
+ this.datasource = dataSource;
this.cacheTimeout = options.cacheTimeout;
this.queryCachingTTL = options.queryCachingTTL;
diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts
index 7a09899fe5b..24ab46c15b7 100644
--- a/public/app/features/datasources/state/actions.ts
+++ b/public/app/features/datasources/state/actions.ts
@@ -18,6 +18,7 @@ import {
import { updateNavIndex } from 'app/core/actions';
import { appEvents, contextSrv } from 'app/core/core';
import { getBackendSrv } from 'app/core/services/backend_srv';
+import { DatasourceAPIVersions } from 'app/features/apiserver/client';
import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
@@ -264,6 +265,8 @@ export function loadDataSourcePlugins(): ThunkResult {
};
}
+const dsApiVersions = new DatasourceAPIVersions();
+
export function updateDataSource(dataSource: DataSourceSettings) {
return async (
dispatch: (
@@ -271,13 +274,16 @@ export function updateDataSource(dataSource: DataSourceSettings) {
) => DataSourceSettings
) => {
try {
+ if (config.featureToggles.grafanaAPIServerWithExperimentalAPIs) {
+ dataSource.apiVersion = await dsApiVersions.get(dataSource.type);
+ }
await api.updateDataSource(dataSource);
} catch (err) {
const formattedError = parseHealthCheckError(err);
dispatch(testDataSourceFailed(formattedError));
-
- return Promise.reject(err);
+ const errorInfo = isFetchError(err) ? err.data : { message: 'An unexpected error occurred.', traceID: '' };
+ return Promise.reject(errorInfo);
}
await getDatasourceSrv().reload();
diff --git a/public/app/features/explore/ContentOutline/ContentOutline.tsx b/public/app/features/explore/ContentOutline/ContentOutline.tsx
index a40362363de..415f34f3e52 100644
--- a/public/app/features/explore/ContentOutline/ContentOutline.tsx
+++ b/public/app/features/explore/ContentOutline/ContentOutline.tsx
@@ -7,6 +7,7 @@ import { reportInteraction } from '@grafana/runtime';
import { useStyles2, PanelContainer, CustomScrollbar } from '@grafana/ui';
import { ContentOutlineItemContextProps, useContentOutlineContext } from './ContentOutlineContext';
+import { ITEM_TYPES } from './ContentOutlineItem';
import { ContentOutlineItemButton } from './ContentOutlineItemButton';
function scrollableChildren(item: ContentOutlineItemContextProps) {
@@ -56,7 +57,12 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
}, {});
});
- const scrollIntoView = (ref: HTMLElement | null, itemPanelId: string, customOffsetTop = 0) => {
+ const scrollIntoView = (
+ ref: HTMLElement | null,
+ itemPanelId: string,
+ itemType: ITEM_TYPES | undefined,
+ customOffsetTop = 0
+ ) => {
let scrollValue = 0;
let el: HTMLElement | null | undefined = ref;
@@ -73,11 +79,24 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
top: scrollValue + customOffsetTop,
behavior: 'smooth',
});
+ };
- reportInteraction('explore_toolbar_contentoutline_clicked', {
- item: 'select_section',
- type: itemPanelId,
- });
+ const handleItemClicked = (item: ContentOutlineItemContextProps) => {
+ if (item.level === 'child' && item.type === 'filter') {
+ const activeParent = outlineItems.find((parent) => {
+ return parent.children?.find((child) => child.id === item.id);
+ });
+
+ if (activeParent) {
+ scrollIntoView(activeParent.ref, activeParent.panelId, activeParent.type, activeParent.customTopOffset);
+ }
+ } else {
+ scrollIntoView(item.ref, item.panelId, item.type, item.customTopOffset);
+ reportInteraction('explore_toolbar_contentoutline_clicked', {
+ item: 'select_section',
+ type: item.panelId,
+ });
+ }
};
const toggle = () => {
@@ -131,16 +150,6 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
}
}, [outlineItems, verticalScroll]);
- const activateFilter = (filterId: string) => {
- const activeParent = outlineItems.find((item) => {
- return item.children?.find((child) => child.id === filterId);
- });
-
- if (activeParent) {
- scrollIntoView(activeParent.ref, activeParent.panelId, activeParent.customTopOffset);
- }
- };
-
return (
@@ -173,7 +182,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
isChildActive(item, activeSectionChildId) && !contentOutlineExpanded && sectionsExpanded[item.id],
})}
icon={item.icon}
- onClick={() => scrollIntoView(item.ref, item.panelId)}
+ onClick={() => handleItemClicked(item)}
tooltip={item.title}
collapsible={isCollapsible(item)}
collapsed={!sectionsExpanded[item.id]}
@@ -208,9 +217,7 @@ export function ContentOutline({ scroller, panelId }: { scroller: HTMLElement |
})}
indentStyle={styles.indentChild}
onClick={(e) => {
- child.type === 'filter'
- ? activateFilter(child.id)
- : scrollIntoView(child.ref, child.panelId, child.customTopOffset);
+ handleItemClicked(child);
child.onClick?.(e);
}}
tooltip={child.title}
diff --git a/public/app/features/explore/Logs/Logs.tsx b/public/app/features/explore/Logs/Logs.tsx
index 0544b3720fd..6cce1574c5c 100644
--- a/public/app/features/explore/Logs/Logs.tsx
+++ b/public/app/features/explore/Logs/Logs.tsx
@@ -262,6 +262,10 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => {
highlight: currentLevelSelected && !allLevelsSelected,
onClick: (e: React.MouseEvent) => {
toggleLegendRef.current?.(level.levelStr, mapMouseEventToMode(e));
+ reportInteraction('explore_toolbar_contentoutline_clicked', {
+ item: 'section',
+ type: `Logs:filter:${level.levelStr}`,
+ });
},
ref: null,
color: LogLevelColor[level.logLevel],
@@ -658,6 +662,11 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => {
);
+
+ reportInteraction('explore_toolbar_contentoutline_clicked', {
+ item: 'section',
+ type: 'Logs:pinned:pinned-log-limit-reached',
+ });
return;
}
@@ -677,16 +686,31 @@ const UnthemedLogs: React.FunctionComponent = (props: Props) => {
ref: null,
color: LogLevelColor[row.logLevel],
childOnTop: true,
- onClick: () => onOpenContext(row, () => {}),
+ onClick: () => {
+ onOpenContext(row, () => {});
+ reportInteraction('explore_toolbar_contentoutline_clicked', {
+ item: 'section',
+ type: 'Logs:pinned:pinned-log-clicked',
+ });
+ },
onRemove: (id: string) => {
unregister?.(id);
if (getPinnedLogsCount() < PINNED_LOGS_LIMIT) {
setPinLineButtonTooltipTitle('Pin to content outline');
}
+ reportInteraction('explore_toolbar_contentoutline_clicked', {
+ item: 'section',
+ type: 'Logs:pinned:pinned-log-deleted',
+ });
},
});
props.onPinLineCallback?.();
+
+ reportInteraction('explore_toolbar_contentoutline_clicked', {
+ item: 'section',
+ type: 'Logs:pinned:pinned-log-added',
+ });
};
const getPinnedLogsCount = () => {
diff --git a/public/app/features/library-panels/state/api.ts b/public/app/features/library-panels/state/api.ts
index d5611d98307..ce5faba1f2c 100644
--- a/public/app/features/library-panels/state/api.ts
+++ b/public/app/features/library-panels/state/api.ts
@@ -170,7 +170,6 @@ export function libraryVizPanelToSaveModel(libraryPanel: LibraryVizPanel) {
export async function updateLibraryVizPanel(libraryPanel: LibraryVizPanel): Promise {
const { uid, folderUID, name, model, version, kind } = libraryVizPanelToSaveModel(libraryPanel);
- console.log('updateLibraryVizPanel', model);
const { result } = await getBackendSrv().patch(`/api/library-elements/${uid}`, {
folderUID,
name,
diff --git a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts
index 537535a7920..cd533d124aa 100644
--- a/public/app/features/migrate-to-cloud/api/endpoints.gen.ts
+++ b/public/app/features/migrate-to-cloud/api/endpoints.gen.ts
@@ -143,7 +143,7 @@ export type ErrorResponseBody = {
/** a human readable version of the error */
message: string;
/** Status An optional status to denote the cause of the error.
-
+
For example, a 412 Precondition Failed error may include additional information of why that error happened. */
status?: string;
};
diff --git a/public/app/features/migrate-to-cloud/cloud/EmptyState/InfoPane.tsx b/public/app/features/migrate-to-cloud/cloud/EmptyState/InfoPane.tsx
new file mode 100644
index 00000000000..c94e1a59a3f
--- /dev/null
+++ b/public/app/features/migrate-to-cloud/cloud/EmptyState/InfoPane.tsx
@@ -0,0 +1,20 @@
+import { Box } from '@grafana/ui';
+import { t, Trans } from 'app/core/internationalization';
+
+import { InfoItem } from '../../shared/InfoItem';
+import { MigrationTokenPane } from '../MigrationTokenPane/MigrationTokenPane';
+
+export const InfoPane = () => {
+ return (
+
+
+
+ You can migrate some resources from your self-managed Grafana installation to this cloud stack. To do this
+ securely, you'll need to generate a migration token. Your self-managed instance will use the token to
+ authenticate with this cloud stack.
+
+
+
+
+ );
+};
diff --git a/public/app/features/migrate-to-cloud/cloud/EmptyState/MigrationStepsPane.tsx b/public/app/features/migrate-to-cloud/cloud/EmptyState/MigrationStepsPane.tsx
new file mode 100644
index 00000000000..bc641972e7a
--- /dev/null
+++ b/public/app/features/migrate-to-cloud/cloud/EmptyState/MigrationStepsPane.tsx
@@ -0,0 +1,68 @@
+import { css } from '@emotion/css';
+
+import { GrafanaTheme2 } from '@grafana/data';
+import { Box, Stack, TextLink, useStyles2 } from '@grafana/ui';
+import { t, Trans } from 'app/core/internationalization';
+
+import { InfoItem } from '../../shared/InfoItem';
+
+export const MigrationStepsPane = () => {
+ const styles = useStyles2(getStyles);
+
+ return (
+
+
+
+
+ The migration process must be started from your self-managed Grafana instance.
+
+
+
+
+ Log in to your self-managed instance and navigate to Administration, General, Migrate to Grafana Cloud.
+
+
+
+
+ Select "Migrate this instance to Cloud".
+
+
+
+
+ You'll be prompted for a migration token. Generate one from this screen.
+
+
+
+
+ In your self-managed instance, select "Upload everything" to upload data sources and
+ dashboards to this cloud stack.
+
+
+
+
+ If some of your data sources will not work over the public internet, you’ll need to install Private Data
+ Source Connect in your self-managed environment.
+
+
+
+
+
+
+ {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')}
+
+
+ );
+};
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ list: css({
+ padding: 'revert',
+ }),
+});
diff --git a/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx b/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx
index 825c86a52e1..34b3cf825e6 100644
--- a/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx
+++ b/public/app/features/migrate-to-cloud/cloud/MigrationTokenPane/MigrationTokenPane.tsx
@@ -4,7 +4,6 @@ import { Box, Button, Text } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { useCreateCloudMigrationTokenMutation } from '../../api';
-import { InfoItem } from '../../shared/InfoItem';
import { TokenErrorAlert } from '../TokenErrorAlert';
import { MigrationTokenModal } from './MigrationTokenModal';
@@ -28,14 +27,12 @@ export const MigrationTokenPane = () => {
return (
<>
-
-
-
- Your self-managed Grafana instance will require a special authentication token to securely connect to this
- cloud stack.
-
-
-
+
+
+ {createTokenResponse.isLoading
+ ? t('migrate-to-cloud.migration-token.generate-button-loading', 'Generating a migration token...')
+ : t('migrate-to-cloud.migration-token.generate-button', 'Generate a migration token')}
+
{createTokenResponse?.isError ? (
) : (
@@ -45,12 +42,6 @@ export const MigrationTokenPane = () => {
)}
-
-
- {createTokenResponse.isLoading
- ? t('migrate-to-cloud.migration-token.generate-button-loading', 'Generating a migration token...')
- : t('migrate-to-cloud.migration-token.generate-button', 'Generate a migration token')}
-
{
- const styles = useStyles2(getStyles);
-
return (
-
-
+
+
-
-
-
+
+
+
);
};
-
-const getStyles = (theme: GrafanaTheme2) => ({
- container: css({
- maxWidth: theme.breakpoints.values.xl,
- }),
-});
diff --git a/public/app/features/migrate-to-cloud/onprem/CTAInfo.tsx b/public/app/features/migrate-to-cloud/onprem/CTAInfo.tsx
new file mode 100644
index 00000000000..eed3befa65d
--- /dev/null
+++ b/public/app/features/migrate-to-cloud/onprem/CTAInfo.tsx
@@ -0,0 +1,27 @@
+import { ReactNode } from 'react';
+
+import { Stack, Box, Text } from '@grafana/ui';
+
+interface CTAInfoProps {
+ title: NonNullable;
+ accessory?: ReactNode;
+ children: ReactNode;
+}
+
+export function CTAInfo(props: CTAInfoProps) {
+ const { title, accessory, children } = props;
+
+ return (
+
+ {accessory && {accessory} }
+
+
+
+ {title}
+
+
+ {children}
+
+
+ );
+}
diff --git a/public/app/features/migrate-to-cloud/onprem/MigrationInfo.tsx b/public/app/features/migrate-to-cloud/onprem/MigrationInfo.tsx
index 59062c4d39f..9aa974e78f3 100644
--- a/public/app/features/migrate-to-cloud/onprem/MigrationInfo.tsx
+++ b/public/app/features/migrate-to-cloud/onprem/MigrationInfo.tsx
@@ -1,19 +1,19 @@
import { ReactNode } from 'react';
-import { Stack, Text } from '@grafana/ui';
+import { Box, Text } from '@grafana/ui';
interface MigrationInfoProps {
title: NonNullable;
- value: NonNullable;
+ children: NonNullable;
}
-export function MigrationInfo({ title, value }: MigrationInfoProps) {
+export function MigrationInfo({ title, children }: MigrationInfoProps) {
return (
-
+
{title}
- {value}
-
+ {children}
+
);
}
diff --git a/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx b/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx
new file mode 100644
index 00000000000..f27383c62ec
--- /dev/null
+++ b/public/app/features/migrate-to-cloud/onprem/MigrationSummary.tsx
@@ -0,0 +1,110 @@
+import { Box, Button, Space, Stack, Text } from '@grafana/ui';
+import { Trans, t } from 'app/core/internationalization';
+
+import { GetSessionApiResponse, GetSnapshotResponseDto } from '../api';
+
+import { MigrationInfo } from './MigrationInfo';
+
+interface MigrationSummaryProps {
+ snapshot: GetSnapshotResponseDto | undefined;
+ session: GetSessionApiResponse;
+ isBusy: boolean;
+
+ disconnectIsLoading: boolean;
+ onDisconnect: () => void;
+
+ showBuildSnapshot: boolean;
+ buildSnapshotIsLoading: boolean;
+ onBuildSnapshot: () => void;
+
+ showUploadSnapshot: boolean;
+ uploadSnapshotIsLoading: boolean;
+ onUploadSnapshot: () => void;
+}
+
+export function MigrationSummary(props: MigrationSummaryProps) {
+ const {
+ session,
+ snapshot,
+ isBusy,
+ disconnectIsLoading,
+ onDisconnect,
+ showBuildSnapshot,
+ buildSnapshotIsLoading,
+ onBuildSnapshot,
+
+ showUploadSnapshot,
+ uploadSnapshotIsLoading,
+ onUploadSnapshot,
+ } = props;
+
+ const totalCount = 0;
+ const errorCount = 0;
+ const successCount = 0;
+
+ return (
+
+
+
+ {snapshot?.created ? (
+ snapshot?.created
+ ) : (
+
+ Not yet created
+
+ )}
+
+
+
+ {totalCount}
+
+
+
+ {errorCount}
+
+
+
+ {successCount}
+
+
+
+ {session.slug}
+
+
+ Disconnect
+
+
+
+
+ {showBuildSnapshot && (
+
+ Build snapshot
+
+ )}
+
+ {showUploadSnapshot && (
+
+ Upload snapshot
+
+ )}
+
+ );
+}
diff --git a/public/app/features/migrate-to-cloud/onprem/Page.tsx b/public/app/features/migrate-to-cloud/onprem/Page.tsx
index 91b59f801a4..b991818abff 100644
--- a/public/app/features/migrate-to-cloud/onprem/Page.tsx
+++ b/public/app/features/migrate-to-cloud/onprem/Page.tsx
@@ -1,11 +1,12 @@
import { skipToken } from '@reduxjs/toolkit/query/react';
import { useCallback, useEffect, useState } from 'react';
-import { Alert, Box, Button, Stack } from '@grafana/ui';
+import { Alert, Box, Stack } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import {
SnapshotDto,
+ useCancelSnapshotMutation,
useCreateSnapshotMutation,
useDeleteSessionMutation,
useGetSessionListQuery,
@@ -16,22 +17,25 @@ import {
import { DisconnectModal } from './DisconnectModal';
import { EmptyState } from './EmptyState/EmptyState';
-import { MigrationInfo } from './MigrationInfo';
+import { MigrationSummary } from './MigrationSummary';
import { ResourcesTable } from './ResourcesTable';
+import { BuildSnapshotCTA, CreatingSnapshotCTA } from './SnapshotCTAs';
/**
* Here's how migrations work:
*
- * A single on-prem instance can be configured to be migrated to multiple cloud instances.
- * - GetMigrationList returns this the list of migration targets for the on prem instance
- * - If GetMigrationList returns an empty list, then the empty state with a prompt to enter a token should be shown
+ * A single on-prem instance can be configured to be migrated to multiple cloud instances. We call these 'sessions'.
+ * - GetSessionList returns this the list of migration targets for the on prem instance
+ * - If GetMigrationList returns an empty list, then an empty state to prompt for token should be shown
* - The UI (at the moment) only shows the most recently created migration target (the last one returned from the API)
* and doesn't allow for others to be created
*
- * A single on-prem migration 'target' (CloudMigrationResponse) can have multiple migration runs (CloudMigrationRun)
- * - To list the migration resources:
- * 1. call GetCloudMigratiopnRunList to list all runs
- * 2. call GetCloudMigrationRun with the ID from first step to list the result of that migration
+ * A single on-prem migration 'target' (CloudMigrationSession) can have multiple snapshots.
+ * A snapshot represents a copy of all migratable resources at a fixed point in time.
+ * A snapshots are created asynchronously in the background, so GetSnapshot must be polled to get the current status.
+ *
+ * After a snapshot has been created, it will be PENDING_UPLOAD. UploadSnapshot is then called which asynchronously
+ * uploads and migrates the snapshot to the cloud instance.
*/
function useGetLatestSession() {
@@ -52,6 +56,10 @@ const SHOULD_POLL_STATUSES: Array = [
'PROCESSING',
];
+const SNAPSHOT_BUILDING_STATUSES: Array = ['INITIALIZING', 'CREATING'];
+
+const SNAPSHOT_UPLOADING_STATUSES: Array = ['UPLOADING', 'PENDING_PROCESSING', 'PROCESSING'];
+
const STATUS_POLL_INTERVAL = 5 * 1000;
function useGetLatestSnapshot(sessionUid?: string) {
@@ -91,23 +99,27 @@ export const Page = () => {
const snapshot = useGetLatestSnapshot(session.data?.uid);
const [performCreateSnapshot, createSnapshotResult] = useCreateSnapshotMutation();
const [performUploadSnapshot, uploadSnapshotResult] = useUploadSnapshotMutation();
+ const [performCancelSnapshot, cancelSnapshotResult] = useCancelSnapshotMutation();
const [performDisconnect, disconnectResult] = useDeleteSessionMutation();
const sessionUid = session.data?.uid;
const snapshotUid = snapshot.data?.uid;
- const migrationMeta = session.data;
const isInitialLoading = session.isLoading;
+ const status = snapshot.data?.status;
// isBusy is not a loading state, but indicates that the system is doing *something*
// and all buttons should be disabled
const isBusy =
createSnapshotResult.isLoading ||
uploadSnapshotResult.isLoading ||
+ cancelSnapshotResult.isLoading ||
session.isLoading ||
snapshot.isLoading ||
disconnectResult.isLoading;
- const resources = snapshot.data?.results;
+ const showBuildSnapshot = !snapshot.isLoading && !snapshot.data;
+ const showBuildingSnapshot = SNAPSHOT_BUILDING_STATUSES.includes(status);
+ const showUploadSnapshot = status === 'PENDING_UPLOAD' || SNAPSHOT_UPLOADING_STATUSES.includes(status);
const handleDisconnect = useCallback(async () => {
if (sessionUid) {
@@ -127,16 +139,23 @@ export const Page = () => {
}
}, [performUploadSnapshot, sessionUid, snapshotUid]);
+ const handleCancelSnapshot = useCallback(() => {
+ if (sessionUid && snapshotUid) {
+ performCancelSnapshot({ uid: sessionUid, snapshotUid: snapshotUid });
+ }
+ }, [performCancelSnapshot, sessionUid, snapshotUid]);
+
if (isInitialLoading) {
// TODO: better loading state
return Loading...
;
- } else if (!migrationMeta) {
+ } else if (!session.data) {
return ;
}
return (
<>
+ {/* TODO: show errors from all mutation's in a... modal? */}
{createSnapshotResult.isError && (
{
)}
- {migrationMeta.slug && (
-
-
- {migrationMeta.slug}{' '}
- setDisconnectModalOpen(true)}
- variant="secondary"
- size="sm"
- icon={disconnectResult.isLoading ? 'spinner' : undefined}
- >
- Disconnect
-
- >
- }
- />
+ {session.data && (
+
+ )}
-
+ {(showBuildSnapshot || showBuildingSnapshot) && (
+
+ {showBuildSnapshot && (
+
+ )}
-
- Build snapshot
-
-
-
- Upload & migrate snapshot
-
+ {showBuildingSnapshot && (
+
+ )}
)}
- {resources && }
+ {snapshot.data?.results && snapshot.data.results.length > 0 && (
+
+ )}
void;
+}
+
+export function BuildSnapshotCTA(props: SnapshotCTAProps) {
+ const { disabled, isLoading, onClick } = props;
+
+ return (
+ }
+ >
+
+
+ This tool can migrate some resources from this installation to your cloud stack. To get started, you'll
+ need to create a snapshot of this installation. Creating a snapshot typically takes less than two minutes. The
+ snapshot is stored alongside this Grafana installation.
+
+
+
+
+
+ Once the snapshot is complete, you will be able to upload it to your cloud stack.
+
+
+
+
+ Build snapshot
+
+
+ );
+}
+
+export function CreatingSnapshotCTA(props: SnapshotCTAProps) {
+ const { disabled, isLoading, onClick } = props;
+
+ return (
+ }
+ >
+
+
+ We're creating a point-in-time snapshot of the current state of this installation. Once the snapshot is
+ complete. you'll be able to upload it to Grafana Cloud.
+
+
+
+
+
+ Creating a snapshot typically takes less than two minutes.
+
+
+
+
+ Cancel snapshot
+
+
+ );
+}
diff --git a/public/app/features/preferences/api/index.ts b/public/app/features/preferences/api/index.ts
new file mode 100644
index 00000000000..1da53081140
--- /dev/null
+++ b/public/app/features/preferences/api/index.ts
@@ -0,0 +1,19 @@
+import { generatedAPI } from './user/endpoints.gen';
+
+export const { useGetUserPreferencesQuery, usePatchUserPreferencesMutation, useUpdateUserPreferencesMutation } =
+ generatedAPI;
+
+export const userPreferencesAPI = generatedAPI.enhanceEndpoints({
+ addTagTypes: ['UserPreferences'],
+ endpoints: {
+ getUserPreferences: {
+ providesTags: ['UserPreferences'],
+ },
+ updateUserPreferences: {
+ invalidatesTags: ['UserPreferences'],
+ },
+ patchUserPreferences: {
+ invalidatesTags: ['UserPreferences'],
+ },
+ },
+});
diff --git a/public/app/features/preferences/api/user/baseAPI.ts b/public/app/features/preferences/api/user/baseAPI.ts
new file mode 100644
index 00000000000..b1b75e7b036
--- /dev/null
+++ b/public/app/features/preferences/api/user/baseAPI.ts
@@ -0,0 +1,36 @@
+import { BaseQueryFn, createApi } from '@reduxjs/toolkit/query/react';
+import { lastValueFrom } from 'rxjs';
+
+import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
+
+interface RequestOptions extends BackendSrvRequest {
+ manageError?: (err: unknown) => { error: unknown };
+ showErrorAlert?: boolean;
+ body?: BackendSrvRequest['data'];
+}
+
+function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn {
+ async function backendSrvBaseQuery(requestOptions: RequestOptions) {
+ try {
+ const { data: responseData, ...meta } = await lastValueFrom(
+ getBackendSrv().fetch({
+ ...requestOptions,
+ url: baseURL + requestOptions.url,
+ showErrorAlert: requestOptions.showErrorAlert,
+ data: requestOptions.body,
+ })
+ );
+ return { data: responseData, meta };
+ } catch (error) {
+ return requestOptions.manageError ? requestOptions.manageError(error) : { error };
+ }
+ }
+
+ return backendSrvBaseQuery;
+}
+
+export const baseAPI = createApi({
+ reducerPath: 'userPreferencesAPI',
+ baseQuery: createBackendSrvBaseQuery({ baseURL: '/api' }),
+ endpoints: () => ({}),
+});
diff --git a/public/app/features/preferences/api/user/endpoints.gen.ts b/public/app/features/preferences/api/user/endpoints.gen.ts
new file mode 100644
index 00000000000..e5aff73929c
--- /dev/null
+++ b/public/app/features/preferences/api/user/endpoints.gen.ts
@@ -0,0 +1,102 @@
+import { baseAPI as api } from './baseAPI';
+const injectedRtkApi = api.injectEndpoints({
+ endpoints: (build) => ({
+ getUserPreferences: build.query({
+ query: () => ({ url: `/user/preferences` }),
+ }),
+ patchUserPreferences: build.mutation({
+ query: (queryArg) => ({ url: `/user/preferences`, method: 'PATCH', body: queryArg.patchPrefsCmd }),
+ }),
+ updateUserPreferences: build.mutation({
+ query: (queryArg) => ({ url: `/user/preferences`, method: 'PUT', body: queryArg.updatePrefsCmd }),
+ }),
+ }),
+ overrideExisting: false,
+});
+export { injectedRtkApi as generatedAPI };
+export type GetUserPreferencesApiResponse = /** status 200 (empty) */ Preferences;
+export type GetUserPreferencesApiArg = void;
+export type PatchUserPreferencesApiResponse =
+ /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody;
+export type PatchUserPreferencesApiArg = {
+ patchPrefsCmd: PatchPrefsCmd;
+};
+export type UpdateUserPreferencesApiResponse =
+ /** status 200 An OKResponse is returned if the request was successful. */ SuccessResponseBody;
+export type UpdateUserPreferencesApiArg = {
+ updatePrefsCmd: UpdatePrefsCmd;
+};
+export type CookiePreferencesDefinesModelForCookiePreferences = {
+ analytics?: {
+ [key: string]: any;
+ };
+ functional?: {
+ [key: string]: any;
+ };
+ performance?: {
+ [key: string]: any;
+ };
+};
+export type NavbarPreferenceDefinesModelForNavbarPreference = {
+ savedItemIds?: string[];
+};
+export type QueryHistoryPreferenceDefinesModelForQueryHistoryPreference = {
+ /** HomeTab one of: '' | 'query' | 'starred'; */
+ homeTab?: string;
+};
+export type Preferences = {
+ cookiePreferences?: CookiePreferencesDefinesModelForCookiePreferences;
+ /** UID for the home dashboard */
+ homeDashboardUID?: string;
+ /** Selected language (beta) */
+ language?: string;
+ navbar?: NavbarPreferenceDefinesModelForNavbarPreference;
+ queryHistory?: QueryHistoryPreferenceDefinesModelForQueryHistoryPreference;
+ /** Theme light, dark, empty is default */
+ theme?: string;
+ /** The timezone selection
+ TODO: this should use the timezone defined in common */
+ timezone?: string;
+ /** WeekStart day of the week (sunday, monday, etc) */
+ weekStart?: string;
+};
+export type ErrorResponseBody = {
+ /** Error An optional detailed description of the actual error. Only included if running in developer mode. */
+ error?: string;
+ /** a human readable version of the error */
+ message: string;
+ /** Status An optional status to denote the cause of the error.
+
+ For example, a 412 Precondition Failed error may include additional information of why that error happened. */
+ status?: string;
+};
+export type SuccessResponseBody = {
+ message?: string;
+};
+export type CookieType = string;
+export type PatchPrefsCmd = {
+ cookies?: CookieType[];
+ /** The numerical :id of a favorited dashboard */
+ homeDashboardId?: number;
+ homeDashboardUID?: string;
+ language?: string;
+ navbar?: NavbarPreferenceDefinesModelForNavbarPreference;
+ queryHistory?: QueryHistoryPreferenceDefinesModelForQueryHistoryPreference;
+ theme?: 'light' | 'dark';
+ timezone?: 'utc' | 'browser';
+ weekStart?: string;
+};
+export type UpdatePrefsCmd = {
+ cookies?: CookieType[];
+ /** The numerical :id of a favorited dashboard */
+ homeDashboardId?: number;
+ homeDashboardUID?: string;
+ language?: string;
+ navbar?: NavbarPreferenceDefinesModelForNavbarPreference;
+ queryHistory?: QueryHistoryPreferenceDefinesModelForQueryHistoryPreference;
+ theme?: 'light' | 'dark' | 'system';
+ timezone?: 'utc' | 'browser';
+ weekStart?: string;
+};
+export const { useGetUserPreferencesQuery, usePatchUserPreferencesMutation, useUpdateUserPreferencesMutation } =
+ injectedRtkApi;
diff --git a/public/app/features/query/components/QueryEditorRow.tsx b/public/app/features/query/components/QueryEditorRow.tsx
index d58d80f4dcd..a68d235fe1f 100644
--- a/public/app/features/query/components/QueryEditorRow.tsx
+++ b/public/app/features/query/components/QueryEditorRow.tsx
@@ -20,6 +20,7 @@ import {
PanelEvents,
QueryResultMetaNotice,
TimeRange,
+ getDataSourceRef,
toLegacyResponseData,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@@ -364,7 +365,7 @@ export class QueryEditorRow extends PureComponent {
if (query.datasource === undefined) {
- query.datasource = { type: this.props.dataSource.type, uid: this.props.dataSource.uid };
+ query.datasource = getDataSourceRef(this.props.dataSource);
}
this.props.onChange({
diff --git a/public/app/features/query/components/QueryEditorRows.tsx b/public/app/features/query/components/QueryEditorRows.tsx
index 024c1da6d80..c3854935f4b 100644
--- a/public/app/features/query/components/QueryEditorRows.tsx
+++ b/public/app/features/query/components/QueryEditorRows.tsx
@@ -5,10 +5,10 @@ import {
CoreApp,
DataQuery,
DataSourceInstanceSettings,
- DataSourceRef,
EventBusExtended,
HistoryItem,
PanelData,
+ getDataSourceRef,
} from '@grafana/data';
import { getDataSourceSrv, reportInteraction } from '@grafana/runtime';
@@ -65,10 +65,7 @@ export class QueryEditorRows extends PureComponent {
return item;
}
- const dataSourceRef: DataSourceRef = {
- type: dataSource.type,
- uid: dataSource.uid,
- };
+ const dataSourceRef = getDataSourceRef(dataSource);
if (item.datasource) {
const previous = getDataSourceSrv().getInstanceSettings(item.datasource);
diff --git a/public/app/features/query/components/QueryGroup.tsx b/public/app/features/query/components/QueryGroup.tsx
index 79361af6ad3..ffa97a3fce2 100644
--- a/public/app/features/query/components/QueryGroup.tsx
+++ b/public/app/features/query/components/QueryGroup.tsx
@@ -7,6 +7,7 @@ import {
CoreApp,
DataSourceApi,
DataSourceInstanceSettings,
+ getDataSourceRef,
getDefaultTimeRange,
LoadingState,
PanelData,
@@ -149,8 +150,7 @@ export class QueryGroup extends PureComponent {
dataSource: {
name: newSettings.name,
uid: newSettings.uid,
- type: newSettings.meta.id,
- default: newSettings.isDefault,
+ ...getDataSourceRef(newSettings),
},
});
@@ -174,11 +174,16 @@ export class QueryGroup extends PureComponent {
newQuery(): Partial {
const { dsSettings, defaultDataSource } = this.state;
- const ds = !dsSettings?.meta.mixed ? dsSettings : defaultDataSource;
+ const ds =
+ dsSettings && !dsSettings.meta.mixed
+ ? getDataSourceRef(dsSettings)
+ : defaultDataSource
+ ? defaultDataSource.getRef()
+ : { type: undefined, uid: undefined };
return {
...this.state.dataSource?.getDefaultQuery?.(CoreApp.PanelEditor),
- datasource: { uid: ds?.uid, type: ds?.type },
+ datasource: ds,
};
}
@@ -241,7 +246,9 @@ export class QueryGroup extends PureComponent {
onAddQuery = (query: Partial) => {
const { dsSettings, queries } = this.state;
- this.onQueriesChange(addQuery(queries, query, { type: dsSettings?.type, uid: dsSettings?.uid }));
+ this.onQueriesChange(
+ addQuery(queries, query, dsSettings ? getDataSourceRef(dsSettings) : { type: undefined, uid: undefined })
+ );
this.onScrollBottom();
};
diff --git a/public/app/features/query/state/updateQueries.test.ts b/public/app/features/query/state/updateQueries.test.ts
index 155b70545af..f213f75a420 100644
--- a/public/app/features/query/state/updateQueries.test.ts
+++ b/public/app/features/query/state/updateQueries.test.ts
@@ -12,6 +12,7 @@ const oldUidDS = {
meta: {
id: 'old-type',
},
+ getRef: () => ({ uid: 'old-uid', type: 'old-type' }),
} as DataSourceApi;
const mixedDS = {
@@ -20,6 +21,7 @@ const mixedDS = {
id: 'mixed',
mixed: true,
},
+ getRef: () => ({ uid: 'mixed' }),
} as DataSourceApi;
const newUidDS = {
@@ -28,6 +30,7 @@ const newUidDS = {
meta: {
id: 'new-type',
},
+ getRef: () => ({ uid: 'new-uid', type: 'new-type' }),
} as DataSourceApi;
const newUidSameTypeDS = {
@@ -36,6 +39,7 @@ const newUidSameTypeDS = {
meta: {
id: 'old-type',
},
+ getRef: () => ({ uid: 'new-uid-same-type', type: 'old-type' }),
} as DataSourceApi;
const templateSrv = new TemplateSrv();
@@ -376,6 +380,7 @@ describe('updateQueries with import', () => {
const importedQueries = queries.map((q) => ({ ...q, imported: true }));
return Promise.resolve(importedQueries);
},
+ getRef: () => ({ uid: 'new-uid', type: 'new-type' }),
} as DataSourceWithQueryImportSupport;
const oldUidDSWithAbstract = {
@@ -389,6 +394,7 @@ describe('updateQueries with import', () => {
const exportedQueries = queries.map((q) => ({ ...q, exported: true }));
return Promise.resolve(exportedQueries);
},
+ getRef: () => ({ uid: 'old-uid', type: 'old-type' }),
} as DataSourceWithQueryExportSupport;
const queries = [
@@ -452,6 +458,7 @@ describe('updateQueries with import', () => {
importFromAbstractQueries: () => {
return Promise.resolve([]);
},
+ getRef: () => ({ uid: 'new-uid', type: 'new-type' }),
} as DataSourceWithQueryImportSupport;
const oldUidDSWithAbstract = {
@@ -464,6 +471,7 @@ describe('updateQueries with import', () => {
const exportedQueries = queries.map((q) => ({ ...q, exported: true }));
return Promise.resolve(exportedQueries);
},
+ getRef: () => ({ uid: 'old-uid', type: 'old-type' }),
} as DataSourceWithQueryExportSupport;
const queries = [
@@ -510,6 +518,7 @@ describe('updateQueries with import', () => {
const importedQueries = queries.map((q) => ({ ...q, imported: true }));
return Promise.resolve(importedQueries);
},
+ getRef: () => ({ uid: 'new-uid', type: 'new-type' }),
} as DataSourceApi;
const oldUidDS = {
@@ -573,6 +582,7 @@ describe('updateQueries with import', () => {
importQueries: (queries, origin) => {
return Promise.resolve([] as DataQuery[]);
},
+ getRef: () => ({ uid: 'new-uid', type: 'new-type' }),
} as DataSourceApi;
const oldUidDS = {
diff --git a/public/app/features/query/state/updateQueries.ts b/public/app/features/query/state/updateQueries.ts
index e8464ac40f5..f19030e6fd4 100644
--- a/public/app/features/query/state/updateQueries.ts
+++ b/public/app/features/query/state/updateQueries.ts
@@ -10,7 +10,7 @@ export async function updateQueries(
currentDS?: DataSourceApi
): Promise {
let nextQueries = queries;
- const datasource = { type: nextDS.type, uid: nextDSUidOrVariableExpression };
+ const datasource = { ...nextDS.getRef(), uid: nextDSUidOrVariableExpression };
const DEFAULT_QUERY = { ...nextDS?.getDefaultQuery?.(CoreApp.PanelEditor), datasource, refId: 'A' };
// we are changing data source type
diff --git a/public/app/features/trails/ActionTabs/LayoutSwitcher.tsx b/public/app/features/trails/ActionTabs/LayoutSwitcher.tsx
index e9382e9fa1b..b1f621b03c7 100644
--- a/public/app/features/trails/ActionTabs/LayoutSwitcher.tsx
+++ b/public/app/features/trails/ActionTabs/LayoutSwitcher.tsx
@@ -4,6 +4,7 @@ import { Field, RadioButtonGroup } from '@grafana/ui';
import { MetricScene } from '../MetricScene';
import { reportExploreMetrics } from '../interactions';
+import { TRAIL_BREAKDOWN_VIEW_KEY } from '../shared';
import { LayoutType } from './types';
@@ -38,6 +39,7 @@ export class LayoutSwitcher extends SceneObjectBase {
public onLayoutChange = (layout: LayoutType) => {
reportExploreMetrics('breakdown_layout_changed', { layout });
+ localStorage.setItem(TRAIL_BREAKDOWN_VIEW_KEY, layout);
this.getMetricScene().setState({ layout });
};
diff --git a/public/app/features/trails/ActionTabs/types.ts b/public/app/features/trails/ActionTabs/types.ts
index 37a6333e91a..d65482c4a29 100644
--- a/public/app/features/trails/ActionTabs/types.ts
+++ b/public/app/features/trails/ActionTabs/types.ts
@@ -1 +1,7 @@
-export type LayoutType = 'single' | 'grid' | 'rows';
+const LAYOUT_TYPES = ['single', 'grid', 'rows'] as const;
+
+export type LayoutType = (typeof LAYOUT_TYPES)[number];
+
+export function isLayoutType(layoutType: string | null | undefined): layoutType is LayoutType {
+ return !!layoutType && layoutType in LAYOUT_TYPES;
+}
diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx
index d13e890a318..d8b243a109b 100644
--- a/public/app/features/trails/DataTrail.tsx
+++ b/public/app/features/trails/DataTrail.tsx
@@ -272,7 +272,6 @@ function getStyles(theme: GrafanaTheme2) {
flexGrow: 1,
display: 'flex',
gap: theme.spacing(1),
- minHeight: '100%',
flexDirection: 'column',
background: theme.isLight ? theme.colors.background.primary : theme.colors.background.canvas,
padding: theme.spacing(2, 3, 2, 3),
diff --git a/public/app/features/trails/MetricScene.tsx b/public/app/features/trails/MetricScene.tsx
index 4cb134f0e52..65c43546b20 100644
--- a/public/app/features/trails/MetricScene.tsx
+++ b/public/app/features/trails/MetricScene.tsx
@@ -19,7 +19,7 @@ import { getExploreUrl } from '../../core/utils/explore';
import { buildBreakdownActionScene } from './ActionTabs/BreakdownScene';
import { buildMetricOverviewScene } from './ActionTabs/MetricOverviewScene';
import { buildRelatedMetricsScene } from './ActionTabs/RelatedMetricsScene';
-import { LayoutType } from './ActionTabs/types';
+import { isLayoutType, LayoutType } from './ActionTabs/types';
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine';
import { AutoQueryDef, AutoQueryInfo } from './AutomaticMetricQueries/types';
import { MAIN_PANEL_MAX_HEIGHT, MAIN_PANEL_MIN_HEIGHT, MetricGraphScene } from './MetricGraphScene';
@@ -32,6 +32,7 @@ import {
getVariablesWithMetricConstant,
MakeOptional,
MetricSelectedEvent,
+ TRAIL_BREAKDOWN_VIEW_KEY,
trailDS,
VAR_GROUP_BY,
VAR_METRIC_EXPR,
@@ -53,12 +54,13 @@ export class MetricScene extends SceneObjectBase {
public constructor(state: MakeOptional) {
const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric);
+ const layout = localStorage.getItem(TRAIL_BREAKDOWN_VIEW_KEY);
super({
$variables: state.$variables ?? getVariableSet(state.metric),
body: state.body ?? new MetricGraphScene({}),
autoQuery,
queryDef: state.queryDef ?? autoQuery.main,
- layout: state.layout ?? 'grid',
+ layout: isLayoutType(layout) ? layout : 'grid',
...state,
});
diff --git a/public/app/features/trails/shared.ts b/public/app/features/trails/shared.ts
index 4b12725cb9a..8da3b78f63c 100644
--- a/public/app/features/trails/shared.ts
+++ b/public/app/features/trails/shared.ts
@@ -34,6 +34,8 @@ export const RECENT_TRAILS_KEY = 'grafana.trails.recent';
export const TRAIL_BOOKMARKS_KEY = 'grafana.trails.bookmarks';
+export const TRAIL_BREAKDOWN_VIEW_KEY = 'grafana.trails.breakdown.view';
+
export type MakeOptional = Pick, K> & Omit;
export function getVariablesWithMetricConstant(metric: string) {
diff --git a/public/app/features/users/UsersListPage.tsx b/public/app/features/users/UsersListPage.tsx
index 357326d28bc..452901a89bb 100644
--- a/public/app/features/users/UsersListPage.tsx
+++ b/public/app/features/users/UsersListPage.tsx
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { renderMarkdown } from '@grafana/data';
+import { Alert } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
import { OrgUser, OrgRole, StoreState } from 'app/types';
@@ -104,7 +105,9 @@ export const UsersListPageUnconnected = ({
{externalUserMngInfoHtml && (
-
+
+
+
)}
{isLoading && renderTable()}
diff --git a/public/app/features/variables/pickers/shared/VariableLink.tsx b/public/app/features/variables/pickers/shared/VariableLink.tsx
index 25e6c0694e6..f481fa68454 100644
--- a/public/app/features/variables/pickers/shared/VariableLink.tsx
+++ b/public/app/features/variables/pickers/shared/VariableLink.tsx
@@ -7,6 +7,7 @@ import { Icon, useStyles2 } from '@grafana/ui';
import { LoadingIndicator } from '@grafana/ui/src/components/PanelChrome/LoadingIndicator';
import { t } from 'app/core/internationalization';
+import { getStyles as getTagBadgeStyles } from '../../../../core/components/TagFilter/TagBadge';
import { ALL_VARIABLE_TEXT } from '../../constants';
interface Props {
@@ -76,34 +77,38 @@ const VariableLinkText = ({ text }: VariableLinkTextProps) => {
);
};
-const getStyles = (theme: GrafanaTheme2) => ({
- container: css`
- max-width: 500px;
- padding-right: 10px;
- padding: 0 ${theme.spacing(1)};
- background-color: ${theme.components.input.background};
- border: 1px solid ${theme.components.input.borderColor};
- border-radius: ${theme.shape.radius.default};
- display: flex;
- align-items: center;
- color: ${theme.colors.text};
- height: ${theme.spacing(theme.components.height.md)};
+const getStyles = (theme: GrafanaTheme2) => {
+ const tagBadgeStyles = getTagBadgeStyles(theme);
- .label-tag {
- margin: 0 5px;
- }
+ return {
+ container: css({
+ maxWidth: '500px',
+ paddingRight: '10px',
+ padding: theme.spacing(0, 1),
+ backgroundColor: theme.components.input.background,
+ border: `1px solid ${theme.components.input.borderColor}`,
+ borderRadius: theme.shape.radius.default,
+ display: 'flex',
+ alignItems: 'center',
+ color: theme.colors.text.primary,
+ height: theme.spacing(theme.components.height.md),
- &:disabled {
- background-color: ${theme.colors.action.disabledBackground};
- color: ${theme.colors.action.disabledText};
- border: 1px solid ${theme.colors.action.disabledBackground};
- }
- `,
- textAndTags: css`
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- margin-right: ${theme.spacing(0.25)};
- user-select: none;
- `,
-});
+ [`.${tagBadgeStyles.badge}`]: {
+ margin: '0 5px',
+ },
+
+ '&:disabled': {
+ backgroundColor: theme.colors.action.disabledBackground,
+ color: theme.colors.action.disabledText,
+ border: `1px solid ${theme.colors.action.disabledBackground}`,
+ },
+ }),
+ textAndTags: css({
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ marginRight: theme.spacing(0.25),
+ userSelect: 'none',
+ }),
+ };
+};
diff --git a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationsHelp.tsx b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationsHelp.tsx
index 91a81625017..807850f41c0 100644
--- a/public/app/plugins/datasource/cloud-monitoring/components/AnnotationsHelp.tsx
+++ b/public/app/plugins/datasource/cloud-monitoring/components/AnnotationsHelp.tsx
@@ -1,8 +1,9 @@
+import { Alert } from '@grafana/ui';
+
export const AnnotationsHelp = () => {
return (
-
+
-
Annotation Query Format
An annotation is an event that is overlaid on top of graphs. Annotation rendering is expensive so it is
important to limit the number of rows returned.{' '}
@@ -37,6 +38,6 @@ export const AnnotationsHelp = () => {
{`${'{{resource.label.label_name}}'}`} = Resource label metadata e.g. resource.label.zone
-
+
);
};
diff --git a/public/app/plugins/datasource/cloud-monitoring/dashboards/gke-prometheus-pod-node-monitoring.json b/public/app/plugins/datasource/cloud-monitoring/dashboards/gke-prometheus-pod-node-monitoring.json
index 627f821feec..ada7a9849f8 100644
--- a/public/app/plugins/datasource/cloud-monitoring/dashboards/gke-prometheus-pod-node-monitoring.json
+++ b/public/app/plugins/datasource/cloud-monitoring/dashboards/gke-prometheus-pod-node-monitoring.json
@@ -9,7 +9,10 @@
"list": [
{
"builtIn": 1,
- "datasource": "-- Grafana --",
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
@@ -19,466 +22,683 @@
]
},
"editable": true,
- "gnetId": null,
+ "fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
- "datasource": null,
- "description": "",
- "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] },
- "gridPos": { "h": 3, "w": 24, "x": 0, "y": 0 },
- "id": 0,
- "options": {
- "mode": "markdown",
- "content": "This dashboard has example charts for metrics exported by Prometheus, for example metrics from [Kubernetes pod metrics](https://github.com/kubernetes/kube-state-metrics/blob/master/docs/pod-metrics.md)"
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
},
- "pluginVersion": "7.4.0-pre",
- "timeFrom": null,
- "timeShift": null,
- "title": "GKE Prometheus Pod/Node Monitoring",
+ "description": "",
+ "gridPos": {
+ "h": 7,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "id": 7,
+ "options": {
+ "code": {
+ "language": "plaintext",
+ "showLineNumbers": false,
+ "showMiniMap": false
+ },
+ "content": "This dashboard has example charts for metrics exported by Prometheus, for example metrics from [Kubernetes Pods](https://github.com/kubernetes/kube-state-metrics/blob/master/docs/pod-metrics.md) and [Kubernetes Nodes](https://github.com/kubernetes/kube-state-metrics/blob/main/docs/metrics/cluster/node-metrics.md).\n\nNote that if you are using the deprecated [Stackdriver Prometheus sidecar](https://cloud.google.com/monitoring/api/metrics_other#prometheus) then this dashboard will not function as expected. This dashboard requires metrics to be collected by Google Managed Prometheus as they will then be exposed under the `prometheus.googleapis.com` metrics descriptor.\n\nIf you are using a GKE cluster that has been configured to automatically scrape Kube state metrics then some of the panels on this dashboard will not show data as the default configuration only sends a [subset](https://cloud.google.com/kubernetes-engine/docs/how-to/kube-state-metrics) of the available Kube state metrics.\n\nTo retrieve all available Kube state metrics [this](https://cloud.google.com/stackdriver/docs/managed-prometheus/exporters/kube_state_metrics#install-exporter) documentation can be followed.",
+ "mode": "markdown"
+ },
+ "pluginVersion": "11.2.0-pre",
+ "targets": [
+ {
+ "datasource": {
+ "type": "datasource",
+ "uid": "grafana"
+ },
+ "refId": "A"
+ }
+ ],
+ "title": "GKE Prometheus Pod/Node Monitoring Test",
"type": "text"
},
{
- "aliasColors": {},
- "bars": true,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "decimals": 2,
+ "datasource": {
+ "type": "stackdriver",
+ "uid": "$datasource"
+ },
"description": "",
- "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] },
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 },
- "hiddenSeries": false,
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "bars",
+ "fillOpacity": 100,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "normal"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 3
+ },
"id": 1,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "total": false,
- "values": false,
- "sideWidth": 220
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true,
+ "width": 220
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
},
- "lines": false,
- "linewidth": 1,
- "nullPointMode": "connected",
- "options": { "alertThreshold": false },
- "percentage": true,
"pluginVersion": "7.4.0-pre",
- "pointradius": 2,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "spaceLength": 10,
- "stack": true,
- "steppedLine": false,
"targets": [
{
- "queryType": "metrics",
+ "aliasBy": "{{metric.label.phase}}",
+ "datasource": {
+ "uid": "$datasource"
+ },
+ "queryType": "timeSeriesList",
"refId": "A",
- "metricQuery": {
- "aliasBy": "",
+ "timeSeriesList": {
"alignmentPeriod": "$alignmentPeriod",
"crossSeriesReducer": "REDUCE_SUM",
- "perSeriesAligner": "ALIGN_MEAN",
- "filters": ["resource.type", "=", "k8s_container"],
+ "filters": ["metric.type", "=", "prometheus.googleapis.com/kube_pod_status_phase/gauge"],
"groupBys": ["metric.label.phase"],
- "metricKind": "",
- "metricType": "external.googleapis.com/prometheus/kube_pod_status_phase",
- "projectName": "$project",
- "unit": "",
- "valueType": ""
+ "perSeriesAligner": "ALIGN_MEAN",
+ "projectName": "$project"
}
}
],
- "thresholds": [],
- "timeFrom": null,
- "timeRegions": [],
- "timeShift": null,
"title": "kube_pod_status_phase [SUM]",
- "tooltip": { "shared": true, "sort": 0, "value_type": "individual" },
- "type": "graph",
- "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
- "yaxes": [
- { "format": "percent", "label": null, "logBase": 1, "max": null, "min": null, "show": true },
- { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
- ],
- "yaxis": { "align": false, "alignLevel": null }
+ "type": "timeseries"
},
{
- "aliasColors": {},
- "bars": true,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "decimals": 2,
+ "datasource": {
+ "type": "stackdriver",
+ "uid": "$datasource"
+ },
"description": "",
- "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] },
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 },
- "hiddenSeries": false,
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "bars",
+ "fillOpacity": 100,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "normal"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 3
+ },
"id": 2,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "total": false,
- "values": false,
- "sideWidth": 220
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true,
+ "width": 220
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
},
- "lines": false,
- "linewidth": 1,
- "nullPointMode": "connected",
- "options": { "alertThreshold": false },
- "percentage": true,
"pluginVersion": "7.4.0-pre",
- "pointradius": 2,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "spaceLength": 10,
- "stack": true,
- "steppedLine": false,
"targets": [
{
- "queryType": "metrics",
+ "aliasBy": "{{resource.label.namespace}}",
+ "datasource": {
+ "uid": "$datasource"
+ },
+ "queryType": "timeSeriesList",
"refId": "A",
- "metricQuery": {
- "aliasBy": "",
+ "timeSeriesList": {
"alignmentPeriod": "$alignmentPeriod",
"crossSeriesReducer": "REDUCE_SUM",
+ "filters": ["metric.type", "=", "prometheus.googleapis.com/kube_pod_container_status_ready/gauge"],
+ "groupBys": ["resource.label.namespace"],
"perSeriesAligner": "ALIGN_MEAN",
- "filters": ["resource.type", "=", "k8s_container"],
- "groupBys": ["metric.label.namespace"],
- "metricKind": "",
- "metricType": "external.googleapis.com/prometheus/kube_pod_status_ready",
- "projectName": "$project",
- "unit": "",
- "valueType": ""
+ "projectName": "$project"
}
}
],
- "thresholds": [],
- "timeFrom": null,
- "timeRegions": [],
- "timeShift": null,
"title": "kube_pod_status_ready [SUM]",
- "tooltip": { "shared": true, "sort": 0, "value_type": "individual" },
- "type": "graph",
- "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
- "yaxes": [
- { "format": "percent", "label": null, "logBase": 1, "max": null, "min": null, "show": true },
- { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
- ],
- "yaxis": { "align": false, "alignLevel": null }
+ "type": "timeseries"
},
{
- "aliasColors": {},
- "bars": true,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "decimals": 2,
+ "datasource": {
+ "type": "stackdriver",
+ "uid": "$datasource"
+ },
"description": "",
- "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] },
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
- "hiddenSeries": false,
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "bars",
+ "fillOpacity": 100,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "normal"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 11
+ },
"id": 3,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "total": false,
- "values": false,
- "sideWidth": 220
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true,
+ "width": 220
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
},
- "lines": false,
- "linewidth": 1,
- "nullPointMode": "connected",
- "options": { "alertThreshold": false },
- "percentage": true,
"pluginVersion": "7.4.0-pre",
- "pointradius": 2,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "spaceLength": 10,
- "stack": true,
- "steppedLine": false,
"targets": [
{
- "queryType": "metrics",
+ "aliasBy": "{{resource.label.namespace}}",
+ "datasource": {
+ "uid": "$datasource"
+ },
+ "queryType": "timeSeriesList",
"refId": "A",
- "metricQuery": {
- "aliasBy": "",
+ "timeSeriesList": {
"alignmentPeriod": "$alignmentPeriod",
"crossSeriesReducer": "REDUCE_SUM",
+ "filters": ["metric.type", "=", "prometheus.googleapis.com/kube_pod_container_status_running/gauge"],
+ "groupBys": ["resource.label.namespace"],
"perSeriesAligner": "ALIGN_MEAN",
- "filters": ["resource.type", "=", "k8s_container"],
- "groupBys": ["metric.label.namespace"],
- "metricKind": "",
- "metricType": "external.googleapis.com/prometheus/kube_pod_container_status_running",
- "projectName": "$project",
- "unit": "",
- "valueType": ""
+ "projectName": "$project"
}
}
],
- "thresholds": [],
- "timeFrom": null,
- "timeRegions": [],
- "timeShift": null,
"title": "kube_pod_container_status_running [SUM]",
- "tooltip": { "shared": true, "sort": 0, "value_type": "individual" },
- "type": "graph",
- "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
- "yaxes": [
- { "format": "percent", "label": null, "logBase": 1, "max": null, "min": null, "show": true },
- { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
- ],
- "yaxis": { "align": false, "alignLevel": null }
+ "type": "timeseries"
},
{
- "aliasColors": {},
- "bars": true,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "decimals": 2,
+ "datasource": {
+ "type": "stackdriver",
+ "uid": "$datasource"
+ },
"description": "",
- "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] },
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
- "hiddenSeries": false,
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "bars",
+ "fillOpacity": 100,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "normal"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 11
+ },
"id": 4,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "total": false,
- "values": false,
- "sideWidth": 220
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true,
+ "width": 220
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
},
- "lines": false,
- "linewidth": 1,
- "nullPointMode": "connected",
- "options": { "alertThreshold": false },
- "percentage": true,
"pluginVersion": "7.4.0-pre",
- "pointradius": 2,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "spaceLength": 10,
- "stack": true,
- "steppedLine": false,
"targets": [
{
- "queryType": "metrics",
+ "aliasBy": "{{metric.label.condition}}",
+ "datasource": {
+ "uid": "$datasource"
+ },
+ "queryType": "timeSeriesList",
"refId": "A",
- "metricQuery": {
- "aliasBy": "",
+ "timeSeriesList": {
"alignmentPeriod": "$alignmentPeriod",
"crossSeriesReducer": "REDUCE_SUM",
- "perSeriesAligner": "ALIGN_MEAN",
- "filters": ["resource.type", "=", "k8s_container"],
+ "filters": ["metric.type", "=", "prometheus.googleapis.com/kube_node_status_condition/gauge"],
"groupBys": ["metric.label.condition"],
- "metricKind": "",
- "metricType": "external.googleapis.com/prometheus/kube_node_status_condition",
- "projectName": "$project",
- "unit": "",
- "valueType": ""
+ "perSeriesAligner": "ALIGN_MEAN",
+ "projectName": "$project"
}
}
],
- "thresholds": [],
- "timeFrom": null,
- "timeRegions": [],
- "timeShift": null,
"title": "kube_node_status_condition [SUM]",
- "tooltip": { "shared": true, "sort": 0, "value_type": "individual" },
- "type": "graph",
- "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
- "yaxes": [
- { "format": "percent", "label": null, "logBase": 1, "max": null, "min": null, "show": true },
- { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
- ],
- "yaxis": { "align": false, "alignLevel": null }
+ "type": "timeseries"
},
{
- "aliasColors": {},
- "bars": true,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "decimals": 2,
+ "datasource": {
+ "type": "stackdriver",
+ "uid": "$datasource"
+ },
"description": "",
- "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] },
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 },
- "hiddenSeries": false,
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "bars",
+ "fillOpacity": 100,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "normal"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 19
+ },
"id": 5,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "total": false,
- "values": false,
- "sideWidth": 220
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true,
+ "width": 220
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
},
- "lines": false,
- "linewidth": 1,
- "nullPointMode": "connected",
- "options": { "alertThreshold": false },
- "percentage": true,
"pluginVersion": "7.4.0-pre",
- "pointradius": 2,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "spaceLength": 10,
- "stack": true,
- "steppedLine": false,
"targets": [
{
- "queryType": "metrics",
+ "aliasBy": "{{metric.label.node}}",
+ "datasource": {
+ "uid": "$datasource"
+ },
+ "queryType": "timeSeriesList",
"refId": "A",
- "metricQuery": {
- "aliasBy": "",
+ "timeSeriesList": {
"alignmentPeriod": "$alignmentPeriod",
- "perSeriesAligner": "ALIGN_MEAN",
- "filters": ["resource.type", "=", "k8s_container"],
+ "crossSeriesReducer": "REDUCE_NONE",
+ "filters": [
+ "metric.label.unit",
+ "=",
+ "integer",
+ "AND",
+ "metric.type",
+ "=",
+ "prometheus.googleapis.com/kube_node_status_capacity/gauge"
+ ],
"groupBys": [],
- "metricKind": "",
- "metricType": "external.googleapis.com/prometheus/kube_node_status_capacity_pods",
- "projectName": "$project",
- "unit": "",
- "valueType": ""
+ "perSeriesAligner": "ALIGN_MEAN",
+ "projectName": "$project"
}
}
],
- "thresholds": [],
- "timeFrom": null,
- "timeRegions": [],
- "timeShift": null,
"title": "kube_node_status_capacity_pods",
- "tooltip": { "shared": true, "sort": 0, "value_type": "individual" },
- "type": "graph",
- "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
- "yaxes": [
- { "format": "percent", "label": null, "logBase": 1, "max": null, "min": null, "show": true },
- { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
- ],
- "yaxis": { "align": false, "alignLevel": null }
+ "type": "timeseries"
},
{
- "aliasColors": {},
- "bars": true,
- "dashLength": 10,
- "dashes": false,
- "datasource": "$datasource",
- "decimals": 2,
+ "datasource": {
+ "type": "stackdriver",
+ "uid": "$datasource"
+ },
"description": "",
- "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] },
- "fill": 1,
- "fillGradient": 0,
- "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 },
- "hiddenSeries": false,
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "bars",
+ "fillOpacity": 100,
+ "gradientMode": "none",
+ "hideFrom": {
+ "legend": false,
+ "tooltip": false,
+ "viz": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "never",
+ "spanNulls": true,
+ "stacking": {
+ "group": "A",
+ "mode": "normal"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ },
+ {
+ "color": "red",
+ "value": 80
+ }
+ ]
+ }
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 19
+ },
"id": 6,
- "legend": {
- "alignAsTable": false,
- "avg": false,
- "current": false,
- "max": false,
- "min": false,
- "rightSide": false,
- "show": true,
- "total": false,
- "values": false,
- "sideWidth": 220
+ "options": {
+ "legend": {
+ "calcs": [],
+ "displayMode": "list",
+ "placement": "bottom",
+ "showLegend": true,
+ "width": 220
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
},
- "lines": false,
- "linewidth": 1,
- "nullPointMode": "connected",
- "options": { "alertThreshold": false },
- "percentage": true,
"pluginVersion": "7.4.0-pre",
- "pointradius": 2,
- "points": false,
- "renderer": "flot",
- "seriesOverrides": [],
- "spaceLength": 10,
- "stack": true,
- "steppedLine": false,
"targets": [
{
- "queryType": "metrics",
+ "aliasBy": "{{metric.label.node}}",
+ "datasource": {
+ "uid": "$datasource"
+ },
+ "queryType": "timeSeriesList",
"refId": "A",
- "metricQuery": {
- "aliasBy": "",
+ "timeSeriesList": {
"alignmentPeriod": "$alignmentPeriod",
- "perSeriesAligner": "ALIGN_MEAN",
- "filters": ["resource.type", "=", "k8s_container"],
+ "filters": [
+ "metric.label.unit",
+ "=",
+ "integer",
+ "AND",
+ "metric.type",
+ "=",
+ "prometheus.googleapis.com/kube_node_status_allocatable/gauge"
+ ],
"groupBys": [],
- "metricKind": "",
- "metricType": "external.googleapis.com/prometheus/kube_node_status_allocatable_pods",
- "projectName": "$project",
- "unit": "",
- "valueType": ""
+ "perSeriesAligner": "ALIGN_MEAN",
+ "projectName": "$project"
}
}
],
- "thresholds": [],
- "timeFrom": null,
- "timeRegions": [],
- "timeShift": null,
"title": "kube_node_status_allocatable_pods",
- "tooltip": { "shared": true, "sort": 0, "value_type": "individual" },
- "type": "graph",
- "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
- "yaxes": [
- { "format": "percent", "label": null, "logBase": 1, "max": null, "min": null, "show": true },
- { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
- ],
- "yaxis": { "align": false, "alignLevel": null }
+ "type": "timeseries"
}
],
- "schemaVersion": 26,
+ "refresh": "",
+ "schemaVersion": 39,
"tags": ["Compute", "Cloud Monitoring", "GCP"],
"templating": {
"list": [
{
"current": {},
- "description": null,
- "error": null,
"hide": 0,
"includeAll": false,
"label": "Datasource",
@@ -493,12 +713,11 @@
"type": "datasource"
},
{
- "allValue": null,
"current": {},
- "datasource": "$datasource",
+ "datasource": {
+ "uid": "$datasource"
+ },
"definition": "Google Cloud Monitoring - Projects",
- "description": null,
- "error": null,
"hide": 0,
"includeAll": false,
"label": "Project",
@@ -521,46 +740,39 @@
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
- "tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
- "allValue": null,
- "current": { "selected": false, "text": "grafana auto", "value": "grafana-auto" },
- "datasource": "${datasource}",
+ "current": {
+ "selected": false,
+ "text": "grafana auto",
+ "value": "grafana-auto"
+ },
+ "datasource": {
+ "uid": "${datasource}"
+ },
"definition": "",
- "description": null,
- "error": null,
"hide": 0,
"includeAll": false,
"label": "Alignment Period",
"multi": false,
"name": "alignmentPeriod",
- "options": [
- { "selected": true, "text": "grafana auto", "value": "grafana-auto" },
- { "selected": false, "text": "stackdriver auto", "value": "stackdriver-auto" },
- { "selected": false, "text": "cloud monitoring auto", "value": "cloud-monitoring-auto" },
- { "selected": false, "text": "1m", "value": "+60s" },
- { "selected": false, "text": "2m", "value": "+120s" },
- { "selected": false, "text": "5m", "value": "+300s" },
- { "selected": false, "text": "10m", "value": "+600s" },
- { "selected": false, "text": "30m", "value": "+1800s" },
- { "selected": false, "text": "1h", "value": "+3600s" },
- { "selected": false, "text": "3h", "value": "+7200s" },
- { "selected": false, "text": "6h", "value": "+21600s" },
- { "selected": false, "text": "1d", "value": "+86400s" },
- { "selected": false, "text": "3d", "value": "+259200s" },
- { "selected": false, "text": "1w", "value": "+604800s" }
- ],
+ "options": [],
"query": {
"labelKey": "",
"loading": false,
"projectName": "$project",
"projects": [
- { "name": "project-1", "value": "project-1" },
- { "name": "project-2", "value": "project-2" }
+ {
+ "name": "project-1",
+ "value": "project-1"
+ },
+ {
+ "name": "project-2",
+ "value": "project-2"
+ }
],
"refId": "CloudMonitoringVariableQueryEditor-VariableQuery",
"selectedMetricType": "actions.googleapis.com/smarthome_action/num_active_users",
@@ -569,12 +781,11 @@
"selectedService": "actions.googleapis.com",
"sloServices": []
},
- "refresh": 0,
+ "refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
- "tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
@@ -586,5 +797,6 @@
"timezone": "",
"title": "GKE Prometheus Pod/Node Monitoring",
"uid": "",
- "version": 5
+ "version": 5,
+ "weekStart": ""
}
diff --git a/public/app/plugins/datasource/cloudwatch/__mocks__/queries.ts b/public/app/plugins/datasource/cloudwatch/__mocks__/queries.ts
index 00c5d4c0d18..36b3a2df017 100644
--- a/public/app/plugins/datasource/cloudwatch/__mocks__/queries.ts
+++ b/public/app/plugins/datasource/cloudwatch/__mocks__/queries.ts
@@ -44,7 +44,7 @@ export const validMetricQueryBuilderQuery: CloudWatchMetricsQuery = {
region: 'us-east-1',
namespace: 'ec2',
dimensions: { somekey: 'somevalue' },
- metricQueryType: MetricQueryType.Query,
+ metricQueryType: MetricQueryType.Insights,
metricEditorMode: MetricEditorMode.Builder,
sql: {
from: {
@@ -77,7 +77,7 @@ export const validMetricQueryCodeQuery: CloudWatchMetricsQuery = {
statistic: 'Average',
sqlExpression: 'SELECT * FROM "AWS/EC2" WHERE "InstanceId" = \'i-123\'',
refId: 'A',
- metricQueryType: MetricQueryType.Query,
+ metricQueryType: MetricQueryType.Insights,
metricEditorMode: MetricEditorMode.Code,
hide: false,
};
diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.tsx
index 6798f650186..83263fac95a 100644
--- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.tsx
+++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MetricsQueryEditor.tsx
@@ -31,7 +31,7 @@ export interface Props extends QueryEditorProps> = [
{ label: 'Metric Search', value: MetricQueryType.Search },
- { label: 'Metric Query', value: MetricQueryType.Query },
+ { label: 'Metric Insights', value: MetricQueryType.Insights },
];
const editorModes = [
{ label: 'Builder', value: MetricEditorMode.Builder },
@@ -48,7 +48,7 @@ export const MetricsQueryEditor = (props: Props) => {
(newMetricEditorMode: MetricEditorMode) => {
if (
codeEditorIsDirty &&
- query.metricQueryType === MetricQueryType.Query &&
+ query.metricQueryType === MetricQueryType.Insights &&
query.metricEditorMode === MetricEditorMode.Code
) {
setShowConfirm(true);
@@ -90,7 +90,7 @@ export const MetricsQueryEditor = (props: Props) => {
{
onChange({
...query,
...DEFAULT_METRICS_QUERY,
- metricQueryType: MetricQueryType.Query,
+ metricQueryType: MetricQueryType.Insights,
metricEditorMode: MetricEditorMode.Builder,
});
}}
@@ -152,7 +152,7 @@ export const MetricsQueryEditor = (props: Props) => {
)}
>
)}
- {query.metricQueryType === MetricQueryType.Query && (
+ {query.metricQueryType === MetricQueryType.Insights && (
<>
{query.metricEditorMode === MetricEditorMode.Code && (
({
region: 'us-east-1',
namespace: 'ec2',
dimensions: { somekey: 'somevalue' },
- metricQueryType: MetricQueryType.Query,
+ metricQueryType: MetricQueryType.Insights,
metricEditorMode: MetricEditorMode.Builder,
sql: sql,
});
diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/SQLBuilderSelectRow.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/SQLBuilderSelectRow.test.tsx
index fc8178311b0..65b709bf72c 100644
--- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/SQLBuilderSelectRow.test.tsx
+++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/SQLBuilderSelectRow.test.tsx
@@ -16,7 +16,7 @@ const makeSQLQuery = (sql?: SQLExpression): CloudWatchMetricsQuery => ({
region: 'us-east-1',
namespace: 'ec2',
dimensions: { somekey: 'somevalue' },
- metricQueryType: MetricQueryType.Query,
+ metricQueryType: MetricQueryType.Insights,
metricEditorMode: MetricEditorMode.Builder,
sql: sql,
});
diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/SQLGroupBy.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/SQLGroupBy.test.tsx
index 388fe8722c6..9f98569e3e7 100644
--- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/SQLGroupBy.test.tsx
+++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/SQLGroupBy.test.tsx
@@ -17,7 +17,7 @@ const makeSQLQuery = (sql?: SQLExpression): CloudWatchMetricsQuery => ({
region: 'us-east-1',
namespace: 'ec2',
dimensions: { somekey: 'somevalue' },
- metricQueryType: MetricQueryType.Query,
+ metricQueryType: MetricQueryType.Insights,
metricEditorMode: MetricEditorMode.Builder,
sql: sql,
});
diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.test.tsx
index 7276d65efbc..ed093bed858 100644
--- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.test.tsx
+++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryEditor.test.tsx
@@ -21,7 +21,7 @@ import { QueryEditor } from './QueryEditor';
const migratedFields = {
statistic: 'Average',
metricEditorMode: MetricEditorMode.Builder,
- metricQueryType: MetricQueryType.Query,
+ metricQueryType: MetricQueryType.Insights,
};
const props: QueryEditorProps = {
@@ -201,12 +201,12 @@ describe('QueryEditor should render right editor', () => {
describe('should not be displayed when a monitoring account is returned and', () => {
const cases: MonitoringBadgeScenario[] = [
{
- name: 'it is metric query builder query and toggle is enabled',
+ name: 'it is metric insights builder query and toggle is enabled',
query: validMetricQueryBuilderQuery,
toggle: true,
},
{
- name: 'it is metric query code query and toggle is not enabled',
+ name: 'it is metric insights code query and toggle is not enabled',
query: validMetricQueryCodeQuery,
toggle: false,
},
@@ -232,24 +232,24 @@ describe('QueryEditor should render right editor', () => {
});
describe('QueryHeader', () => {
- it('should display metric actions in header when metric query is used', async () => {
+ it('should display metric actions in header when metric insights is used', async () => {
render( );
expect(await screen.findByText('CloudWatch Metrics')).toBeInTheDocument();
expect(screen.getByLabelText(/Region.*/)).toBeInTheDocument();
expect(screen.getByLabelText('Builder')).toBeInTheDocument();
expect(screen.getByLabelText('Code')).toBeInTheDocument();
- expect(screen.getByText('Metric Query')).toBeInTheDocument();
+ expect(screen.getByText('Metric Insights')).toBeInTheDocument();
});
- it('should display metric actions in header when metric query is used', async () => {
+ it('should display metric actions in header when metric insights is used', async () => {
render( );
expect(await screen.findByText('CloudWatch Logs')).toBeInTheDocument();
expect(screen.getByLabelText(/Region.*/)).toBeInTheDocument();
expect(screen.queryByLabelText('Builder')).not.toBeInTheDocument();
expect(screen.queryByLabelText('Code')).not.toBeInTheDocument();
- expect(screen.queryByText('Metric Query')).not.toBeInTheDocument();
+ expect(screen.queryByText('Metric Insights')).not.toBeInTheDocument();
});
});
@@ -270,18 +270,18 @@ describe('QueryEditor should render right editor', () => {
expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy();
});
- it('when metric query type is metric query and editor mode is builder', async () => {
+ it('when metric query type is metric insights and editor mode is builder', async () => {
render( );
- expect(await screen.findByText('Metric Query')).toBeInTheDocument();
+ expect(await screen.findByText('Metric Insights')).toBeInTheDocument();
const radio = screen.getByLabelText('Builder');
expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy();
});
- it('when metric query type is metric query and editor mode is raw', async () => {
+ it('when metric query type is metric Insights and editor mode is raw', async () => {
render( );
- expect(await screen.findByText('Metric Query')).toBeInTheDocument();
+ expect(await screen.findByText('Metric Insights')).toBeInTheDocument();
const radio = screen.getByLabelText('Code');
expect(radio instanceof HTMLInputElement && radio.checked).toBeTruthy();
});
@@ -347,7 +347,7 @@ describe('QueryEditor should render right editor', () => {
config.featureToggles.cloudwatchMetricInsightsCrossAccount = true;
props.datasource.resources.getAccounts = jest.fn().mockResolvedValue(['account123']);
render( );
- await screen.findByText('Metric Query');
+ await screen.findByText('Metric Insights');
expect(await screen.findByText('Account')).toBeInTheDocument();
});
});
diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.tsx
index 7e77f93b4a5..7908c3437ac 100644
--- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.tsx
+++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/QueryHeader.tsx
@@ -58,12 +58,16 @@ const QueryHeader = ({
onChange({ ...query, region });
}
};
+ const metricInsightsCrossAccountEnabled = config.featureToggles.cloudwatchMetricInsightsCrossAccount;
const shouldDisplayMonitoringBadge =
config.featureToggles.cloudWatchCrossAccountQuerying &&
isMonitoringAccount &&
(query.queryMode === 'Logs' ||
- (isCloudWatchMetricsQuery(query) && query.metricQueryType === MetricQueryType.Search));
+ (isCloudWatchMetricsQuery(query) && query.metricQueryType === MetricQueryType.Search) ||
+ (metricInsightsCrossAccountEnabled &&
+ isCloudWatchMetricsQuery(query) &&
+ query.metricQueryType === MetricQueryType.Insights));
return (
<>
diff --git a/public/app/plugins/datasource/cloudwatch/components/shared/MetricStatEditor/MetricStatEditor.tsx b/public/app/plugins/datasource/cloudwatch/components/shared/MetricStatEditor/MetricStatEditor.tsx
index 9f9ecd45fdf..63d54a33e56 100644
--- a/public/app/plugins/datasource/cloudwatch/components/shared/MetricStatEditor/MetricStatEditor.tsx
+++ b/public/app/plugins/datasource/cloudwatch/components/shared/MetricStatEditor/MetricStatEditor.tsx
@@ -26,7 +26,7 @@ const percentileSyntaxRE = /^(p|tm|tc|ts|wm)\d{2}(?:\.\d{1,2})?$/;
const boundariesInnerParenthesesSyntax = `\\d*(\\.\\d+)?%?:\\d*(\\.\\d+)?%?`;
const boundariesSyntaxRE = new RegExp(`^(PR|TM|TC|TS|WM)\\((${boundariesInnerParenthesesSyntax})\\)$`);
-// used in both Metric Query editor and in Annotations Editor
+// used in both Metric query editor and in Annotations Editor
export const MetricStatEditor = ({
refId,
metricStat,
diff --git a/public/app/plugins/datasource/cloudwatch/dataquery.cue b/public/app/plugins/datasource/cloudwatch/dataquery.cue
index bc3cb0d1821..e1a5277f394 100644
--- a/public/app/plugins/datasource/cloudwatch/dataquery.cue
+++ b/public/app/plugins/datasource/cloudwatch/dataquery.cue
@@ -56,7 +56,7 @@ composableKinds: DataQuery: {
// Whether a query is a Metrics, Logs, or Annotations query
queryMode?: #CloudWatchQueryMode
- // Whether to use a metric search or metric query. Metric query is referred to as "Metrics Insights" in the AWS console.
+ // Whether to use a metric search or metric insights query
metricQueryType?: #MetricQueryType
// Whether to use the query builder or code editor to create the query
metricEditorMode?: #MetricEditorMode
@@ -69,14 +69,14 @@ composableKinds: DataQuery: {
label?: string
// Math expression query
expression?: string
- // When the metric query type is `metricQueryType` is set to `Query`, this field is used to specify the query string.
+ // When the metric query type is set to `Insights`, this field is used to specify the query string.
sqlExpression?: string
- // When the metric query type is `metricQueryType` is set to `Query` and the `metricEditorMode` is set to `Builder`, this field is used to build up an object representation of a SQL query.
+ // When the metric query type is set to `Insights` and the `metricEditorMode` is set to `Builder`, this field is used to build up an object representation of a SQL query.
sql?: #SQLExpression
} @cuetsy(kind="interface")
#CloudWatchQueryMode: "Metrics" | "Logs" | "Annotations" @cuetsy(kind="type")
- #MetricQueryType: 0 | 1 @cuetsy(kind="enum", memberNames="Search|Query")
+ #MetricQueryType: 0 | 1 @cuetsy(kind="enum", memberNames="Search|Insights")
#MetricEditorMode: 0 | 1 @cuetsy(kind="enum", memberNames="Builder|Code")
#SQLExpression: {
// SELECT part of the SQL expression
diff --git a/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts b/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts
index 9cf62e4b588..ff71dad3656 100644
--- a/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts
+++ b/public/app/plugins/datasource/cloudwatch/dataquery.gen.ts
@@ -84,7 +84,7 @@ export interface CloudWatchMetricsQuery extends common.DataQuery, MetricStat {
*/
metricEditorMode?: MetricEditorMode;
/**
- * Whether to use a metric search or metric query. Metric query is referred to as "Metrics Insights" in the AWS console.
+ * Whether to use a metric search or metric insights query
*/
metricQueryType?: MetricQueryType;
/**
@@ -92,11 +92,11 @@ export interface CloudWatchMetricsQuery extends common.DataQuery, MetricStat {
*/
queryMode?: CloudWatchQueryMode;
/**
- * When the metric query type is `metricQueryType` is set to `Query` and the `metricEditorMode` is set to `Builder`, this field is used to build up an object representation of a SQL query.
+ * When the metric query type is set to `Insights` and the `metricEditorMode` is set to `Builder`, this field is used to build up an object representation of a SQL query.
*/
sql?: SQLExpression;
/**
- * When the metric query type is `metricQueryType` is set to `Query`, this field is used to specify the query string.
+ * When the metric query type is set to `Insights`, this field is used to specify the query string.
*/
sqlExpression?: string;
}
@@ -104,7 +104,7 @@ export interface CloudWatchMetricsQuery extends common.DataQuery, MetricStat {
export type CloudWatchQueryMode = ('Metrics' | 'Logs' | 'Annotations');
export enum MetricQueryType {
- Query = 1,
+ Insights = 1,
Search = 0,
}
diff --git a/public/app/plugins/datasource/cloudwatch/migrations/dashboardMigrations.ts b/public/app/plugins/datasource/cloudwatch/migrations/dashboardMigrations.ts
index 03da65ed526..d492a5ad3ce 100644
--- a/public/app/plugins/datasource/cloudwatch/migrations/dashboardMigrations.ts
+++ b/public/app/plugins/datasource/cloudwatch/migrations/dashboardMigrations.ts
@@ -58,7 +58,7 @@ export function migrateCloudWatchQuery(query: CloudWatchMetricsQuery) {
}
if (!query.hasOwnProperty('metricEditorMode')) {
- if (query.metricQueryType === MetricQueryType.Query) {
+ if (query.metricQueryType === MetricQueryType.Insights) {
query.metricEditorMode = MetricEditorMode.Code;
} else {
query.metricEditorMode = query.expression ? MetricEditorMode.Code : MetricEditorMode.Builder;
diff --git a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts
index 0ac0bedbc3e..637328f022a 100644
--- a/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts
+++ b/public/app/plugins/datasource/cloudwatch/query-runner/CloudWatchMetricsQueryRunner.test.ts
@@ -493,7 +493,7 @@ describe('CloudWatchMetricsQueryRunner', () => {
matchExact: true,
statistic: '',
expression: '',
- metricQueryType: MetricQueryType.Query,
+ metricQueryType: MetricQueryType.Insights,
metricEditorMode: MetricEditorMode.Code,
sqlExpression: 'SELECT SUM($metric) FROM "$namespace" GROUP BY InstanceId,InstanceType LIMIT $limit',
},
@@ -734,7 +734,7 @@ describe('CloudWatchMetricsQueryRunner', () => {
matchExact: true,
statistic: '',
expression: '',
- metricQueryType: MetricQueryType.Query,
+ metricQueryType: MetricQueryType.Insights,
metricEditorMode: MetricEditorMode.Code,
sqlExpression: 'SELECT SUM($metric) FROM "$namespace" GROUP BY ${labels:raw} LIMIT $limit',
};
@@ -968,11 +968,11 @@ describe('CloudWatchMetricsQueryRunner', () => {
});
});
- describe('metric query queries', () => {
+ describe('metric insights queries', () => {
beforeEach(() => {
baseQuery = {
...baseQuery,
- metricQueryType: MetricQueryType.Query,
+ metricQueryType: MetricQueryType.Insights,
metricEditorMode: MetricEditorMode.Code,
};
});
diff --git a/public/app/plugins/datasource/cloudwatch/tracking.ts b/public/app/plugins/datasource/cloudwatch/tracking.ts
index a44c6d866a3..3607b3ef99a 100644
--- a/public/app/plugins/datasource/cloudwatch/tracking.ts
+++ b/public/app/plugins/datasource/cloudwatch/tracking.ts
@@ -106,12 +106,12 @@ export const onDashboardLoadedHandler = ({
q.metricQueryType === MetricQueryType.Search && q.metricEditorMode === MetricEditorMode.Code
);
e.metrics_search_match_exact_count += +Boolean(isMetricSearchBuilder(q) && q.matchExact);
- e.metrics_query_count += +Boolean(q.metricQueryType === MetricQueryType.Query);
+ e.metrics_query_count += +Boolean(q.metricQueryType === MetricQueryType.Insights);
e.metrics_query_builder_count += +Boolean(
- q.metricQueryType === MetricQueryType.Query && q.metricEditorMode === MetricEditorMode.Builder
+ q.metricQueryType === MetricQueryType.Insights && q.metricEditorMode === MetricEditorMode.Builder
);
e.metrics_query_code_count += +Boolean(
- q.metricQueryType === MetricQueryType.Query && q.metricEditorMode === MetricEditorMode.Code
+ q.metricQueryType === MetricQueryType.Insights && q.metricEditorMode === MetricEditorMode.Code
);
e.metrics_queries_with_account_count += +Boolean(
config.featureToggles.cloudWatchCrossAccountQuerying && isMetricSearchBuilder(q) && q.accountId
diff --git a/public/app/plugins/datasource/cloudwatch/utils/utils.ts b/public/app/plugins/datasource/cloudwatch/utils/utils.ts
index 1666c8a3865..b98751ce156 100644
--- a/public/app/plugins/datasource/cloudwatch/utils/utils.ts
+++ b/public/app/plugins/datasource/cloudwatch/utils/utils.ts
@@ -21,7 +21,7 @@ export const filterMetricsQuery = (query: CloudWatchMetricsQuery): boolean => {
return !!namespace && !!metricName && !!statistic;
} else if (metricQueryType === MetricQueryType.Search && metricEditorMode === MetricEditorMode.Code) {
return !!expression;
- } else if (metricQueryType === MetricQueryType.Query) {
+ } else if (metricQueryType === MetricQueryType.Insights) {
// still TBD how to validate the visual query builder for SQL
return !!sqlExpression;
}
diff --git a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx
index 6dcca3cea3d..35e05ae4794 100644
--- a/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx
+++ b/public/app/plugins/datasource/tempo/configuration/ConfigEditor.tsx
@@ -23,6 +23,7 @@ import { SecureSocksProxySettings, useStyles2, Divider, Stack } from '@grafana/u
import { QuerySettings } from './QuerySettings';
import { ServiceGraphSettings } from './ServiceGraphSettings';
+import { StreamingSection } from './StreamingSection';
import { TraceQLSearchSettings } from './TraceQLSearchSettings';
export type Props = DataSourcePluginOptionsEditorProps;
@@ -48,8 +49,11 @@ export const ConfigEditor = ({ options, onOptionsChange }: Props) => {
onChange: onOptionsChange,
})}
/>
-
+
+
+
+
diff --git a/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx b/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx
new file mode 100644
index 00000000000..bba18945603
--- /dev/null
+++ b/public/app/plugins/datasource/tempo/configuration/StreamingSection.tsx
@@ -0,0 +1,81 @@
+import { css } from '@emotion/css';
+import React from 'react';
+
+import {
+ DataSourceJsonData,
+ DataSourcePluginOptionsEditorProps,
+ GrafanaTheme2,
+ updateDatasourcePluginJsonDataOption,
+} from '@grafana/data';
+import { ConfigSection } from '@grafana/experimental';
+import { InlineFieldRow, InlineField, InlineSwitch, Alert, Stack, useStyles2 } from '@grafana/ui';
+
+import { FeatureName, featuresToTempoVersion } from '../datasource';
+
+interface StreamingOptions extends DataSourceJsonData {
+ streamingEnabled?: {
+ search?: boolean;
+ };
+}
+interface Props extends DataSourcePluginOptionsEditorProps {}
+
+export const StreamingSection = ({ options, onOptionsChange }: Props) => {
+ const styles = useStyles2(getStyles);
+ return (
+
+ {`Enable streaming for different Tempo features.
+ Currently supported only for search queries and from Tempo version ${featuresToTempoVersion[FeatureName.streaming]} onwards.`}
+
+ Learn more
+
+
+ }
+ >
+
+ If your Tempo instance is behind a load balancer or proxy that does not supporting gRPC or HTTP2, streaming will
+ probably not work and should be disabled.
+
+
+
+ ) => {
+ updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'streamingEnabled', {
+ ...options.jsonData.streamingEnabled,
+ search: event.currentTarget.checked,
+ });
+ }}
+ />
+
+
+
+ );
+};
+const getStyles = (theme: GrafanaTheme2) => {
+ return {
+ a: css({
+ color: theme.colors.text.link,
+ textDecoration: 'underline',
+ marginLeft: '5px',
+ '&:hover': {
+ textDecoration: 'none',
+ },
+ }),
+ };
+};
diff --git a/public/app/plugins/datasource/tempo/datasource.test.ts b/public/app/plugins/datasource/tempo/datasource.test.ts
index 771220631ee..43dbb94b2a7 100644
--- a/public/app/plugins/datasource/tempo/datasource.test.ts
+++ b/public/app/plugins/datasource/tempo/datasource.test.ts
@@ -75,7 +75,7 @@ describe('Tempo data source', () => {
const range = {
from: dateTime(new Date(2022, 8, 13, 16, 0, 0, 0)),
to: dateTime(new Date(2022, 8, 13, 16, 15, 0, 0)),
- raw: { from: '15m', to: 'now' },
+ raw: { from: 'now-15m', to: 'now' },
};
const traceqlQuery = {
targets: [{ refId: 'refid1', queryType: 'traceql', query: '{}' }],
@@ -365,9 +365,14 @@ describe('Tempo data source', () => {
describe('test the testDatasource function', () => {
it('should return a success msg if response.ok is true', async () => {
mockObservable = () => of({ ok: true });
+ const handleStreamingSearch = jest
+ .spyOn(TempoDatasource.prototype, 'handleStreamingSearch')
+ .mockImplementation(() => of({ data: [] }));
+
const ds = new TempoDatasource(defaultSettings);
const response = await ds.testDatasource();
expect(response.status).toBe('success');
+ expect(handleStreamingSearch).toHaveBeenCalled();
});
});
@@ -389,7 +394,7 @@ describe('Tempo data source', () => {
const range = {
from: dateTime(new Date(2022, 8, 13, 16, 0, 0, 0)),
to: dateTime(new Date(2022, 8, 13, 16, 15, 0, 0)),
- raw: { from: '15m', to: 'now' },
+ raw: { from: 'now-15m', to: 'now' },
};
const request = ds.traceIdQueryRequest(
@@ -434,7 +439,7 @@ describe('Tempo data source', () => {
range: {
from: dateTime(new Date(2022, 8, 13, 16, 0, 0, 0)),
to: dateTime(new Date(2022, 8, 13, 16, 15, 0, 0)),
- raw: { from: '15m', to: 'now' },
+ raw: { from: 'now-15m', to: 'now' },
},
},
[{ refId: 'refid1', queryType: 'traceql', query: '' } as TempoQuery]
@@ -1264,6 +1269,9 @@ export const defaultSettings: DataSourceInstanceSettings = {
nodeGraph: {
enabled: true,
},
+ streamingEnabled: {
+ search: true,
+ },
},
readOnly: false,
};
diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts
index 93db409c37f..ae754a0c2c7 100644
--- a/public/app/plugins/datasource/tempo/datasource.ts
+++ b/public/app/plugins/datasource/tempo/datasource.ts
@@ -1,5 +1,5 @@
import { groupBy } from 'lodash';
-import { EMPTY, from, lastValueFrom, merge, Observable, of } from 'rxjs';
+import { EMPTY, forkJoin, from, lastValueFrom, merge, Observable, of } from 'rxjs';
import { catchError, concatMap, map, mergeMap, toArray } from 'rxjs/operators';
import semver from 'semver';
@@ -41,7 +41,7 @@ import {
} from './SearchTraceQLEditor/utils';
import { TempoVariableQuery, TempoVariableQueryType } from './VariableQueryEditor';
import { PrometheusDatasource, PromQuery } from './_importedDependencies/datasources/prometheus/types';
-import { TraceqlFilter, TraceqlSearchScope } from './dataquery.gen';
+import { SearchTableType, TraceqlFilter, TraceqlSearchScope } from './dataquery.gen';
import {
defaultTableFilter,
durationMetric,
@@ -69,7 +69,7 @@ import { TempoVariableSupport } from './variables';
export const DEFAULT_LIMIT = 20;
export const DEFAULT_SPSS = 3; // spans per span set
-enum FeatureName {
+export enum FeatureName {
streaming = 'streaming',
}
@@ -77,7 +77,7 @@ enum FeatureName {
** feature available. If the running Tempo instance on the user's backend is older than the
** target version, the feature is disabled in Grafana (frontend).
*/
-const featuresToTempoVersion = {
+export const featuresToTempoVersion = {
[FeatureName.streaming]: '2.2.0',
};
@@ -115,6 +115,10 @@ export class TempoDatasource extends DataSourceWithBackend): Observable {
const subQueries: Array> = [];
const filteredTargets = options.targets.filter((target) => !target.hide);
@@ -317,7 +342,7 @@ export class TempoDatasource extends DataSourceWithBackend => {
- if (
- config.featureToggles.traceQLStreaming &&
- this.isFeatureAvailable(FeatureName.streaming) &&
- config.liveEnabled
- ) {
+ if (this.isStreamingSearchEnabled()) {
return this.handleStreamingSearch(options, targets.traceql, queryValue);
} else {
return this._request('/api/search', {
@@ -717,24 +734,86 @@ export class TempoDatasource extends DataSourceWithBackend {
+ const observables = [];
+
const options: BackendSrvRequest = {
headers: {},
method: 'GET',
url: `${this.instanceSettings.url}/api/echo`,
};
-
- return await lastValueFrom(
+ observables.push(
getBackendSrv()
.fetch(options)
.pipe(
mergeMap(() => {
- return of({ status: 'success', message: 'Data source successfully connected.' });
+ return of({ status: 'success', message: 'Health check succeeded' });
}),
catchError((err) => {
- return of({ status: 'error', message: getErrorMessage(err.data.message, 'Unable to connect with Tempo') });
+ return of({
+ status: 'error',
+ message: getErrorMessage(err.data.message, 'Unable to connect with Tempo'),
+ });
})
)
);
+
+ if (this.streamingEnabled?.search) {
+ const now = new Date();
+ const from = new Date(now);
+ from.setMinutes(from.getMinutes() - 15);
+ observables.push(
+ this.handleStreamingSearch(
+ {
+ range: {
+ from: dateTime(from),
+ to: dateTime(now),
+ raw: { from: 'now-15m', to: 'now' },
+ },
+ requestId: '',
+ interval: '',
+ intervalMs: 0,
+ scopedVars: {},
+ targets: [],
+ timezone: '',
+ app: '',
+ startTime: 0,
+ },
+ [
+ {
+ datasource: this.instanceSettings,
+ limit: 1,
+ query: '{}',
+ queryType: 'traceql',
+ refId: 'A',
+ tableType: SearchTableType.Traces,
+ filters: [],
+ },
+ ],
+ '{}'
+ ).pipe(
+ mergeMap(() => {
+ return of({ status: 'success', message: 'Streaming test succeeded.' });
+ }),
+ catchError((err) => {
+ return of({
+ status: 'error',
+ message: getErrorMessage(err.data.message, 'Test for streaming failed, consider disabling streaming'),
+ });
+ })
+ )
+ );
+ }
+
+ return await lastValueFrom(
+ forkJoin(observables).pipe(
+ mergeMap((observableResults) => {
+ const erroredResult = observableResults.find((result) => result.status !== 'success');
+ return erroredResult
+ ? of(erroredResult)
+ : of({ status: 'success', message: 'Successfully connected to Tempo data source.' });
+ })
+ )
+ );
}
getQueryDisplayText(query: TempoQuery) {
diff --git a/public/app/plugins/datasource/tempo/streaming.ts b/public/app/plugins/datasource/tempo/streaming.ts
index 23eef3182ce..71c336edf2f 100644
--- a/public/app/plugins/datasource/tempo/streaming.ts
+++ b/public/app/plugins/datasource/tempo/streaming.ts
@@ -20,6 +20,7 @@ import { SearchStreamingState } from './dataquery.gen';
import { DEFAULT_SPSS, TempoDatasource } from './datasource';
import { formatTraceQLResponse } from './resultTransformer';
import { SearchMetrics, TempoJsonData, TempoQuery } from './types';
+
function getLiveStreamKey(): string {
return uuidv4();
}
diff --git a/public/app/plugins/datasource/tempo/types.ts b/public/app/plugins/datasource/tempo/types.ts
index 09c31e05b93..31439867edd 100644
--- a/public/app/plugins/datasource/tempo/types.ts
+++ b/public/app/plugins/datasource/tempo/types.ts
@@ -21,6 +21,9 @@ export interface TempoJsonData extends DataSourceJsonData {
spanStartTimeShift?: string;
spanEndTimeShift?: string;
};
+ streamingEnabled?: {
+ search?: boolean;
+ };
}
export interface TempoQuery extends TempoBase {
diff --git a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
index 0e0b3ce4fc6..2e615646324 100644
--- a/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
+++ b/public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
@@ -25,9 +25,9 @@ import {
fetchAllPromAndRulerRulesAction,
fetchPromAndRulerRulesAction,
} from 'app/features/alerting/unified/state/actions';
-import { parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
import { Annotation } from 'app/features/alerting/unified/utils/constants';
import { GRAFANA_DATASOURCE_NAME, GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
+import { parsePromQLStyleMatcherLooseSafe } from 'app/features/alerting/unified/utils/matchers';
import {
isAsyncRequestMapSlicePartiallyDispatched,
isAsyncRequestMapSlicePartiallyFulfilled,
@@ -132,7 +132,7 @@ function UnifiedAlertList(props: PanelProps) {
};
const matcherList = useMemo(
- () => parseMatchers(parsedOptions.alertInstanceLabelFilter),
+ () => parsePromQLStyleMatcherLooseSafe(parsedOptions.alertInstanceLabelFilter),
[parsedOptions.alertInstanceLabelFilter]
);
diff --git a/public/app/plugins/panel/alertlist/util.ts b/public/app/plugins/panel/alertlist/util.ts
index 5acf905d27c..a7984904696 100644
--- a/public/app/plugins/panel/alertlist/util.ts
+++ b/public/app/plugins/panel/alertlist/util.ts
@@ -1,14 +1,15 @@
import { isEmpty } from 'lodash';
import { Labels } from '@grafana/data';
-import { labelsMatchMatchers, parseMatchers } from 'app/features/alerting/unified/utils/alertmanager';
+import { labelsMatchMatchers } from 'app/features/alerting/unified/utils/alertmanager';
+import { parsePromQLStyleMatcherLooseSafe } from 'app/features/alerting/unified/utils/matchers';
import { Alert, hasAlertState } from 'app/types/unified-alerting';
import { GrafanaAlertState, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { UnifiedAlertListOptions } from './types';
function hasLabelFilter(alertInstanceLabelFilter: string, labels: Labels) {
- const matchers = parseMatchers(alertInstanceLabelFilter);
+ const matchers = parsePromQLStyleMatcherLooseSafe(alertInstanceLabelFilter);
return labelsMatchMatchers(labels, matchers);
}
diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts
index 6d43a8bb000..6697ec42208 100644
--- a/public/app/store/configureStore.ts
+++ b/public/app/store/configureStore.ts
@@ -5,6 +5,7 @@ import { Middleware } from 'redux';
import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi';
import { cloudMigrationAPI } from 'app/features/migrate-to-cloud/api';
+import { userPreferencesAPI } from 'app/features/preferences/api';
import { StoreState } from 'app/types/store';
import { buildInitialState } from '../core/reducers/navModel';
@@ -39,6 +40,7 @@ export function configureStore(initialState?: Partial) {
browseDashboardsAPI.middleware,
cloudMigrationAPI.middleware,
queryLibraryApi.middleware,
+ userPreferencesAPI.middleware,
...extraMiddleware
),
devTools: process.env.NODE_ENV !== 'production',
diff --git a/public/app/types/unified-alerting-dto.ts b/public/app/types/unified-alerting-dto.ts
index 47c94d70367..f1503e356b2 100644
--- a/public/app/types/unified-alerting-dto.ts
+++ b/public/app/types/unified-alerting-dto.ts
@@ -47,9 +47,15 @@ export function mapStateWithReasonToReason(state: GrafanaAlertStateWithReason):
return match ? match[1] : '';
}
+type StateWithReasonToBaseStateReturnType = T extends GrafanaAlertStateWithReason
+ ? GrafanaAlertState
+ : T extends PromAlertingRuleState
+ ? PromAlertingRuleState
+ : never;
+
export function mapStateWithReasonToBaseState(
state: GrafanaAlertStateWithReason | PromAlertingRuleState
-): GrafanaAlertState | PromAlertingRuleState {
+): StateWithReasonToBaseStateReturnType {
if (isAlertStateWithReason(state)) {
const fields = state.split(' ');
return fields[0] as GrafanaAlertState;