diff --git a/apps/dashboard/kinds/manifest.cue b/apps/dashboard/kinds/manifest.cue index 1ec0fb4f21c..f1044a39e24 100644 --- a/apps/dashboard/kinds/manifest.cue +++ b/apps/dashboard/kinds/manifest.cue @@ -5,5 +5,6 @@ manifest: { groupOverride: "dashboard.grafana.app" kinds: [ dashboard, + snapshot, ] } diff --git a/apps/dashboard/kinds/snapshot.cue b/apps/dashboard/kinds/snapshot.cue new file mode 100644 index 00000000000..c224daf8492 --- /dev/null +++ b/apps/dashboard/kinds/snapshot.cue @@ -0,0 +1,46 @@ +package kinds + +snapshot: { + kind: "Snapshot" + pluralName: "Snapshots" + scope: "Namespaced" + current: "v0alpha1" + + codegen: { + ts: { + enabled: true + } + go: { + enabled: true + } + } + + versions: { + "v0alpha1": { + schema: { + spec: { + // Snapshot title + title?: string + + // Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds) + expires?: int64 | *0 + + // When set to true, the snapshot exists in a remote server + external?: bool | *false + + // The external URL where the snapshot can be seen + externalUrl?: string + + // The URL that created the dashboard originally + originalUrl?: string + + // Snapshot creation timestamp + timestamp?: string + + // The raw dashboard (unstructured for now) + dashboard?: [string]: _ + } + } + } + } +} diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/register.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/register.go index 7b3934492fa..105601a0d84 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/register.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/register.go @@ -19,6 +19,7 @@ const ( // Resource constants DASHBOARD_RESOURCE = "dashboards" LIBRARY_PANEL_RESOURCE = "librarypanels" + SNAPSHOT_RESOURCE = "snapshots" ) var DashboardResourceInfo = utils.NewResourceInfo(GROUP, VERSION, @@ -75,6 +76,30 @@ var LibraryPanelResourceInfo = utils.NewResourceInfo(GROUP, VERSION, }, ) +var SnapshotResourceInfo = utils.NewResourceInfo(GROUP, VERSION, + "snapshots", "snapshot", "Snapshot", + func() runtime.Object { return &Snapshot{} }, + func() runtime.Object { return &SnapshotList{} }, + utils.TableColumns{ + Definition: []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name"}, + {Name: "Title", Type: "string", Format: "string", Description: "The snapshot name"}, + {Name: "Created At", Type: "date"}, + }, + Reader: func(obj any) ([]interface{}, error) { + m, ok := obj.(*Snapshot) + if ok { + return []interface{}{ + m.Name, + m.Spec.Title, + m.CreationTimestamp.UTC().Format(time.RFC3339), + }, nil + } + return nil, fmt.Errorf("expected snapshot") + }, + }, // default table converter +) + var ( SchemeBuilder runtime.SchemeBuilder localSchemeBuilder = &SchemeBuilder @@ -94,6 +119,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &DashboardWithAccessInfo{}, &LibraryPanel{}, &LibraryPanelList{}, + &Snapshot{}, + &SnapshotList{}, &metav1.PartialObjectMetadata{}, &metav1.PartialObjectMetadataList{}, &metav1.Table{}, diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot.go new file mode 100644 index 00000000000..85c3bbba4eb --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot.go @@ -0,0 +1,69 @@ +package v0alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" +) + +// This is returned from the POST command +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type DashboardSnapshotWithDeleteKey struct { + Snapshot `json:",inline"` + + // The delete key is only returned when the item is created. It is not returned from a get request + DeleteKey string `json:"deleteKey,omitempty"` +} + +// Each tenant, may have different sharing options +// This is currently set using custom.ini, but multi-tenant support will need +// to be managed differently +type SnapshotSharingOptions struct { + SnapshotsEnabled bool `json:"snapshotEnabled"` + ExternalSnapshotURL string `json:"externalSnapshotURL,omitempty"` + ExternalSnapshotName string `json:"externalSnapshotName,omitempty"` + ExternalEnabled bool `json:"externalEnabled,omitempty"` +} + +// These are the values expected to be sent from an end user +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type DashboardCreateCommand struct { + metav1.TypeMeta `json:",inline"` + + // Snapshot name + // required:false + Name string `json:"name"` + + // The complete dashboard model. + // required:true + Dashboard *common.Unstructured `json:"dashboard" binding:"Required"` + + // When the snapshot should expire in seconds in seconds. Default is never to expire. + // required:false + // default:0 + Expires int64 `json:"expires"` + + // these are passed when storing an external snapshot ref + // Save the snapshot on an external server rather than locally. + // required:false + // default: false + External bool `json:"external"` +} + +// The create response +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type DashboardCreateResponse struct { + metav1.TypeMeta `json:",inline"` + + // The unique key + Key string `json:"key"` + + // A unique key that will allow delete + DeleteKey string `json:"deleteKey"` + + // Absolute URL to show the dashboard + URL string `json:"url"` + + // URL that will delete the response + DeleteURL string `json:"deleteUrl"` +} diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_client_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_client_gen.go new file mode 100644 index 00000000000..f6001a20597 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_client_gen.go @@ -0,0 +1,80 @@ +package v0alpha1 + +import ( + "context" + + "github.com/grafana/grafana-app-sdk/resource" +) + +type SnapshotClient struct { + client *resource.TypedClient[*Snapshot, *SnapshotList] +} + +func NewSnapshotClient(client resource.Client) *SnapshotClient { + return &SnapshotClient{ + client: resource.NewTypedClient[*Snapshot, *SnapshotList](client, SnapshotKind()), + } +} + +func NewSnapshotClientFromGenerator(generator resource.ClientGenerator) (*SnapshotClient, error) { + c, err := generator.ClientFor(SnapshotKind()) + if err != nil { + return nil, err + } + return NewSnapshotClient(c), nil +} + +func (c *SnapshotClient) Get(ctx context.Context, identifier resource.Identifier) (*Snapshot, error) { + return c.client.Get(ctx, identifier) +} + +func (c *SnapshotClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*SnapshotList, error) { + return c.client.List(ctx, namespace, opts) +} + +func (c *SnapshotClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*SnapshotList, error) { + resp, err := c.client.List(ctx, namespace, resource.ListOptions{ + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + for resp.GetContinue() != "" { + page, err := c.client.List(ctx, namespace, resource.ListOptions{ + Continue: resp.GetContinue(), + ResourceVersion: opts.ResourceVersion, + Limit: opts.Limit, + LabelFilters: opts.LabelFilters, + FieldSelectors: opts.FieldSelectors, + }) + if err != nil { + return nil, err + } + resp.SetContinue(page.GetContinue()) + resp.SetResourceVersion(page.GetResourceVersion()) + resp.SetItems(append(resp.GetItems(), page.GetItems()...)) + } + return resp, nil +} + +func (c *SnapshotClient) Create(ctx context.Context, obj *Snapshot, opts resource.CreateOptions) (*Snapshot, error) { + // Make sure apiVersion and kind are set + obj.APIVersion = GroupVersion.Identifier() + obj.Kind = SnapshotKind().Kind() + return c.client.Create(ctx, obj, opts) +} + +func (c *SnapshotClient) Update(ctx context.Context, obj *Snapshot, opts resource.UpdateOptions) (*Snapshot, error) { + return c.client.Update(ctx, obj, opts) +} + +func (c *SnapshotClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*Snapshot, error) { + return c.client.Patch(ctx, identifier, req, opts) +} + +func (c *SnapshotClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error { + return c.client.Delete(ctx, identifier, opts) +} diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_codec_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_codec_gen.go new file mode 100644 index 00000000000..868f2f403b9 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_codec_gen.go @@ -0,0 +1,28 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "encoding/json" + "io" + + "github.com/grafana/grafana-app-sdk/resource" +) + +// SnapshotJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding +type SnapshotJSONCodec struct{} + +// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into` +func (*SnapshotJSONCodec) Read(reader io.Reader, into resource.Object) error { + return json.NewDecoder(reader).Decode(into) +} + +// Write writes JSON-encoded bytes into `writer` marshaled from `from` +func (*SnapshotJSONCodec) Write(writer io.Writer, from resource.Object) error { + return json.NewEncoder(writer).Encode(from) +} + +// Interface compliance checks +var _ resource.Codec = &SnapshotJSONCodec{} diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_metadata_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_metadata_gen.go new file mode 100644 index 00000000000..f400156b741 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_metadata_gen.go @@ -0,0 +1,31 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +import ( + time "time" +) + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +type SnapshotMetadata struct { + UpdateTimestamp time.Time `json:"updateTimestamp"` + CreatedBy string `json:"createdBy"` + Uid string `json:"uid"` + CreationTimestamp time.Time `json:"creationTimestamp"` + DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"` + Finalizers []string `json:"finalizers"` + ResourceVersion string `json:"resourceVersion"` + Generation int64 `json:"generation"` + UpdatedBy string `json:"updatedBy"` + Labels map[string]string `json:"labels"` +} + +// NewSnapshotMetadata creates a new SnapshotMetadata object. +func NewSnapshotMetadata() *SnapshotMetadata { + return &SnapshotMetadata{ + Finalizers: []string{}, + Labels: map[string]string{}, + } +} diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_object_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_object_gen.go new file mode 100644 index 00000000000..d917cebc0bf --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_object_gen.go @@ -0,0 +1,293 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "fmt" + "github.com/grafana/grafana-app-sdk/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "time" +) + +// +k8s:openapi-gen=true +type Snapshot struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata" yaml:"metadata"` + + // Spec is the spec of the Snapshot + Spec SnapshotSpec `json:"spec" yaml:"spec"` +} + +func (o *Snapshot) GetSpec() any { + return o.Spec +} + +func (o *Snapshot) SetSpec(spec any) error { + cast, ok := spec.(SnapshotSpec) + if !ok { + return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec) + } + o.Spec = cast + return nil +} + +func (o *Snapshot) GetSubresources() map[string]any { + return map[string]any{} +} + +func (o *Snapshot) GetSubresource(name string) (any, bool) { + switch name { + default: + return nil, false + } +} + +func (o *Snapshot) SetSubresource(name string, value any) error { + switch name { + default: + return fmt.Errorf("subresource '%s' does not exist", name) + } +} + +func (o *Snapshot) GetStaticMetadata() resource.StaticMetadata { + gvk := o.GroupVersionKind() + return resource.StaticMetadata{ + Name: o.ObjectMeta.Name, + Namespace: o.ObjectMeta.Namespace, + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + } +} + +func (o *Snapshot) SetStaticMetadata(metadata resource.StaticMetadata) { + o.Name = metadata.Name + o.Namespace = metadata.Namespace + o.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.Group, + Version: metadata.Version, + Kind: metadata.Kind, + }) +} + +func (o *Snapshot) GetCommonMetadata() resource.CommonMetadata { + dt := o.DeletionTimestamp + var deletionTimestamp *time.Time + if dt != nil { + deletionTimestamp = &dt.Time + } + // Legacy ExtraFields support + extraFields := make(map[string]any) + if o.Annotations != nil { + extraFields["annotations"] = o.Annotations + } + if o.ManagedFields != nil { + extraFields["managedFields"] = o.ManagedFields + } + if o.OwnerReferences != nil { + extraFields["ownerReferences"] = o.OwnerReferences + } + return resource.CommonMetadata{ + UID: string(o.UID), + ResourceVersion: o.ResourceVersion, + Generation: o.Generation, + Labels: o.Labels, + CreationTimestamp: o.CreationTimestamp.Time, + DeletionTimestamp: deletionTimestamp, + Finalizers: o.Finalizers, + UpdateTimestamp: o.GetUpdateTimestamp(), + CreatedBy: o.GetCreatedBy(), + UpdatedBy: o.GetUpdatedBy(), + ExtraFields: extraFields, + } +} + +func (o *Snapshot) SetCommonMetadata(metadata resource.CommonMetadata) { + o.UID = types.UID(metadata.UID) + o.ResourceVersion = metadata.ResourceVersion + o.Generation = metadata.Generation + o.Labels = metadata.Labels + o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp) + if metadata.DeletionTimestamp != nil { + dt := metav1.NewTime(*metadata.DeletionTimestamp) + o.DeletionTimestamp = &dt + } else { + o.DeletionTimestamp = nil + } + o.Finalizers = metadata.Finalizers + if o.Annotations == nil { + o.Annotations = make(map[string]string) + } + if !metadata.UpdateTimestamp.IsZero() { + o.SetUpdateTimestamp(metadata.UpdateTimestamp) + } + if metadata.CreatedBy != "" { + o.SetCreatedBy(metadata.CreatedBy) + } + if metadata.UpdatedBy != "" { + o.SetUpdatedBy(metadata.UpdatedBy) + } + // Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields + if metadata.ExtraFields != nil { + if annotations, ok := metadata.ExtraFields["annotations"]; ok { + if cast, ok := annotations.(map[string]string); ok { + o.Annotations = cast + } + } + if managedFields, ok := metadata.ExtraFields["managedFields"]; ok { + if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok { + o.ManagedFields = cast + } + } + if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok { + if cast, ok := ownerReferences.([]metav1.OwnerReference); ok { + o.OwnerReferences = cast + } + } + } +} + +func (o *Snapshot) GetCreatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/createdBy"] +} + +func (o *Snapshot) SetCreatedBy(createdBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy +} + +func (o *Snapshot) GetUpdateTimestamp() time.Time { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"]) + return parsed +} + +func (o *Snapshot) SetUpdateTimestamp(updateTimestamp time.Time) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339) +} + +func (o *Snapshot) GetUpdatedBy() string { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + return o.ObjectMeta.Annotations["grafana.com/updatedBy"] +} + +func (o *Snapshot) SetUpdatedBy(updatedBy string) { + if o.ObjectMeta.Annotations == nil { + o.ObjectMeta.Annotations = make(map[string]string) + } + + o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy +} + +func (o *Snapshot) Copy() resource.Object { + return resource.CopyObject(o) +} + +func (o *Snapshot) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *Snapshot) DeepCopy() *Snapshot { + cpy := &Snapshot{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *Snapshot) DeepCopyInto(dst *Snapshot) { + dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion + dst.TypeMeta.Kind = o.TypeMeta.Kind + o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta) + o.Spec.DeepCopyInto(&dst.Spec) +} + +// Interface compliance compile-time check +var _ resource.Object = &Snapshot{} + +// +k8s:openapi-gen=true +type SnapshotList struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ListMeta `json:"metadata" yaml:"metadata"` + Items []Snapshot `json:"items" yaml:"items"` +} + +func (o *SnapshotList) DeepCopyObject() runtime.Object { + return o.Copy() +} + +func (o *SnapshotList) Copy() resource.ListObject { + cpy := &SnapshotList{ + TypeMeta: o.TypeMeta, + Items: make([]Snapshot, len(o.Items)), + } + o.ListMeta.DeepCopyInto(&cpy.ListMeta) + for i := 0; i < len(o.Items); i++ { + if item, ok := o.Items[i].Copy().(*Snapshot); ok { + cpy.Items[i] = *item + } + } + return cpy +} + +func (o *SnapshotList) GetItems() []resource.Object { + items := make([]resource.Object, len(o.Items)) + for i := 0; i < len(o.Items); i++ { + items[i] = &o.Items[i] + } + return items +} + +func (o *SnapshotList) SetItems(items []resource.Object) { + o.Items = make([]Snapshot, len(items)) + for i := 0; i < len(items); i++ { + o.Items[i] = *items[i].(*Snapshot) + } +} + +func (o *SnapshotList) DeepCopy() *SnapshotList { + cpy := &SnapshotList{} + o.DeepCopyInto(cpy) + return cpy +} + +func (o *SnapshotList) DeepCopyInto(dst *SnapshotList) { + resource.CopyObjectInto(dst, o) +} + +// Interface compliance compile-time check +var _ resource.ListObject = &SnapshotList{} + +// Copy methods for all subresource types + +// DeepCopy creates a full deep copy of Spec +func (s *SnapshotSpec) DeepCopy() *SnapshotSpec { + cpy := &SnapshotSpec{} + s.DeepCopyInto(cpy) + return cpy +} + +// DeepCopyInto deep copies Spec into another Spec object +func (s *SnapshotSpec) DeepCopyInto(dst *SnapshotSpec) { + resource.CopyObjectInto(dst, s) +} diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_schema_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_schema_gen.go new file mode 100644 index 00000000000..b6086c5fd1f --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_schema_gen.go @@ -0,0 +1,34 @@ +// +// Code generated by grafana-app-sdk. DO NOT EDIT. +// + +package v0alpha1 + +import ( + "github.com/grafana/grafana-app-sdk/resource" +) + +// schema is unexported to prevent accidental overwrites +var ( + schemaSnapshot = resource.NewSimpleSchema("dashboard.grafana.app", "v0alpha1", &Snapshot{}, &SnapshotList{}, resource.WithKind("Snapshot"), + resource.WithPlural("snapshots"), resource.WithScope(resource.NamespacedScope)) + kindSnapshot = resource.Kind{ + Schema: schemaSnapshot, + Codecs: map[resource.KindEncoding]resource.Codec{ + resource.KindEncodingJSON: &SnapshotJSONCodec{}, + }, + } +) + +// Kind returns a resource.Kind for this Schema with a JSON codec +func SnapshotKind() resource.Kind { + return kindSnapshot +} + +// Schema returns a resource.SimpleSchema representation of Snapshot +func SnapshotSchema() *resource.SimpleSchema { + return schemaSnapshot +} + +// Interface compliance checks +var _ resource.Schema = kindSnapshot diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_spec_gen.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_spec_gen.go new file mode 100644 index 00000000000..f0c125aff97 --- /dev/null +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/snapshot_spec_gen.go @@ -0,0 +1,29 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +package v0alpha1 + +// +k8s:openapi-gen=true +type SnapshotSpec struct { + // Snapshot title + Title *string `json:"title,omitempty"` + // Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds) + Expires *int64 `json:"expires,omitempty"` + // When set to true, the snapshot exists in a remote server + External *bool `json:"external,omitempty"` + // The external URL where the snapshot can be seen + ExternalUrl *string `json:"externalUrl,omitempty"` + // The URL that created the dashboard originally + OriginalUrl *string `json:"originalUrl,omitempty"` + // Snapshot creation timestamp + Timestamp *string `json:"timestamp,omitempty"` + // The raw dashboard (unstructured for now) + Dashboard map[string]interface{} `json:"dashboard,omitempty"` +} + +// NewSnapshotSpec creates a new SnapshotSpec object. +func NewSnapshotSpec() *SnapshotSpec { + return &SnapshotSpec{ + Expires: (func(input int64) *int64 { return &input })(0), + External: (func(input bool) *bool { return &input })(false), + } +} diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go index b4a69d0031d..6e91e4938d8 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi.go @@ -15,31 +15,41 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.AnnotationActions": schema_pkg_apis_dashboard_v0alpha1_AnnotationActions(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.AnnotationPermission": schema_pkg_apis_dashboard_v0alpha1_AnnotationPermission(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.Dashboard": schema_pkg_apis_dashboard_v0alpha1_Dashboard(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardAccess": schema_pkg_apis_dashboard_v0alpha1_DashboardAccess(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardClient": schema_pkg_apis_dashboard_v0alpha1_DashboardClient(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardConversionStatus": schema_pkg_apis_dashboard_v0alpha1_DashboardConversionStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardHit": schema_pkg_apis_dashboard_v0alpha1_DashboardHit(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardJSONCodec": schema_pkg_apis_dashboard_v0alpha1_DashboardJSONCodec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardList": schema_pkg_apis_dashboard_v0alpha1_DashboardList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardMetadata": schema_pkg_apis_dashboard_v0alpha1_DashboardMetadata(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardStatus": schema_pkg_apis_dashboard_v0alpha1_DashboardStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v0alpha1_DashboardWithAccessInfo(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.FacetResult": schema_pkg_apis_dashboard_v0alpha1_FacetResult(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.GridPos": schema_pkg_apis_dashboard_v0alpha1_GridPos(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.LibraryPanel": schema_pkg_apis_dashboard_v0alpha1_LibraryPanel(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.LibraryPanelList": schema_pkg_apis_dashboard_v0alpha1_LibraryPanelList(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.LibraryPanelSpec": schema_pkg_apis_dashboard_v0alpha1_LibraryPanelSpec(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.LibraryPanelStatus": schema_pkg_apis_dashboard_v0alpha1_LibraryPanelStatus(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.ManagedBy": schema_pkg_apis_dashboard_v0alpha1_ManagedBy(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SearchResults": schema_pkg_apis_dashboard_v0alpha1_SearchResults(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SortBy": schema_pkg_apis_dashboard_v0alpha1_SortBy(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SortableField": schema_pkg_apis_dashboard_v0alpha1_SortableField(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SortableFields": schema_pkg_apis_dashboard_v0alpha1_SortableFields(ref), - "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.TermFacet": schema_pkg_apis_dashboard_v0alpha1_TermFacet(ref), - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured": commonv0alpha1.Unstructured{}.OpenAPIDefinition(), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.AnnotationActions": schema_pkg_apis_dashboard_v0alpha1_AnnotationActions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.AnnotationPermission": schema_pkg_apis_dashboard_v0alpha1_AnnotationPermission(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.Dashboard": schema_pkg_apis_dashboard_v0alpha1_Dashboard(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardAccess": schema_pkg_apis_dashboard_v0alpha1_DashboardAccess(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardClient": schema_pkg_apis_dashboard_v0alpha1_DashboardClient(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardConversionStatus": schema_pkg_apis_dashboard_v0alpha1_DashboardConversionStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardCreateCommand": schema_pkg_apis_dashboard_v0alpha1_DashboardCreateCommand(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardCreateResponse": schema_pkg_apis_dashboard_v0alpha1_DashboardCreateResponse(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardHit": schema_pkg_apis_dashboard_v0alpha1_DashboardHit(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardJSONCodec": schema_pkg_apis_dashboard_v0alpha1_DashboardJSONCodec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardList": schema_pkg_apis_dashboard_v0alpha1_DashboardList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardMetadata": schema_pkg_apis_dashboard_v0alpha1_DashboardMetadata(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardSnapshotWithDeleteKey": schema_pkg_apis_dashboard_v0alpha1_DashboardSnapshotWithDeleteKey(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardStatus": schema_pkg_apis_dashboard_v0alpha1_DashboardStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.DashboardWithAccessInfo": schema_pkg_apis_dashboard_v0alpha1_DashboardWithAccessInfo(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.FacetResult": schema_pkg_apis_dashboard_v0alpha1_FacetResult(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.GridPos": schema_pkg_apis_dashboard_v0alpha1_GridPos(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.LibraryPanel": schema_pkg_apis_dashboard_v0alpha1_LibraryPanel(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.LibraryPanelList": schema_pkg_apis_dashboard_v0alpha1_LibraryPanelList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.LibraryPanelSpec": schema_pkg_apis_dashboard_v0alpha1_LibraryPanelSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.LibraryPanelStatus": schema_pkg_apis_dashboard_v0alpha1_LibraryPanelStatus(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.ManagedBy": schema_pkg_apis_dashboard_v0alpha1_ManagedBy(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SearchResults": schema_pkg_apis_dashboard_v0alpha1_SearchResults(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.Snapshot": schema_pkg_apis_dashboard_v0alpha1_Snapshot(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SnapshotClient": schema_pkg_apis_dashboard_v0alpha1_SnapshotClient(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SnapshotJSONCodec": schema_pkg_apis_dashboard_v0alpha1_SnapshotJSONCodec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SnapshotList": schema_pkg_apis_dashboard_v0alpha1_SnapshotList(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SnapshotMetadata": schema_pkg_apis_dashboard_v0alpha1_SnapshotMetadata(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SnapshotSharingOptions": schema_pkg_apis_dashboard_v0alpha1_SnapshotSharingOptions(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SnapshotSpec": schema_pkg_apis_dashboard_v0alpha1_SnapshotSpec(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SortBy": schema_pkg_apis_dashboard_v0alpha1_SortBy(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SortableField": schema_pkg_apis_dashboard_v0alpha1_SortableField(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SortableFields": schema_pkg_apis_dashboard_v0alpha1_SortableFields(ref), + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.TermFacet": schema_pkg_apis_dashboard_v0alpha1_TermFacet(ref), + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured": commonv0alpha1.Unstructured{}.OpenAPIDefinition(), } } @@ -290,6 +300,126 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardConversionStatus(ref common.Ref } } +func schema_pkg_apis_dashboard_v0alpha1_DashboardCreateCommand(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "These are the values expected to be sent from an end user", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Snapshot name required:false", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "dashboard": { + SchemaProps: spec.SchemaProps{ + Description: "The complete dashboard model. required:true", + Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), + }, + }, + "expires": { + SchemaProps: spec.SchemaProps{ + Description: "When the snapshot should expire in seconds in seconds. Default is never to expire. required:false default:0", + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "external": { + SchemaProps: spec.SchemaProps{ + Description: "these are passed when storing an external snapshot ref Save the snapshot on an external server rather than locally. required:false default: false", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"name", "dashboard", "expires", "external"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"}, + } +} + +func schema_pkg_apis_dashboard_v0alpha1_DashboardCreateResponse(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "The create response", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "key": { + SchemaProps: spec.SchemaProps{ + Description: "The unique key", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "deleteKey": { + SchemaProps: spec.SchemaProps{ + Description: "A unique key that will allow delete", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "url": { + SchemaProps: spec.SchemaProps{ + Description: "Absolute URL to show the dashboard", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "deleteUrl": { + SchemaProps: spec.SchemaProps{ + Description: "URL that will delete the response", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"key", "deleteKey", "url", "deleteUrl"}, + }, + }, + } +} + func schema_pkg_apis_dashboard_v0alpha1_DashboardHit(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -539,6 +669,56 @@ func schema_pkg_apis_dashboard_v0alpha1_DashboardMetadata(ref common.ReferenceCa } } +func schema_pkg_apis_dashboard_v0alpha1_DashboardSnapshotWithDeleteKey(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "This is returned from the POST command", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the Snapshot", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SnapshotSpec"), + }, + }, + "deleteKey": { + SchemaProps: spec.SchemaProps{ + Description: "The delete key is only returned when the item is created. It is not returned from a get request", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SnapshotSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + func schema_pkg_apis_dashboard_v0alpha1_DashboardStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1067,6 +1247,331 @@ func schema_pkg_apis_dashboard_v0alpha1_SearchResults(ref common.ReferenceCallba } } +func schema_pkg_apis_dashboard_v0alpha1_Snapshot(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), + }, + }, + "spec": { + SchemaProps: spec.SchemaProps{ + Description: "Spec is the spec of the Snapshot", + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SnapshotSpec"), + }, + }, + }, + Required: []string{"metadata", "spec"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.SnapshotSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, + } +} + +func schema_pkg_apis_dashboard_v0alpha1_SnapshotClient(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "client": { + SchemaProps: spec.SchemaProps{ + Ref: ref("github.com/grafana/grafana-app-sdk/resource.TypedClient[T,L]"), + }, + }, + }, + Required: []string{"client"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana-app-sdk/resource.TypedClient[T,L]"}, + } +} + +func schema_pkg_apis_dashboard_v0alpha1_SnapshotJSONCodec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "SnapshotJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding", + Type: []string{"object"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v0alpha1_SnapshotList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.Snapshot"), + }, + }, + }, + }, + }, + }, + Required: []string{"metadata", "items"}, + }, + }, + Dependencies: []string{ + "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1.Snapshot", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, + } +} + +func schema_pkg_apis_dashboard_v0alpha1_SnapshotMetadata(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "metadata contains embedded CommonMetadata and can be extended with custom string fields without external reference as using the CommonMetadata reference breaks thema codegen.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "updateTimestamp": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "date-time", + }, + }, + "createdBy": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "creationTimestamp": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "date-time", + }, + }, + "deletionTimestamp": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "date-time", + }, + }, + "finalizers": { + SchemaProps: spec.SchemaProps{ + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "resourceVersion": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "generation": { + SchemaProps: spec.SchemaProps{ + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "updatedBy": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "labels": { + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"updateTimestamp", "createdBy", "uid", "creationTimestamp", "finalizers", "resourceVersion", "generation", "updatedBy", "labels"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v0alpha1_SnapshotSharingOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Each tenant, may have different sharing options This is currently set using custom.ini, but multi-tenant support will need to be managed differently", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "snapshotEnabled": { + SchemaProps: spec.SchemaProps{ + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, + "externalSnapshotURL": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "externalSnapshotName": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + "externalEnabled": { + SchemaProps: spec.SchemaProps{ + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"snapshotEnabled"}, + }, + }, + } +} + +func schema_pkg_apis_dashboard_v0alpha1_SnapshotSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "title": { + SchemaProps: spec.SchemaProps{ + Description: "Snapshot title", + Type: []string{"string"}, + Format: "", + }, + }, + "expires": { + SchemaProps: spec.SchemaProps{ + Description: "Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds)", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "external": { + SchemaProps: spec.SchemaProps{ + Description: "When set to true, the snapshot exists in a remote server", + Type: []string{"boolean"}, + Format: "", + }, + }, + "externalUrl": { + SchemaProps: spec.SchemaProps{ + Description: "The external URL where the snapshot can be seen", + Type: []string{"string"}, + Format: "", + }, + }, + "originalUrl": { + SchemaProps: spec.SchemaProps{ + Description: "The URL that created the dashboard originally", + Type: []string{"string"}, + Format: "", + }, + }, + "timestamp": { + SchemaProps: spec.SchemaProps{ + Description: "Snapshot creation timestamp", + Type: []string{"string"}, + Format: "", + }, + }, + "dashboard": { + SchemaProps: spec.SchemaProps{ + Description: "The raw dashboard (unstructured for now)", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + func schema_pkg_apis_dashboard_v0alpha1_SortBy(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi_violation_exceptions.list b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi_violation_exceptions.list index ba05be4946d..65b1e136c7c 100644 --- a/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ b/apps/dashboard/pkg/apis/dashboard/v0alpha1/zz_generated.openapi_violation_exceptions.list @@ -4,9 +4,14 @@ API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/ API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,LibraryPanelSpec,Links API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,LibraryPanelStatus,Warnings API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,SearchResults,Hits +API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,SnapshotMetadata,Finalizers API rule violation: list_type_missing,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,SortableFields,Fields API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,DashboardClient,client +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,DashboardCreateResponse,DeleteURL +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,SnapshotClient,client +API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,SnapshotSharingOptions,SnapshotsEnabled API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,SortBy,Descending API rule violation: names_match,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,SortableField,Field API rule violation: names_match,github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1,Unstructured,Object API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,DashboardList,ListMeta +API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1,SnapshotList,ListMeta diff --git a/apps/dashboard/pkg/apis/dashboard_manifest.go b/apps/dashboard/pkg/apis/dashboard_manifest.go index 1409424fbe2..974062efcec 100644 --- a/apps/dashboard/pkg/apis/dashboard_manifest.go +++ b/apps/dashboard/pkg/apis/dashboard_manifest.go @@ -36,6 +36,13 @@ var appManifestData = app.ManifestData{ Scope: "Namespaced", Conversion: false, }, + + { + Kind: "Snapshot", + Plural: "Snapshots", + Scope: "Namespaced", + Conversion: false, + }, }, Routes: app.ManifestVersionRoutes{ Namespaced: map[string]spec3.PathProps{}, @@ -110,6 +117,7 @@ func RemoteManifest() app.Manifest { var kindVersionToGoType = map[string]resource.Kind{ "Dashboard/v0alpha1": v0alpha1.DashboardKind(), + "Snapshot/v0alpha1": v0alpha1.SnapshotKind(), "Dashboard/v1beta1": v1beta1.DashboardKind(), "Dashboard/v2alpha1": v2alpha1.DashboardKind(), "Dashboard/v2beta1": v2beta1.DashboardKind(), diff --git a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.panel-datasource-type-datasource.json b/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.panel-datasource-type-datasource.json deleted file mode 100644 index 2ed8151a549..00000000000 --- a/apps/dashboard/pkg/migration/conversion/testdata/input/v1beta1.panel-datasource-type-datasource.json +++ /dev/null @@ -1,234 +0,0 @@ -{ - "kind": "DashboardWithAccessInfo", - "apiVersion": "dashboard.grafana.app/v1beta1", - "metadata": { - "name": "ad6phhg", - "namespace": "default", - "uid": "spsmxIXYXdgxtY972XX7j3dj3ijM3IZXDmsCfjygSP8X", - "resourceVersion": "2", - "generation": 2, - "creationTimestamp": "2025-11-27T12:31:46Z", - "labels": { - "grafana.app/deprecatedInternalID": "283" - }, - "annotations": { - "grafana.app/createdBy": "user:eex2ofwuj0agwd", - "grafana.app/updatedBy": "user:eex2ofwuj0agwd", - "grafana.app/updatedTimestamp": "2025-11-27T12:32:09Z" - } - }, - "spec": { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations \u0026 Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "links": [], - "panels": [ - { - "datasource": { - "type": "datasource" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "max": 100, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": 0 - }, - { - "color": "yellow", - "value": 50 - } - ] - }, - "unit": "percent" - }, - "overrides": [] - }, - "gridPos": { - "h": 15, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 4, - "options": { - "displayMode": "gradient", - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "maxVizHeight": 300, - "minVizHeight": 16, - "minVizWidth": 8, - "namePlacement": "auto", - "orientation": "horizontal", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showUnfilled": true, - "sizing": "auto", - "valueMode": "color" - }, - "pluginVersion": "12.4.0-pre", - "targets": [ - { - "datasource": {}, - "refId": "A", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "B", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "C", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "D", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "E", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "F", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "G", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "labels": "", - "refId": "H", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "I", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "J", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "K", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "L", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "M", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "N", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "labels": "", - "refId": "O", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "P", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "Q", - "scenarioId": "random_walk" - } - ], - "title": "Panel Title", - "type": "bargauge" - } - ], - "preload": false, - "refresh": "", - "schemaVersion": 42, - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": {}, - "timezone": "", - "title": "Bar Gauge Demo Unfilled Copy v1", - "weekStart": "" - }, - "status": {}, - "access": { - "slug": "bar-gauge-demo-unfilled-copy-v1", - "url": "/d/ad6phhg/bar-gauge-demo-unfilled-copy-v1", - "isPublic": false, - "canSave": true, - "canEdit": true, - "canAdmin": true, - "canStar": true, - "canDelete": true, - "annotationsPermissions": { - "dashboard": { - "canAdd": true, - "canEdit": true, - "canDelete": true - }, - "organization": { - "canAdd": true, - "canEdit": true, - "canDelete": true - } - } - } -} diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v0alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v0alpha1.json deleted file mode 100644 index 7c089035f26..00000000000 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v0alpha1.json +++ /dev/null @@ -1,217 +0,0 @@ -{ - "kind": "DashboardWithAccessInfo", - "apiVersion": "dashboard.grafana.app/v0alpha1", - "metadata": { - "name": "ad6phhg", - "namespace": "default", - "uid": "spsmxIXYXdgxtY972XX7j3dj3ijM3IZXDmsCfjygSP8X", - "resourceVersion": "2", - "generation": 2, - "creationTimestamp": "2025-11-27T12:31:46Z", - "labels": { - "grafana.app/deprecatedInternalID": "283" - }, - "annotations": { - "grafana.app/createdBy": "user:eex2ofwuj0agwd", - "grafana.app/updatedBy": "user:eex2ofwuj0agwd", - "grafana.app/updatedTimestamp": "2025-11-27T12:32:09Z" - } - }, - "spec": { - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations \u0026 Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "links": [], - "panels": [ - { - "datasource": { - "type": "datasource" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "max": 100, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": 0 - }, - { - "color": "yellow", - "value": 50 - } - ] - }, - "unit": "percent" - }, - "overrides": [] - }, - "gridPos": { - "h": 15, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 4, - "options": { - "displayMode": "gradient", - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "maxVizHeight": 300, - "minVizHeight": 16, - "minVizWidth": 8, - "namePlacement": "auto", - "orientation": "horizontal", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showUnfilled": true, - "sizing": "auto", - "valueMode": "color" - }, - "pluginVersion": "12.4.0-pre", - "targets": [ - { - "datasource": {}, - "refId": "A", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "B", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "C", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "D", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "E", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "F", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "G", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "labels": "", - "refId": "H", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "I", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "J", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "K", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "L", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "M", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "N", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "labels": "", - "refId": "O", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "P", - "scenarioId": "random_walk" - }, - { - "datasource": {}, - "refId": "Q", - "scenarioId": "random_walk" - } - ], - "title": "Panel Title", - "type": "bargauge" - } - ], - "preload": false, - "refresh": "", - "schemaVersion": 42, - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": {}, - "timezone": "", - "title": "Bar Gauge Demo Unfilled Copy v1", - "weekStart": "" - }, - "status": { - "conversion": { - "failed": false, - "storedVersion": "v1beta1" - } - } -} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2alpha1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2alpha1.json deleted file mode 100644 index 037136e9e4f..00000000000 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2alpha1.json +++ /dev/null @@ -1,455 +0,0 @@ -{ - "kind": "DashboardWithAccessInfo", - "apiVersion": "dashboard.grafana.app/v2alpha1", - "metadata": { - "name": "ad6phhg", - "namespace": "default", - "uid": "spsmxIXYXdgxtY972XX7j3dj3ijM3IZXDmsCfjygSP8X", - "resourceVersion": "2", - "generation": 2, - "creationTimestamp": "2025-11-27T12:31:46Z", - "labels": { - "grafana.app/deprecatedInternalID": "283" - }, - "annotations": { - "grafana.app/createdBy": "user:eex2ofwuj0agwd", - "grafana.app/updatedBy": "user:eex2ofwuj0agwd", - "grafana.app/updatedTimestamp": "2025-11-27T12:32:09Z" - } - }, - "spec": { - "annotations": [ - { - "kind": "AnnotationQuery", - "spec": { - "query": { - "kind": "", - "spec": {} - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations \u0026 Alerts", - "builtIn": true, - "legacyOptions": { - "type": "dashboard" - } - } - } - ], - "cursorSync": "Off", - "editable": true, - "elements": { - "panel-4": { - "kind": "Panel", - "spec": { - "id": 4, - "title": "Panel Title", - "description": "", - "links": [], - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "A", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "B", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "C", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "D", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "E", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "F", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "G", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "labels": "", - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "H", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "I", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "J", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "K", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "L", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "M", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "N", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "labels": "", - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "O", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "P", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "datasource", - "spec": { - "scenarioId": "random_walk" - } - }, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "Q", - "hidden": false - } - } - ], - "transformations": [], - "queryOptions": {} - } - }, - "vizConfig": { - "kind": "bargauge", - "spec": { - "pluginVersion": "12.4.0-pre", - "options": { - "displayMode": "gradient", - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "maxVizHeight": 300, - "minVizHeight": 16, - "minVizWidth": 8, - "namePlacement": "auto", - "orientation": "horizontal", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showUnfilled": true, - "sizing": "auto", - "valueMode": "color" - }, - "fieldConfig": { - "defaults": { - "unit": "percent", - "min": 0, - "max": 100, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "value": 0, - "color": "red" - }, - { - "value": 50, - "color": "yellow" - } - ] - }, - "color": { - "mode": "thresholds" - } - }, - "overrides": [] - } - } - } - } - } - }, - "layout": { - "kind": "GridLayout", - "spec": { - "items": [ - { - "kind": "GridLayoutItem", - "spec": { - "x": 0, - "y": 0, - "width": 24, - "height": 15, - "element": { - "kind": "ElementReference", - "name": "panel-4" - } - } - } - ] - } - }, - "links": [], - "liveNow": false, - "preload": false, - "tags": [], - "timeSettings": { - "timezone": "", - "from": "now-6h", - "to": "now", - "autoRefresh": "", - "autoRefreshIntervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "hideTimepicker": false, - "fiscalYearStartMonth": 0 - }, - "title": "Bar Gauge Demo Unfilled Copy v1", - "variables": [] - }, - "status": {} -} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2beta1.json b/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2beta1.json deleted file mode 100644 index 5a357b52534..00000000000 --- a/apps/dashboard/pkg/migration/conversion/testdata/output/v1beta1.panel-datasource-type-datasource.v2beta1.json +++ /dev/null @@ -1,475 +0,0 @@ -{ - "kind": "DashboardWithAccessInfo", - "apiVersion": "dashboard.grafana.app/v2beta1", - "metadata": { - "name": "ad6phhg", - "namespace": "default", - "uid": "spsmxIXYXdgxtY972XX7j3dj3ijM3IZXDmsCfjygSP8X", - "resourceVersion": "2", - "generation": 2, - "creationTimestamp": "2025-11-27T12:31:46Z", - "labels": { - "grafana.app/deprecatedInternalID": "283" - }, - "annotations": { - "grafana.app/createdBy": "user:eex2ofwuj0agwd", - "grafana.app/updatedBy": "user:eex2ofwuj0agwd", - "grafana.app/updatedTimestamp": "2025-11-27T12:32:09Z" - } - }, - "spec": { - "annotations": [ - { - "kind": "AnnotationQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "", - "version": "v0", - "spec": {} - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations \u0026 Alerts", - "builtIn": true, - "legacyOptions": { - "type": "dashboard" - } - } - } - ], - "cursorSync": "Off", - "editable": true, - "elements": { - "panel-4": { - "kind": "Panel", - "spec": { - "id": 4, - "title": "Panel Title", - "description": "", - "links": [], - "data": { - "kind": "QueryGroup", - "spec": { - "queries": [ - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "A", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "B", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "C", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "D", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "E", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "F", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "G", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "labels": "", - "scenarioId": "random_walk" - } - }, - "refId": "H", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "I", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "J", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "K", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "L", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "M", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "N", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "labels": "", - "scenarioId": "random_walk" - } - }, - "refId": "O", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "P", - "hidden": false - } - }, - { - "kind": "PanelQuery", - "spec": { - "query": { - "kind": "DataQuery", - "group": "datasource", - "version": "v0", - "datasource": { - "name": "grafana" - }, - "spec": { - "scenarioId": "random_walk" - } - }, - "refId": "Q", - "hidden": false - } - } - ], - "transformations": [], - "queryOptions": {} - } - }, - "vizConfig": { - "kind": "VizConfig", - "group": "bargauge", - "version": "12.4.0-pre", - "spec": { - "options": { - "displayMode": "gradient", - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": false - }, - "maxVizHeight": 300, - "minVizHeight": 16, - "minVizWidth": 8, - "namePlacement": "auto", - "orientation": "horizontal", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showUnfilled": true, - "sizing": "auto", - "valueMode": "color" - }, - "fieldConfig": { - "defaults": { - "unit": "percent", - "min": 0, - "max": 100, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "value": 0, - "color": "red" - }, - { - "value": 50, - "color": "yellow" - } - ] - }, - "color": { - "mode": "thresholds" - } - }, - "overrides": [] - } - } - } - } - } - }, - "layout": { - "kind": "GridLayout", - "spec": { - "items": [ - { - "kind": "GridLayoutItem", - "spec": { - "x": 0, - "y": 0, - "width": 24, - "height": 15, - "element": { - "kind": "ElementReference", - "name": "panel-4" - } - } - } - ] - } - }, - "links": [], - "liveNow": false, - "preload": false, - "tags": [], - "timeSettings": { - "timezone": "", - "from": "now-6h", - "to": "now", - "autoRefresh": "", - "autoRefreshIntervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "hideTimepicker": false, - "fiscalYearStartMonth": 0 - }, - "title": "Bar Gauge Demo Unfilled Copy v1", - "variables": [] - }, - "status": {} -} \ No newline at end of file diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go index 58089d12c54..9efd0b68f23 100644 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go +++ b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1.go @@ -16,7 +16,6 @@ import ( dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" schemaversion "github.com/grafana/grafana/apps/dashboard/pkg/migration/schemaversion" "github.com/grafana/grafana/pkg/apimachinery/identity" - "github.com/grafana/grafana/pkg/tsdb/grafanads" ) // getDefaultDatasourceType gets the default datasource type using the datasource provider @@ -53,16 +52,6 @@ func getDatasourceTypeByUID(ctx context.Context, uid string, provider schemavers return getDefaultDatasourceType(ctx, provider) } -// resolveGrafanaDatasourceUID resolves the Grafana datasource UID when type is "datasource" and UID is empty. -// The Grafana datasource has type "datasource" and UID "grafana". When a v1beta1 dashboard has -// datasource: { type: "datasource" } with no UID, it should resolve to uid: "grafana". -func resolveGrafanaDatasourceUID(dsType, dsUID string) string { - if dsType == "datasource" && dsUID == "" { - return grafanads.DatasourceUID - } - return dsUID -} - // prepareV1beta1ConversionContext sets up the context with namespace and service identity // for v1beta1 dashboard conversions. This context is needed to retrieve datasources for // converting dashboard datasource references. @@ -1685,9 +1674,6 @@ func buildGroupByVariable(ctx context.Context, varMap map[string]interface{}, co // If no UID and no type, use default datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider) } - - // Resolve Grafana datasource UID when type is "datasource" and UID is empty - datasourceUID = resolveGrafanaDatasourceUID(datasourceType, datasourceUID) } else { datasourceType = getDefaultDatasourceType(ctx, dsIndexProvider) } @@ -1890,45 +1876,22 @@ func transformPanelQueries(ctx context.Context, panelMap map[string]interface{}, // Get panel datasource var panelDatasource *dashv2alpha1.DashboardDataSourceRef - ds, dsExists := panelMap["datasource"] + if ds, ok := panelMap["datasource"].(map[string]interface{}); ok { + dsUID := schemaversion.GetStringValue(ds, "uid") + dsType := schemaversion.GetStringValue(ds, "type") - if dsExists && ds != nil { - if dsMap, ok := ds.(map[string]interface{}); ok { - // Handle panel datasource as object - dsUID := schemaversion.GetStringValue(dsMap, "uid") - dsType := schemaversion.GetStringValue(dsMap, "type") + // If we have a UID, use it to get the correct type from the datasource service + // BUT: Don't try to resolve types for template variables + if dsUID != "" && dsType == "" && !isTemplateVariable(dsUID) { + dsType = getDatasourceTypeByUID(ctx, dsUID, dsIndexProvider) + } else if dsUID == "" && dsType == "" { + // If no UID and no type, use default + dsType = getDefaultDatasourceType(ctx, dsIndexProvider) + } - // Check if datasource object is effectively empty (no uid and no type) - // Empty objects {} should be preserved as empty, not converted to defaults - isEmpty := dsUID == "" && dsType == "" - - // If we have a UID, use it to get the correct type from the datasource service - // BUT: Don't try to resolve types for template variables - if dsUID != "" && dsType == "" && !isTemplateVariable(dsUID) { - dsType = getDatasourceTypeByUID(ctx, dsUID, dsIndexProvider) - } else if !isEmpty && dsUID == "" && dsType == "" { - // Only set default if datasource is missing (not empty object) - // Empty objects {} should remain empty - dsType = getDefaultDatasourceType(ctx, dsIndexProvider) - } - - // Resolve Grafana datasource UID when type is "datasource" and UID is empty - // Only resolve if we have a type (not for empty objects) - if !isEmpty { - dsUID = resolveGrafanaDatasourceUID(dsType, dsUID) - } - - // Only create panelDatasource if it's not empty after resolution - // Empty objects {} should result in nil panelDatasource - // After resolution, check if we have a type or UID (not just the original isEmpty) - // This ensures that type: "datasource" with empty UID gets resolved to uid: "grafana" - // and panelDatasource is created - if dsType != "" || dsUID != "" { - panelDatasource = &dashv2alpha1.DashboardDataSourceRef{ - Type: &dsType, - Uid: &dsUID, - } - } + panelDatasource = &dashv2alpha1.DashboardDataSourceRef{ + Type: &dsType, + Uid: &dsUID, } } @@ -1955,24 +1918,12 @@ func transformSingleQuery(ctx context.Context, targetMap map[string]interface{}, queryDatasourceUID = schemaversion.GetStringValue(ds, "uid") queryDatasourceType = schemaversion.GetStringValue(ds, "type") - // If target datasource is empty object {} (no uid and no type), treat it as missing - // and fall through to use panel datasource (matches frontend behavior in v36 migration) - if queryDatasourceUID == "" && queryDatasourceType == "" { - // Empty datasource object - will use panel datasource below - } else { - // If we have a UID, use it to get the correct type from the datasource service - // BUT: Don't try to resolve types for template variables - if queryDatasourceUID != "" && queryDatasourceType == "" && !isTemplateVariable(queryDatasourceUID) { - queryDatasourceType = getDatasourceTypeByUID(ctx, queryDatasourceUID, dsIndexProvider) - } - - // Resolve Grafana datasource UID when type is "datasource" and UID is empty - queryDatasourceUID = resolveGrafanaDatasourceUID(queryDatasourceType, queryDatasourceUID) + // If we have a UID, use it to get the correct type from the datasource service + // BUT: Don't try to resolve types for template variables + if queryDatasourceUID != "" && queryDatasourceType == "" && !isTemplateVariable(queryDatasourceUID) { + queryDatasourceType = getDatasourceTypeByUID(ctx, queryDatasourceUID, dsIndexProvider) } - } - - // Use panel datasource if target datasource is missing or empty - if queryDatasourceUID == "" && queryDatasourceType == "" && panelDatasource != nil { + } else if panelDatasource != nil { // Only use panel datasource if it's not a mixed datasource // Mixed datasources should not be propagated to individual queries if panelDatasource.Uid != nil && *panelDatasource.Uid != "-- Mixed --" { @@ -1980,10 +1931,6 @@ func transformSingleQuery(ctx context.Context, targetMap map[string]interface{}, queryDatasourceType = *panelDatasource.Type } queryDatasourceUID = *panelDatasource.Uid - } else if panelDatasource.Type != nil && *panelDatasource.Type == "datasource" { - // Handle case where panel datasource has type "datasource" but no UID - queryDatasourceType = *panelDatasource.Type - queryDatasourceUID = resolveGrafanaDatasourceUID(*panelDatasource.Type, "") } } diff --git a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go b/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go deleted file mode 100644 index 6bbdf1ca214..00000000000 --- a/apps/dashboard/pkg/migration/conversion/v1beta1_to_v2alpha1_test.go +++ /dev/null @@ -1,228 +0,0 @@ -package conversion - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/runtime" - - dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1" - dashv2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1" - "github.com/grafana/grafana/apps/dashboard/pkg/migration" - migrationtestutil "github.com/grafana/grafana/apps/dashboard/pkg/migration/testutil" - "github.com/grafana/grafana/pkg/tsdb/grafanads" -) - -// TestV1beta1ToV2alpha1 tests conversion from v1beta1 to v2alpha1 with various datasource scenarios -func TestV1beta1ToV2alpha1(t *testing.T) { - // Initialize the migrator with test providers - dsProvider := migrationtestutil.NewDataSourceProvider(migrationtestutil.StandardTestConfig) - leProvider := migrationtestutil.NewLibraryElementProvider() - migration.Initialize(dsProvider, leProvider) - - // Set up conversion scheme - scheme := runtime.NewScheme() - err := RegisterConversions(scheme, dsProvider, leProvider) - require.NoError(t, err) - - testCases := []struct { - name string - createV1beta1 func() *dashv1.Dashboard - validateV2alpha1 func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) - }{ - { - name: "panel datasource type datasource with no UID - resolves to grafana UID", - createV1beta1: func() *dashv1.Dashboard { - return &dashv1.Dashboard{ - Spec: dashv1.DashboardSpec{ - Object: map[string]interface{}{ - "title": "Test Dashboard", - "panels": []interface{}{ - map[string]interface{}{ - "id": 1, - "type": "bargauge", - // Panel datasource has type: "datasource" but no UID - "datasource": map[string]interface{}{ - "type": "datasource", - // No "uid" field - }, - "targets": []interface{}{ - map[string]interface{}{ - "refId": "A", - "scenarioId": "random_walk", - // Target datasource is empty object {} - "datasource": map[string]interface{}{}, - }, - }, - }, - }, - }, - }, - } - }, - validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { - require.NotNil(t, v2alpha1.Spec.Elements["panel-1"]) - panel := v2alpha1.Spec.Elements["panel-1"].PanelKind - require.NotNil(t, panel) - - // Verify queries have datasource with UID resolved to "grafana" - require.Len(t, panel.Spec.Data.Spec.Queries, 1) - query := panel.Spec.Data.Spec.Queries[0] - require.NotNil(t, query.Spec.Datasource, "Query should have datasource") - - // Verify datasource type is "datasource" - assert.NotNil(t, query.Spec.Datasource.Type) - assert.Equal(t, "datasource", *query.Spec.Datasource.Type) - - // Verify datasource UID is resolved to "grafana" - assert.NotNil(t, query.Spec.Datasource.Uid) - assert.Equal(t, grafanads.DatasourceUID, *query.Spec.Datasource.Uid, "type: 'datasource' with no UID should resolve to uid: 'grafana'") - - // Verify query kind matches datasource type - assert.Equal(t, "datasource", query.Spec.Query.Kind) - }, - }, - { - name: "empty target datasource objects inherit from panel datasource", - createV1beta1: func() *dashv1.Dashboard { - return &dashv1.Dashboard{ - Spec: dashv1.DashboardSpec{ - Object: map[string]interface{}{ - "title": "Test Dashboard", - "panels": []interface{}{ - map[string]interface{}{ - "id": 1, - "type": "bargauge", - // Panel datasource is set - "datasource": map[string]interface{}{ - "type": "prometheus", - "uid": "prometheus-uid", - }, - "targets": []interface{}{ - map[string]interface{}{ - "refId": "A", - "scenarioId": "random_walk", - // Target datasource is empty object {} - should inherit from panel - "datasource": map[string]interface{}{}, - }, - map[string]interface{}{ - "refId": "B", - "scenarioId": "random_walk", - "datasource": map[string]interface{}{}, - }, - }, - }, - }, - }, - }, - } - }, - validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { - require.NotNil(t, v2alpha1.Spec.Elements["panel-1"]) - panel := v2alpha1.Spec.Elements["panel-1"].PanelKind - require.NotNil(t, panel) - - // Verify queries inherit panel datasource - require.Len(t, panel.Spec.Data.Spec.Queries, 2) - for _, query := range panel.Spec.Data.Spec.Queries { - require.NotNil(t, query.Spec.Datasource, "Query should inherit datasource from panel when target datasource is empty") - assert.Equal(t, "prometheus", *query.Spec.Datasource.Type) - assert.Equal(t, "prometheus-uid", *query.Spec.Datasource.Uid) - assert.Equal(t, "prometheus", query.Spec.Query.Kind) - } - }, - }, - { - name: "panel datasource null without empty target datasource objects - no default set", - createV1beta1: func() *dashv1.Dashboard { - return &dashv1.Dashboard{ - Spec: dashv1.DashboardSpec{ - Object: map[string]interface{}{ - "title": "Test Dashboard", - "panels": []interface{}{ - map[string]interface{}{ - "id": 1, - "type": "bargauge", - // Panel datasource is null - "datasource": nil, - "targets": []interface{}{ - map[string]interface{}{ - "refId": "A", - "scenarioId": "random_walk", - // Target has no datasource field at all (not even empty object) - }, - }, - }, - }, - }, - }, - } - }, - validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { - require.NotNil(t, v2alpha1.Spec.Elements["panel-1"]) - panel := v2alpha1.Spec.Elements["panel-1"].PanelKind - require.NotNil(t, panel) - - // Verify queries don't have datasource when panel is null and targets don't have empty datasource objects - require.Len(t, panel.Spec.Data.Spec.Queries, 1) - query := panel.Spec.Data.Spec.Queries[0] - // Query should not have datasource when panel datasource is null and target doesn't have empty datasource object - assert.Nil(t, query.Spec.Datasource, "Query should not have datasource when panel datasource is null and target has no empty datasource object") - }, - }, - { - name: "empty panel datasource object preserved as empty", - createV1beta1: func() *dashv1.Dashboard { - return &dashv1.Dashboard{ - Spec: dashv1.DashboardSpec{ - Object: map[string]interface{}{ - "title": "Test Dashboard", - "panels": []interface{}{ - map[string]interface{}{ - "id": 1, - "type": "bargauge", - // Panel datasource is empty object {} - should be preserved as empty - "datasource": map[string]interface{}{}, - "targets": []interface{}{ - map[string]interface{}{ - "refId": "A", - "scenarioId": "random_walk", - "datasource": map[string]interface{}{}, - }, - }, - }, - }, - }, - }, - } - }, - validateV2alpha1: func(t *testing.T, v2alpha1 *dashv2alpha1.Dashboard) { - require.NotNil(t, v2alpha1.Spec.Elements["panel-1"]) - panel := v2alpha1.Spec.Elements["panel-1"].PanelKind - require.NotNil(t, panel) - - // Verify queries don't have datasource when panel datasource is empty object {} - require.Len(t, panel.Spec.Data.Spec.Queries, 1) - query := panel.Spec.Data.Spec.Queries[0] - // Empty objects {} should be preserved as empty, not converted to defaults - assert.Nil(t, query.Spec.Datasource, "Query should not have datasource when panel datasource is empty object {}") - assert.Equal(t, "", query.Spec.Query.Kind, "Query kind should be empty when datasource is empty object {}") - }, - }, - } - - for _, tt := range testCases { - t.Run(tt.name, func(t *testing.T) { - v1beta1Dash := tt.createV1beta1() - - // Convert to v2alpha1 - var v2alpha1Dash dashv2alpha1.Dashboard - err := scheme.Convert(v1beta1Dash, &v2alpha1Dash, nil) - require.NoError(t, err) - - // Validate the conversion result - tt.validateV2alpha1(t, &v2alpha1Dash) - }) - } -} diff --git a/docs/sources/datasources/jaeger/_index.md b/docs/sources/datasources/jaeger/_index.md index 21961851d85..d9326f0fd2a 100644 --- a/docs/sources/datasources/jaeger/_index.md +++ b/docs/sources/datasources/jaeger/_index.md @@ -387,3 +387,15 @@ To configure this feature, see the [introduction to exemplars](ref:exemplars) do If service dependency information is available in Jaeger, it can be visualized in Grafana. Use the Jaeger data source with the "Dependency Graph" query type on a Node Graph panel for this. + +## Querying Data via gRPC Endpoint (Public Preview) + +Jaeger now offers an alternative method for querying data via HTTP, which utilizes their gRPC service. For detailed information about the API and any necessary Jaeger setup requirements, please consult their [documentation](https://www.jaegertracing.io/docs/2.12/architecture/apis/#query-json-over-http). + +The following queries are currently enabled through the gRPC endpoint: + +- Service search +- Operation search +- Trace ID search + +To enable gRPC querying for Jaeger within Grafana, you must enable the `jaegerEnableGrpcEndpoint` feature flag. Grafana Cloud customers should contact support to request access and provide feedback on this feature. diff --git a/e2e-playwright/panels-suite/canvas-scene.spec.ts b/e2e-playwright/panels-suite/canvas-scene.spec.ts index 7cd738bc6ba..b1fc028f3ae 100644 --- a/e2e-playwright/panels-suite/canvas-scene.spec.ts +++ b/e2e-playwright/panels-suite/canvas-scene.spec.ts @@ -2,17 +2,18 @@ import { Locator } from '@playwright/test'; import { test, expect } from '@grafana/plugin-e2e'; +import { setVisualization } from './vizpicker-utils'; + test.use({ featureToggles: { canvasPanelPanZoom: true, }, }); - test.describe('Canvas Panel - Scene Tests', () => { - test.beforeEach(async ({ page, gotoDashboardPage }) => { + test.beforeEach(async ({ page, gotoDashboardPage, selectors }) => { const dashboardPage = await gotoDashboardPage({}); const panelEditPage = await dashboardPage.addPanel(); - await panelEditPage.setVisualization('Canvas'); + await setVisualization(panelEditPage, 'Canvas', selectors); // Wait for canvas panel to load await page.waitForSelector('[data-testid="canvas-scene-pan-zoom"]', { timeout: 10000 }); diff --git a/e2e-playwright/panels-suite/panelEdit_base.spec.ts b/e2e-playwright/panels-suite/panelEdit_base.spec.ts index 9e12c3b7860..db736d8cbdc 100644 --- a/e2e-playwright/panels-suite/panelEdit_base.spec.ts +++ b/e2e-playwright/panels-suite/panelEdit_base.spec.ts @@ -58,7 +58,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Queries')).click(); // Check that Time series is chosen - await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText( + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header)).toHaveText( 'Time series' ); @@ -71,9 +71,10 @@ test.describe( // Change to Text panel await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('All visualizations')).click(); // <-- should only need to do this once thanks to the session storage await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item('Text')).click(); // Check current visualization shows Text - await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText( + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header)).toHaveText( 'Text' ); @@ -84,7 +85,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker).click(); await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item('Table')).click(); // Check current visualization shows Table - await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker)).toContainText( + await expect(dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header)).toHaveText( 'Table' ); diff --git a/e2e-playwright/panels-suite/vizpicker-utils.ts b/e2e-playwright/panels-suite/vizpicker-utils.ts new file mode 100644 index 00000000000..1785dd7e04a --- /dev/null +++ b/e2e-playwright/panels-suite/vizpicker-utils.ts @@ -0,0 +1,24 @@ +import { expect, E2ESelectorGroups, PanelEditPage } from '@grafana/plugin-e2e'; + +// this replaces the panelEditPage.setVisualization method used previously in tests, since it +// does not know how to use the updated 12.4 viz picker UI to set the visualization +export const setVisualization = async (panelEditPage: PanelEditPage, vizName: string, selectors: E2ESelectorGroups) => { + const vizPicker = panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker); + await expect(vizPicker, '"Change" button should be visible').toBeVisible(); + await vizPicker.click(); + + const allVizTabBtn = panelEditPage.getByGrafanaSelector(selectors.components.Tab.title('All visualizations')); + await expect(allVizTabBtn, '"All visualiations" button should be visible').toBeVisible(); + await allVizTabBtn.click(); + + const vizItem = panelEditPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(vizName)); + await expect(vizItem, `"${vizName}" item should be visible`).toBeVisible(); + await vizItem.scrollIntoViewIfNeeded(); + await vizItem.click(); + + await expect(vizPicker, '"Change" button should be visible again').toBeVisible(); + await expect( + panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header), + 'Panel header should have the new viz type name' + ).toHaveText(vizName); +}; diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts index 0133a3e3712..336dbef0a29 100644 --- a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelDataAssertion.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '@grafana/plugin-e2e'; +import { setVisualization } from '../../../panels-suite/vizpicker-utils'; import { formatExpectError } from '../errors'; import { successfulDataQuery } from '../mocks/queries'; @@ -24,10 +25,10 @@ test.describe( ).toContainText(['Field', 'Max', 'Mean', 'Last']); }); - test('table panel data assertions', async ({ panelEditPage }) => { + test('table panel data assertions', async ({ panelEditPage, selectors }) => { await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); await panelEditPage.datasource.set('gdev-testdata'); - await panelEditPage.setVisualization('Table'); + await setVisualization(panelEditPage, 'Table', selectors); await panelEditPage.refreshPanel(); await expect( panelEditPage.panel.locator, @@ -43,10 +44,10 @@ test.describe( ).toContainText(['val1', 'val2', 'val3', 'val4']); }); - test('timeseries panel - table view assertions', async ({ panelEditPage }) => { + test('timeseries panel - table view assertions', async ({ panelEditPage, selectors }) => { await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); await panelEditPage.datasource.set('gdev-testdata'); - await panelEditPage.setVisualization('Time series'); + await setVisualization(panelEditPage, 'Time series', selectors); await panelEditPage.refreshPanel(); await panelEditPage.toggleTableView(); await expect( diff --git a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts index 6ea57641a7f..93e0525ab0e 100644 --- a/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts +++ b/e2e-playwright/plugin-e2e/plugin-e2e-api-tests/as-admin-user/panelEditPage.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from '@grafana/plugin-e2e'; +import { setVisualization } from '../../../panels-suite/vizpicker-utils'; import { formatExpectError } from '../errors'; import { successfulDataQuery } from '../mocks/queries'; import { scenarios } from '../mocks/resources'; @@ -53,10 +54,10 @@ test.describe( ).toHaveText(scenarios.map((s) => s.name)); }); - test('mocked query data response', async ({ panelEditPage, page }) => { + test('mocked query data response', async ({ panelEditPage, page, selectors }) => { await panelEditPage.mockQueryDataResponse(successfulDataQuery, 200); await panelEditPage.datasource.set('gdev-testdata'); - await panelEditPage.setVisualization(TABLE_VIZ_NAME); + await setVisualization(panelEditPage, TABLE_VIZ_NAME, selectors); await panelEditPage.refreshPanel(); await expect( panelEditPage.panel.getErrorIcon(), @@ -75,9 +76,9 @@ test.describe( selectors, page, }) => { - await panelEditPage.setVisualization(TABLE_VIZ_NAME); + await setVisualization(panelEditPage, TABLE_VIZ_NAME, selectors); await expect( - panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.toggleVizPicker), + panelEditPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header), formatExpectError('Expected panel visualization to be set to table') ).toHaveText(TABLE_VIZ_NAME); await panelEditPage.setPanelTitle(PANEL_TITLE); @@ -92,8 +93,8 @@ test.describe( ).toBeVisible(); }); - test('Select time zone in timezone picker', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + test('Select time zone in timezone picker', async ({ panelEditPage, selectors }) => { + await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); const axisOptions = await panelEditPage.getCustomOptions('Axis'); const timeZonePicker = axisOptions.getSelect('Time zone'); @@ -101,8 +102,8 @@ test.describe( await expect(timeZonePicker).toHaveSelected('Europe/Stockholm'); }); - test('select unit in unit picker', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + test('select unit in unit picker', async ({ panelEditPage, selectors }) => { + await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); const standardOptions = panelEditPage.getStandardOptions(); const unitPicker = standardOptions.getUnitPicker('Unit'); @@ -111,8 +112,8 @@ test.describe( await expect(unitPicker).toHaveSelected('Pixels'); }); - test('enter value in number input', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + test('enter value in number input', async ({ panelEditPage, selectors }) => { + await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); const axisOptions = panelEditPage.getCustomOptions('Axis'); const lineWith = axisOptions.getNumberInput('Soft min'); @@ -121,8 +122,8 @@ test.describe( await expect(lineWith).toHaveValue('10'); }); - test('enter value in slider', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + test('enter value in slider', async ({ panelEditPage, selectors }) => { + await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); const graphOptions = panelEditPage.getCustomOptions('Graph styles'); const lineWidth = graphOptions.getSliderInput('Line width'); @@ -131,8 +132,8 @@ test.describe( await expect(lineWidth).toHaveValue('10'); }); - test('select value in single value select', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + test('select value in single value select', async ({ panelEditPage, selectors }) => { + await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); const standardOptions = panelEditPage.getStandardOptions(); const colorSchemeSelect = standardOptions.getSelect('Color scheme'); @@ -140,8 +141,8 @@ test.describe( await expect(colorSchemeSelect).toHaveSelected('Classic palette'); }); - test('clear input', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + test('clear input', async ({ panelEditPage, selectors }) => { + await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); const panelOptions = panelEditPage.getPanelOptions(); const title = panelOptions.getTextInput('Title'); @@ -150,8 +151,8 @@ test.describe( await expect(title).toHaveValue(''); }); - test('enter value in input', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + test('enter value in input', async ({ panelEditPage, selectors }) => { + await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); const panelOptions = panelEditPage.getPanelOptions(); const description = panelOptions.getTextInput('Description'); @@ -160,8 +161,8 @@ test.describe( await expect(description).toHaveValue('This is a panel'); }); - test('unchecking switch', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + test('unchecking switch', async ({ panelEditPage, selectors }) => { + await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); const axisOptions = panelEditPage.getCustomOptions('Axis'); const showBorder = axisOptions.getSwitch('Show border'); @@ -173,8 +174,8 @@ test.describe( await expect(showBorder).toBeChecked({ checked: false }); }); - test('checking switch', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + test('checking switch', async ({ panelEditPage, selectors }) => { + await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); const axisOptions = panelEditPage.getCustomOptions('Axis'); const showBorder = axisOptions.getSwitch('Show border'); @@ -183,8 +184,8 @@ test.describe( await expect(showBorder).toBeChecked(); }); - test('re-selecting value in radio button group', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + test('re-selecting value in radio button group', async ({ panelEditPage, selectors }) => { + await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); const axisOptions = panelEditPage.getCustomOptions('Axis'); const placement = axisOptions.getRadioGroup('Placement'); @@ -195,8 +196,8 @@ test.describe( await expect(placement).toHaveChecked('Auto'); }); - test('selecting value in radio button group', async ({ panelEditPage }) => { - await panelEditPage.setVisualization(TIME_SERIES_VIZ_NAME); + test('selecting value in radio button group', async ({ panelEditPage, selectors }) => { + await setVisualization(panelEditPage, TIME_SERIES_VIZ_NAME, selectors); const axisOptions = panelEditPage.getCustomOptions('Axis'); const placement = axisOptions.getRadioGroup('Placement'); diff --git a/e2e-playwright/smoke-tests-suite/panels.spec.ts b/e2e-playwright/smoke-tests-suite/panels.spec.ts index ded102294ce..22f6088d99d 100644 --- a/e2e-playwright/smoke-tests-suite/panels.spec.ts +++ b/e2e-playwright/smoke-tests-suite/panels.spec.ts @@ -45,7 +45,9 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.PluginVisualization.item(panel.name)).click(); // Verify panel type is selected - await expect(vizPicker).toHaveText(panel.name, { timeout: 10000 }); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.PanelEditor.OptionsPane.header) + ).toHaveText(panel.name, { timeout: 10000 }); // Wait for panel to finish rendering await expect(page.getByLabel('Panel loading bar')).toHaveCount(0, { timeout: 10000 }); diff --git a/e2e/old-arch/smoke-tests-suite/panels_smokescreen.spec.ts b/e2e/old-arch/smoke-tests-suite/panels_smokescreen.spec.ts index 36f091803d7..cc4b9f1115d 100644 --- a/e2e/old-arch/smoke-tests-suite/panels_smokescreen.spec.ts +++ b/e2e/old-arch/smoke-tests-suite/panels_smokescreen.spec.ts @@ -30,7 +30,7 @@ describe('Panels smokescreen', () => { e2e.components.PanelEditor.toggleVizPicker().click(); e2e.components.PluginVisualization.item(panel.name).scrollIntoView().should('be.visible').click(); - e2e.components.PanelEditor.toggleVizPicker().should((e) => expect(e).to.contain(panel.name)); + e2e.components.PanelEditor.OptionsPane.content().should((e) => expect(e).to.contain(panel.name)); // TODO: Come up with better check / better failure messaging to clearly indicate which panel failed cy.contains('An unexpected error happened').should('not.exist'); } diff --git a/go.work.sum b/go.work.sum index c588f44718e..7cec67c61e5 100644 --- a/go.work.sum +++ b/go.work.sum @@ -72,7 +72,6 @@ cloud.google.com/go/cloudtasks v1.13.6/go.mod h1:/IDaQqGKMixD+ayM43CfsvWF2k36Geo cloud.google.com/go/compute v1.40.0 h1:dlEzKo/BtyEGNc+SflXwwoBh52dNl/A5BaSYurT0k0k= cloud.google.com/go/compute v1.40.0/go.mod h1:P1doTJnlwurJDzIQFMp4mgU+vyCe9HU2NWTlqTfq3MY= cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= -cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= cloud.google.com/go/contactcenterinsights v1.17.3 h1:lenyU3uzHwKDveCwmpfNxHYvLS3uEBWdn+O7+rSxy+Q= cloud.google.com/go/contactcenterinsights v1.17.3/go.mod h1:7Uu2CpxS3f6XxhRdlEzYAkrChpR5P5QfcdGAFEdHOG8= @@ -309,7 +308,6 @@ github.com/GoogleCloudPlatform/cloudsql-proxy v1.37.8/go.mod h1:exon/I6I+5u/ab7A github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 h1:2afWGsMzkIcN8Qm4mgPJKZWyroE5QBszMiDMYEBrnfw= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.0 h1:YVtMlmfRUTaWs3+1acwMBp7rBUo6zrxl6Kn13/R9YW4= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.29.0/go.mod h1:rKOFVIPbNs2wZeh7ZeQ0D9p/XLgbNiTr5m7x6KuAshk= @@ -549,7 +547,6 @@ github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nC github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= -github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c h1:2zRrJWIt/f9c9HhNHAgrRgq0San5gRRUJTBXLkchal0= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= @@ -705,9 +702,7 @@ github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRr github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= -github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane/envoy v1.32.3/go.mod h1:F6hWupPfh75TBXGKA++MCT/CZHFq5r9/uwt/kQYkZfE= -github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= @@ -754,7 +749,6 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA= -github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w= github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s= github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= @@ -872,7 +866,6 @@ github.com/grafana/grafana-app-sdk v0.41.0 h1:SYHN3U7B1myRKY3UZZDkFsue9TDmAOap0U github.com/grafana/grafana-app-sdk v0.41.0/go.mod h1:Wg/3vEZfok1hhIWiHaaJm+FwkosfO98o8KbeLFEnZpY= github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc= github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA= -github.com/grafana/grafana-app-sdk v0.48.2/go.mod h1:LDOvQ7OOyHLcXdSa0InATCa5OMoYAd6E1+rGLrMgHuk= github.com/grafana/grafana-app-sdk/logging v0.38.0/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk= github.com/grafana/grafana-app-sdk/logging v0.39.0 h1:3GgN5+dUZYqq74Q+GT9/ET+yo+V54zWQk/Q2/JsJQB4= github.com/grafana/grafana-app-sdk/logging v0.39.0/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk= @@ -886,7 +879,6 @@ github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5 github.com/grafana/grafana-app-sdk/logging v0.46.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-app-sdk/logging v0.48.0 h1:xolkQxBlA2LQF4hprKIAeu+zUem1DigYZ6XC1TOhFJE= github.com/grafana/grafana-app-sdk/logging v0.48.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= -github.com/grafana/grafana-app-sdk/logging v0.48.1/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-app-sdk/logging v0.48.2 h1:tI+a9slUvxKUgweXDzUqkca2LWV3g1UdaSvwt8nQNHg= github.com/grafana/grafana-app-sdk/logging v0.48.2/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= github.com/grafana/grafana-app-sdk/plugin v0.41.0 h1:ShUvGpAVzM3UxcsfwS6l/lwW4ytDeTbCQXf8w2P8Yp8= @@ -1436,7 +1428,6 @@ github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmq github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= -github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc= github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -1792,7 +1783,6 @@ go.opentelemetry.io/contrib/config v0.14.0/go.mod h1:77rDmFPqBae5jtQ2C78RuDTHz4P go.opentelemetry.io/contrib/detectors/aws/ec2 v1.37.0 h1:BJnWw8+FULhuuF/6R6B/JYqAlCTCy9E4J8qmLpo/7KU= go.opentelemetry.io/contrib/detectors/aws/ec2 v1.37.0/go.mod h1:gs3y8jvJscW5D+FzrZvJZEsGj+xlMCF0S1x4R6ktiNo= go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k= -go.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU= go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.59.0 h1:I8k9HW4yl8SRYNmECKKtjhcOvq9lAP9riqYPixBU3qw= go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho v0.59.0/go.mod h1:/vTiuiSKBQAerQeMB3CsVJbXd+cvTbhcdOk5AV5Z5R0= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0/go.mod h1:vsh3ySueQCiKPxFLvjWC4Z135gIa34TQ/NSqkDTZYUM= @@ -2080,7 +2070,6 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go. google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA= google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE= -google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw= google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822 h1:zWFRixYR5QlotL+Uv3YfsPRENIrQFXiGs+iwqel6fOQ= google.golang.org/genproto/googleapis/bytestream v0.0.0-20250603155806-513f23925822/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= @@ -2110,7 +2099,6 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go. google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251002232023-7c0ddcbb5797/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= @@ -2131,7 +2119,6 @@ google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7E google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM= google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI= google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0= @@ -2252,4 +2239,4 @@ sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= tags.cncf.io/container-device-interface v0.7.2/go.mod h1:Xb1PvXv2BhfNb3tla4r9JL129ck1Lxv9KuU6eVOfKto= tags.cncf.io/container-device-interface/specs-go v0.7.0/go.mod h1:hMAwAbMZyBLdmYqWgYcKH0F/yctNpV3P35f+/088A80= -xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= +xorm.io/builder v0.3.6/go.mod h1:LEFAPISnRzG+zxaxj2vPicRwz67BdhFreKg8yv8/TgU= \ No newline at end of file diff --git a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts index d2cc21d31e6..e2346105856 100644 --- a/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts +++ b/packages/grafana-api-clients/src/clients/rtkq/dashboard/v0alpha1/endpoints.gen.ts @@ -1,5 +1,5 @@ import { api } from './baseAPI'; -export const addTagTypes = ['API Discovery', 'Dashboard', 'LibraryPanel', 'Search'] as const; +export const addTagTypes = ['API Discovery', 'Dashboard', 'LibraryPanel', 'Search', 'Snapshot'] as const; const injectedRtkApi = api .enhanceEndpoints({ addTagTypes, @@ -257,6 +257,61 @@ const injectedRtkApi = api query: () => ({ url: `/search/sortable` }), providesTags: ['Search'], }), + listSnapshot: build.query({ + query: (queryArg) => ({ + url: `/snapshots`, + params: { + allowWatchBookmarks: queryArg.allowWatchBookmarks, + continue: queryArg['continue'], + fieldSelector: queryArg.fieldSelector, + labelSelector: queryArg.labelSelector, + limit: queryArg.limit, + pretty: queryArg.pretty, + resourceVersion: queryArg.resourceVersion, + resourceVersionMatch: queryArg.resourceVersionMatch, + sendInitialEvents: queryArg.sendInitialEvents, + timeoutSeconds: queryArg.timeoutSeconds, + watch: queryArg.watch, + }, + }), + providesTags: ['Snapshot'], + }), + createSnapshot: build.mutation({ + query: (queryArg) => ({ url: `/snapshots/create`, method: 'POST', body: queryArg.body }), + invalidatesTags: ['Snapshot'], + }), + deleteWithKey: build.mutation({ + query: (queryArg) => ({ url: `/snapshots/delete/${queryArg.deleteKey}`, method: 'DELETE' }), + invalidatesTags: ['Snapshot'], + }), + getSnapshot: build.query({ + query: (queryArg) => ({ + url: `/snapshots/${queryArg.name}`, + params: { + pretty: queryArg.pretty, + }, + }), + providesTags: ['Snapshot'], + }), + deleteSnapshot: build.mutation({ + query: (queryArg) => ({ + url: `/snapshots/${queryArg.name}`, + method: 'DELETE', + params: { + pretty: queryArg.pretty, + dryRun: queryArg.dryRun, + gracePeriodSeconds: queryArg.gracePeriodSeconds, + ignoreStoreReadErrorWithClusterBreakingPotential: queryArg.ignoreStoreReadErrorWithClusterBreakingPotential, + orphanDependents: queryArg.orphanDependents, + propagationPolicy: queryArg.propagationPolicy, + }, + }), + invalidatesTags: ['Snapshot'], + }), + getSnapshotDashboard: build.query({ + query: (queryArg) => ({ url: `/snapshots/${queryArg.name}/dashboard` }), + providesTags: ['Snapshot'], + }), }), overrideExisting: false, }); @@ -630,6 +685,89 @@ export type GetSearchSortableApiResponse = /** status 200 undefined */ { kind?: string; }; export type GetSearchSortableApiArg = void; +export type ListSnapshotApiResponse = /** status 200 OK */ SnapshotList; +export type ListSnapshotApiArg = { + /** allowWatchBookmarks requests watch events with type "BOOKMARK". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored. */ + allowWatchBookmarks?: boolean; + /** The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the "next key". + + This field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications. */ + continue?: string; + /** A selector to restrict the list of returned objects by their fields. Defaults to everything. */ + fieldSelector?: string; + /** A selector to restrict the list of returned objects by their labels. Defaults to everything. */ + labelSelector?: string; + /** limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true. + + The server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned. */ + limit?: number; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersion?: string; + /** resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details. + + Defaults to unset */ + resourceVersionMatch?: string; + /** `sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic "Bookmark" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `"k8s.io/initial-events-end": "true"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched. + + When `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan + is interpreted as "data at least as new as the provided `resourceVersion`" + and the bookmark event is send when the state is synced + to a `resourceVersion` at least as fresh as the one provided by the ListOptions. + If `resourceVersion` is unset, this is interpreted as "consistent read" and the + bookmark event is send when the state is synced at least to the moment + when request started being processed. + - `resourceVersionMatch` set to any other value or unset + Invalid error is returned. + + Defaults to true if `resourceVersion=""` or `resourceVersion="0"` (for backward compatibility reasons) and to false otherwise. */ + sendInitialEvents?: boolean; + /** Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity. */ + timeoutSeconds?: number; + /** Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion. */ + watch?: boolean; +}; +export type CreateSnapshotApiResponse = /** status 200 undefined */ any; +export type CreateSnapshotApiArg = { + body: any; +}; +export type DeleteWithKeyApiResponse = unknown; +export type DeleteWithKeyApiArg = { + /** unique key returned in create */ + deleteKey: string; +}; +export type GetSnapshotApiResponse = /** status 200 OK */ Snapshot; +export type GetSnapshotApiArg = { + /** name of the Snapshot */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; +}; +export type DeleteSnapshotApiResponse = /** status 200 OK */ Status | /** status 202 Accepted */ Status; +export type DeleteSnapshotApiArg = { + /** name of the Snapshot */ + name: string; + /** If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget). */ + pretty?: string; + /** When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed */ + dryRun?: string; + /** The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately. */ + gracePeriodSeconds?: number; + /** if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it */ + ignoreStoreReadErrorWithClusterBreakingPotential?: boolean; + /** Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the "orphan" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both. */ + orphanDependents?: boolean; + /** Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground. */ + propagationPolicy?: string; +}; +export type GetSnapshotDashboardApiResponse = /** status 200 OK */ Dashboard; +export type GetSnapshotDashboardApiArg = { + /** name of the Dashboard */ + name: string; +}; export type ApiResource = { /** categories is a list of the grouped resources this resource belongs to (e.g. 'all') */ categories?: string[]; @@ -1066,6 +1204,41 @@ export type SearchResults = { /** The number of matching results */ totalHits: number; }; +export type SnapshotSpec = { + /** The raw dashboard (unstructured for now) */ + dashboard?: { + [key: string]: object; + }; + /** Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds) */ + expires?: number; + /** When set to true, the snapshot exists in a remote server */ + external?: boolean; + /** The external URL where the snapshot can be seen */ + externalUrl?: string; + /** The URL that created the dashboard originally */ + originalUrl?: string; + /** Snapshot creation timestamp */ + timestamp?: string; + /** Snapshot title */ + title?: string; +}; +export type Snapshot = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ObjectMeta; + /** Spec is the spec of the Snapshot */ + spec: SnapshotSpec; +}; +export type SnapshotList = { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + items: Snapshot[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata: ListMeta; +}; export const { useGetApiResourcesQuery, useLazyGetApiResourcesQuery, @@ -1093,4 +1266,13 @@ export const { useLazyGetSearchQuery, useGetSearchSortableQuery, useLazyGetSearchSortableQuery, + useListSnapshotQuery, + useLazyListSnapshotQuery, + useCreateSnapshotMutation, + useDeleteWithKeyMutation, + useGetSnapshotQuery, + useLazyGetSnapshotQuery, + useDeleteSnapshotMutation, + useGetSnapshotDashboardQuery, + useLazyGetSnapshotDashboardQuery, } = injectedRtkApi; diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index c643a0d7f2c..47c30187175 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -583,6 +583,9 @@ export const versionedComponents = { '11.1.0': 'data-testid Panel editor option pane content', [MIN_GRAFANA_VERSION]: 'Panel editor option pane content', }, + header: { + '12.4.0': 'data-testid Panel editor OptionsPane header', + }, select: { [MIN_GRAFANA_VERSION]: 'Panel editor option pane select', }, diff --git a/packages/grafana-schema/src/schema/dashboardsnapshot/v0alpha1/dashboardsnapshot_object_gen.ts b/packages/grafana-schema/src/schema/dashboardsnapshot/v0alpha1/dashboardsnapshot_object_gen.ts new file mode 100644 index 00000000000..7b26d2f1680 --- /dev/null +++ b/packages/grafana-schema/src/schema/dashboardsnapshot/v0alpha1/dashboardsnapshot_object_gen.ts @@ -0,0 +1,47 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface DashboardSnapshot { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; +} diff --git a/packages/grafana-schema/src/schema/dashboardsnapshot/v0alpha1/types.metadata.gen.ts b/packages/grafana-schema/src/schema/dashboardsnapshot/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/packages/grafana-schema/src/schema/dashboardsnapshot/v0alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/packages/grafana-schema/src/schema/dashboardsnapshot/v0alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/dashboardsnapshot/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..8ea89517036 --- /dev/null +++ b/packages/grafana-schema/src/schema/dashboardsnapshot/v0alpha1/types.spec.gen.ts @@ -0,0 +1,22 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface Spec { + // Snapshot title + title?: string; + // Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds) + expires?: number; + // When set to true, the snapshot exists in a remote server + external?: boolean; + // The external URL where the snapshot can be seen + externalUrl?: string; + // The URL that created the dashboard originally + originalUrl?: string; + // Snapshot creation timestamp + timestamp?: string; +} + +export const defaultSpec = (): Spec => ({ + expires: 0, + external: false, +}); + diff --git a/packages/grafana-schema/src/schema/sharingoption/v0alpha1/sharingoption_object_gen.ts b/packages/grafana-schema/src/schema/sharingoption/v0alpha1/sharingoption_object_gen.ts new file mode 100644 index 00000000000..4d0b362b429 --- /dev/null +++ b/packages/grafana-schema/src/schema/sharingoption/v0alpha1/sharingoption_object_gen.ts @@ -0,0 +1,47 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface SharingOption { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; +} diff --git a/packages/grafana-schema/src/schema/sharingoption/v0alpha1/types.metadata.gen.ts b/packages/grafana-schema/src/schema/sharingoption/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/packages/grafana-schema/src/schema/sharingoption/v0alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/packages/grafana-schema/src/schema/sharingoption/v0alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/sharingoption/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..494b64f3aa5 --- /dev/null +++ b/packages/grafana-schema/src/schema/sharingoption/v0alpha1/types.spec.gen.ts @@ -0,0 +1,18 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface Spec { + // Snapshot title + snapshotsEnabled?: boolean; + // The external URL where the snapshot can be pushed + externalSnapshotURL?: string; + // The external name of the snapshot in the remote server + externalSnapshotName?: string; + // External snapshots feature enabled + externalEnabled?: boolean; +} + +export const defaultSpec = (): Spec => ({ + snapshotsEnabled: false, + externalEnabled: false, +}); + diff --git a/packages/grafana-schema/src/schema/snapshot/v0alpha1/snapshot_object_gen.ts b/packages/grafana-schema/src/schema/snapshot/v0alpha1/snapshot_object_gen.ts new file mode 100644 index 00000000000..aa51cfd1b24 --- /dev/null +++ b/packages/grafana-schema/src/schema/snapshot/v0alpha1/snapshot_object_gen.ts @@ -0,0 +1,47 @@ +/* + * This file was generated by grafana-app-sdk. DO NOT EDIT. + */ +import { Spec } from './types.spec.gen'; + +export interface Metadata { + name: string; + namespace: string; + generateName?: string; + selfLink?: string; + uid?: string; + resourceVersion?: string; + generation?: number; + creationTimestamp?: string; + deletionTimestamp?: string; + deletionGracePeriodSeconds?: number; + labels?: Record; + annotations?: Record; + ownerReferences?: OwnerReference[]; + finalizers?: string[]; + managedFields?: ManagedFieldsEntry[]; +} + +export interface OwnerReference { + apiVersion: string; + kind: string; + name: string; + uid: string; + controller?: boolean; + blockOwnerDeletion?: boolean; +} + +export interface ManagedFieldsEntry { + manager?: string; + operation?: string; + apiVersion?: string; + time?: string; + fieldsType?: string; + subresource?: string; +} + +export interface Snapshot { + kind: string; + apiVersion: string; + metadata: Metadata; + spec: Spec; +} diff --git a/packages/grafana-schema/src/schema/snapshot/v0alpha1/types.metadata.gen.ts b/packages/grafana-schema/src/schema/snapshot/v0alpha1/types.metadata.gen.ts new file mode 100644 index 00000000000..4377f3c1d08 --- /dev/null +++ b/packages/grafana-schema/src/schema/snapshot/v0alpha1/types.metadata.gen.ts @@ -0,0 +1,30 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +// metadata contains embedded CommonMetadata and can be extended with custom string fields +// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here +// without external reference as using the CommonMetadata reference breaks thema codegen. +export interface Metadata { + updateTimestamp: string; + createdBy: string; + uid: string; + creationTimestamp: string; + deletionTimestamp?: string; + finalizers: string[]; + resourceVersion: string; + generation: number; + updatedBy: string; + labels: Record; +} + +export const defaultMetadata = (): Metadata => ({ + updateTimestamp: "", + createdBy: "", + uid: "", + creationTimestamp: "", + finalizers: [], + resourceVersion: "", + generation: 0, + updatedBy: "", + labels: {}, +}); + diff --git a/packages/grafana-schema/src/schema/snapshot/v0alpha1/types.spec.gen.ts b/packages/grafana-schema/src/schema/snapshot/v0alpha1/types.spec.gen.ts new file mode 100644 index 00000000000..c84e3463409 --- /dev/null +++ b/packages/grafana-schema/src/schema/snapshot/v0alpha1/types.spec.gen.ts @@ -0,0 +1,24 @@ +// Code generated - EDITING IS FUTILE. DO NOT EDIT. + +export interface Spec { + // Snapshot title + title?: string; + // Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds) + expires?: number; + // When set to true, the snapshot exists in a remote server + external?: boolean; + // The external URL where the snapshot can be seen + externalUrl?: string; + // The URL that created the dashboard originally + originalUrl?: string; + // Snapshot creation timestamp + timestamp?: string; + // The raw dashboard (unstructured for now) + dashboard?: Record; +} + +export const defaultSpec = (): Spec => ({ + expires: 0, + external: false, +}); + diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 539b5fd9f33..49194a625f4 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -7,10 +7,10 @@ import ( "strconv" "time" + snapshot "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/identity" - dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/infra/metrics" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" @@ -34,8 +34,8 @@ func (hs *HTTPServer) getCreatedSnapshotHandler() web.Handler { errhttp.Write(r.Context(), fmt.Errorf("no user"), w) return } - r.URL.Path = "/apis/dashboardsnapshot.grafana.app/v0alpha1/namespaces/" + - namespaceMapper(user.GetOrgID()) + "/dashboardsnapshots/create" + r.URL.Path = "/apis/dashboard.grafana.app/v0alpha1/namespaces/" + + namespaceMapper(user.GetOrgID()) + "/snapshots/create" hs.clientConfigProvider.DirectlyServeHTTP(w, r) } } @@ -85,7 +85,7 @@ func (hs *HTTPServer) CreateDashboardSnapshot(c *contextmodel.ReqContext) { } } - dashboardsnapshots.CreateDashboardSnapshot(c, dashboardsnapshot.SnapshotSharingOptions{ + dashboardsnapshots.CreateDashboardSnapshot(c, snapshot.SnapshotSharingOptions{ SnapshotsEnabled: hs.Cfg.SnapshotEnabled, ExternalEnabled: hs.Cfg.ExternalEnabled, ExternalSnapshotName: hs.Cfg.ExternalSnapshotName, diff --git a/pkg/apis/dashboardsnapshot/v0alpha1/doc.go b/pkg/apis/dashboardsnapshot/v0alpha1/doc.go deleted file mode 100644 index a6b2fec52cb..00000000000 --- a/pkg/apis/dashboardsnapshot/v0alpha1/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// +k8s:deepcopy-gen=package -// +k8s:openapi-gen=true -// +k8s:defaulter-gen=TypeMeta -// +groupName=dashboardsnapshot.grafana.app - -package v0alpha1 diff --git a/pkg/apis/dashboardsnapshot/v0alpha1/register.go b/pkg/apis/dashboardsnapshot/v0alpha1/register.go deleted file mode 100644 index b2bb3ecd70c..00000000000 --- a/pkg/apis/dashboardsnapshot/v0alpha1/register.go +++ /dev/null @@ -1,46 +0,0 @@ -package v0alpha1 - -import ( - "fmt" - "time" - - "github.com/grafana/grafana/pkg/apimachinery/utils" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -const ( - GROUP = "dashboardsnapshot.grafana.app" - VERSION = "v0alpha1" - APIVERSION = GROUP + "/" + VERSION -) - -var DashboardSnapshotResourceInfo = utils.NewResourceInfo(GROUP, VERSION, - "dashboardsnapshots", "dashboardsnapshot", "DashboardSnapshot", - func() runtime.Object { return &DashboardSnapshot{} }, - func() runtime.Object { return &DashboardSnapshotList{} }, - utils.TableColumns{ - Definition: []metav1.TableColumnDefinition{ - {Name: "Name", Type: "string", Format: "name"}, - {Name: "Title", Type: "string", Format: "string", Description: "The snapshot name"}, - {Name: "Created At", Type: "date"}, - }, - Reader: func(obj any) ([]interface{}, error) { - m, ok := obj.(*DashboardSnapshot) - if ok { - return []interface{}{ - m.Name, - m.Spec.Title, - m.CreationTimestamp.UTC().Format(time.RFC3339), - }, nil - } - return nil, fmt.Errorf("expected snapshot") - }, - }, // default table converter -) - -var ( - // SchemeGroupVersion is group version used to register these objects - SchemeGroupVersion = schema.GroupVersion{Group: GROUP, Version: VERSION} -) diff --git a/pkg/apis/dashboardsnapshot/v0alpha1/types.go b/pkg/apis/dashboardsnapshot/v0alpha1/types.go deleted file mode 100644 index 64f0263f146..00000000000 --- a/pkg/apis/dashboardsnapshot/v0alpha1/types.go +++ /dev/null @@ -1,134 +0,0 @@ -package v0alpha1 - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" -) - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DashboardSnapshot struct { - metav1.TypeMeta `json:",inline"` - // +optional - metav1.ObjectMeta `json:"metadata,omitempty"` - - // Snapshot summary info - Spec SnapshotInfo `json:"spec"` -} - -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DashboardSnapshotList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []DashboardSnapshot `json:"items"` -} - -type SnapshotInfo struct { - Title string `json:"title,omitempty"` - // Optionally auto-remove the snapshot at a future date - Expires int64 `json:"expires,omitempty"` - // When set to true, the snapshot exists in a remote server - External bool `json:"external,omitempty"` - // The external URL where the snapshot can be seen - ExternalURL string `json:"externalUrl,omitempty"` - // The URL that created the dashboard originally - OriginalUrl string `json:"originalUrl,omitempty"` - // Snapshot creation timestamp - Timestamp string `json:"timestamp,omitempty"` -} - -// This is returned from the POST command -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DashboardSnapshotWithDeleteKey struct { - DashboardSnapshot `json:",inline"` - - // The delete key is only returned when the item is created. It is not returned from a get request - DeleteKey string `json:"deleteKey,omitempty"` -} - -// This is the snapshot returned from the subresource -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type FullDashboardSnapshot struct { - metav1.TypeMeta `json:",inline"` - // +optional - metav1.ObjectMeta `json:"metadata,omitempty"` - - // Snapshot summary info - Info SnapshotInfo `json:"info"` - - // The raw dashboard (unstructured for now) - Dashboard common.Unstructured `json:"dashboard"` -} - -// Each tenant, may have different sharing options -// This is currently set using custom.ini, but multi-tenant support will need -// to be managed differently -type SnapshotSharingOptions struct { - SnapshotsEnabled bool `json:"snapshotEnabled"` - ExternalSnapshotURL string `json:"externalSnapshotURL,omitempty"` - ExternalSnapshotName string `json:"externalSnapshotName,omitempty"` - ExternalEnabled bool `json:"externalEnabled,omitempty"` -} - -// These are the values expected to be sent from an end user -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DashboardCreateCommand struct { - metav1.TypeMeta `json:",inline"` - - // Snapshot name - // required:false - Name string `json:"name"` - - // The complete dashboard model. - // required:true - Dashboard *common.Unstructured `json:"dashboard" binding:"Required"` - - // When the snapshot should expire in seconds in seconds. Default is never to expire. - // required:false - // default:0 - Expires int64 `json:"expires"` - - // these are passed when storing an external snapshot ref - // Save the snapshot on an external server rather than locally. - // required:false - // default: false - External bool `json:"external"` -} - -// The create response -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type DashboardCreateResponse struct { - metav1.TypeMeta `json:",inline"` - - // The unique key - Key string `json:"key"` - - // A unique key that will allow delete - DeleteKey string `json:"deleteKey"` - - // Absolute URL to show the dashboard - URL string `json:"url"` - - // URL that will delete the response - DeleteURL string `json:"deleteUrl"` -} - -// Represents an options object that must be named for each namespace/team/user -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type SharingOptions struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - // Show the options inline - Spec SnapshotSharingOptions `json:"spec"` -} - -// Represents a list of namespaced options -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type SharingOptionsList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []SharingOptions `json:"items"` -} diff --git a/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.deepcopy.go b/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.deepcopy.go deleted file mode 100644 index 51b5075f310..00000000000 --- a/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.deepcopy.go +++ /dev/null @@ -1,271 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by deepcopy-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DashboardCreateCommand) DeepCopyInto(out *DashboardCreateCommand) { - *out = *in - out.TypeMeta = in.TypeMeta - if in.Dashboard != nil { - in, out := &in.Dashboard, &out.Dashboard - *out = (*in).DeepCopy() - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardCreateCommand. -func (in *DashboardCreateCommand) DeepCopy() *DashboardCreateCommand { - if in == nil { - return nil - } - out := new(DashboardCreateCommand) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DashboardCreateCommand) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DashboardCreateResponse) DeepCopyInto(out *DashboardCreateResponse) { - *out = *in - out.TypeMeta = in.TypeMeta - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardCreateResponse. -func (in *DashboardCreateResponse) DeepCopy() *DashboardCreateResponse { - if in == nil { - return nil - } - out := new(DashboardCreateResponse) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DashboardCreateResponse) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DashboardSnapshot) DeepCopyInto(out *DashboardSnapshot) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSnapshot. -func (in *DashboardSnapshot) DeepCopy() *DashboardSnapshot { - if in == nil { - return nil - } - out := new(DashboardSnapshot) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DashboardSnapshot) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DashboardSnapshotList) DeepCopyInto(out *DashboardSnapshotList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]DashboardSnapshot, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSnapshotList. -func (in *DashboardSnapshotList) DeepCopy() *DashboardSnapshotList { - if in == nil { - return nil - } - out := new(DashboardSnapshotList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DashboardSnapshotList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DashboardSnapshotWithDeleteKey) DeepCopyInto(out *DashboardSnapshotWithDeleteKey) { - *out = *in - in.DashboardSnapshot.DeepCopyInto(&out.DashboardSnapshot) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DashboardSnapshotWithDeleteKey. -func (in *DashboardSnapshotWithDeleteKey) DeepCopy() *DashboardSnapshotWithDeleteKey { - if in == nil { - return nil - } - out := new(DashboardSnapshotWithDeleteKey) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *DashboardSnapshotWithDeleteKey) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FullDashboardSnapshot) DeepCopyInto(out *FullDashboardSnapshot) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Info = in.Info - in.Dashboard.DeepCopyInto(&out.Dashboard) - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FullDashboardSnapshot. -func (in *FullDashboardSnapshot) DeepCopy() *FullDashboardSnapshot { - if in == nil { - return nil - } - out := new(FullDashboardSnapshot) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *FullDashboardSnapshot) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SharingOptions) DeepCopyInto(out *SharingOptions) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SharingOptions. -func (in *SharingOptions) DeepCopy() *SharingOptions { - if in == nil { - return nil - } - out := new(SharingOptions) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *SharingOptions) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SharingOptionsList) DeepCopyInto(out *SharingOptionsList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]SharingOptions, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SharingOptionsList. -func (in *SharingOptionsList) DeepCopy() *SharingOptionsList { - if in == nil { - return nil - } - out := new(SharingOptionsList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *SharingOptionsList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SnapshotInfo) DeepCopyInto(out *SnapshotInfo) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotInfo. -func (in *SnapshotInfo) DeepCopy() *SnapshotInfo { - if in == nil { - return nil - } - out := new(SnapshotInfo) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SnapshotSharingOptions) DeepCopyInto(out *SnapshotSharingOptions) { - *out = *in - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SnapshotSharingOptions. -func (in *SnapshotSharingOptions) DeepCopy() *SnapshotSharingOptions { - if in == nil { - return nil - } - out := new(SnapshotSharingOptions) - in.DeepCopyInto(out) - return out -} diff --git a/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.defaults.go b/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.defaults.go deleted file mode 100644 index 238fc2f4edc..00000000000 --- a/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.defaults.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by defaulter-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - runtime "k8s.io/apimachinery/pkg/runtime" -) - -// RegisterDefaults adds defaulters functions to the given scheme. -// Public to allow building arbitrary schemes. -// All generated defaulters are covering - they call all nested defaulters. -func RegisterDefaults(scheme *runtime.Scheme) error { - return nil -} diff --git a/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.openapi.go b/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.openapi.go deleted file mode 100644 index 168a0614149..00000000000 --- a/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.openapi.go +++ /dev/null @@ -1,521 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// SPDX-License-Identifier: AGPL-3.0-only - -// Code generated by openapi-gen. DO NOT EDIT. - -package v0alpha1 - -import ( - common "k8s.io/kube-openapi/pkg/common" - spec "k8s.io/kube-openapi/pkg/validation/spec" -) - -func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { - return map[string]common.OpenAPIDefinition{ - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardCreateCommand": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateCommand(ref), - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardCreateResponse": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateResponse(ref), - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshot": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshot(ref), - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshotList": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshotList(ref), - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshotWithDeleteKey": schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshotWithDeleteKey(ref), - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.FullDashboardSnapshot": schema_pkg_apis_dashboardsnapshot_v0alpha1_FullDashboardSnapshot(ref), - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SharingOptions": schema_pkg_apis_dashboardsnapshot_v0alpha1_SharingOptions(ref), - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SharingOptionsList": schema_pkg_apis_dashboardsnapshot_v0alpha1_SharingOptionsList(ref), - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo": schema_pkg_apis_dashboardsnapshot_v0alpha1_SnapshotInfo(ref), - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotSharingOptions": schema_pkg_apis_dashboardsnapshot_v0alpha1_SnapshotSharingOptions(ref), - } -} - -func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateCommand(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "These are the values expected to be sent from an end user", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "name": { - SchemaProps: spec.SchemaProps{ - Description: "Snapshot name required:false", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "dashboard": { - SchemaProps: spec.SchemaProps{ - Description: "The complete dashboard model. required:true", - Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), - }, - }, - "expires": { - SchemaProps: spec.SchemaProps{ - Description: "When the snapshot should expire in seconds in seconds. Default is never to expire. required:false default:0", - Default: 0, - Type: []string{"integer"}, - Format: "int64", - }, - }, - "external": { - SchemaProps: spec.SchemaProps{ - Description: "these are passed when storing an external snapshot ref Save the snapshot on an external server rather than locally. required:false default: false", - Default: false, - Type: []string{"boolean"}, - Format: "", - }, - }, - }, - Required: []string{"name", "dashboard", "expires", "external"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"}, - } -} - -func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardCreateResponse(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "The create response", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "key": { - SchemaProps: spec.SchemaProps{ - Description: "The unique key", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "deleteKey": { - SchemaProps: spec.SchemaProps{ - Description: "A unique key that will allow delete", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "url": { - SchemaProps: spec.SchemaProps{ - Description: "Absolute URL to show the dashboard", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "deleteUrl": { - SchemaProps: spec.SchemaProps{ - Description: "URL that will delete the response", - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"key", "deleteKey", "url", "deleteUrl"}, - }, - }, - } -} - -func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshot(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Description: "Snapshot summary info", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo"), - }, - }, - }, - Required: []string{"spec"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshotList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshot"), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.DashboardSnapshot", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_dashboardsnapshot_v0alpha1_DashboardSnapshotWithDeleteKey(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "This is returned from the POST command", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Description: "Snapshot summary info", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo"), - }, - }, - "deleteKey": { - SchemaProps: spec.SchemaProps{ - Description: "The delete key is only returned when the item is created. It is not returned from a get request", - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"spec"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_dashboardsnapshot_v0alpha1_FullDashboardSnapshot(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "This is the snapshot returned from the subresource", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "info": { - SchemaProps: spec.SchemaProps{ - Description: "Snapshot summary info", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo"), - }, - }, - "dashboard": { - SchemaProps: spec.SchemaProps{ - Description: "The raw dashboard (unstructured for now)", - Ref: ref("github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured"), - }, - }, - }, - Required: []string{"info", "dashboard"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1.Unstructured", "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotInfo", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_dashboardsnapshot_v0alpha1_SharingOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Represents an options object that must be named for each namespace/team/user", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), - }, - }, - "spec": { - SchemaProps: spec.SchemaProps{ - Description: "Show the options inline", - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotSharingOptions"), - }, - }, - }, - Required: []string{"spec"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SnapshotSharingOptions", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, - } -} - -func schema_pkg_apis_dashboardsnapshot_v0alpha1_SharingOptionsList(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Represents a list of namespaced options", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "kind": { - SchemaProps: spec.SchemaProps{ - Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", - Type: []string{"string"}, - Format: "", - }, - }, - "apiVersion": { - SchemaProps: spec.SchemaProps{ - Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", - Type: []string{"string"}, - Format: "", - }, - }, - "metadata": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"), - }, - }, - "items": { - SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SharingOptions"), - }, - }, - }, - }, - }, - }, - Required: []string{"items"}, - }, - }, - Dependencies: []string{ - "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1.SharingOptions", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"}, - } -} - -func schema_pkg_apis_dashboardsnapshot_v0alpha1_SnapshotInfo(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "title": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "expires": { - SchemaProps: spec.SchemaProps{ - Description: "Optionally auto-remove the snapshot at a future date", - Type: []string{"integer"}, - Format: "int64", - }, - }, - "external": { - SchemaProps: spec.SchemaProps{ - Description: "When set to true, the snapshot exists in a remote server", - Type: []string{"boolean"}, - Format: "", - }, - }, - "externalUrl": { - SchemaProps: spec.SchemaProps{ - Description: "The external URL where the snapshot can be seen", - Type: []string{"string"}, - Format: "", - }, - }, - "originalUrl": { - SchemaProps: spec.SchemaProps{ - Description: "The URL that created the dashboard originally", - Type: []string{"string"}, - Format: "", - }, - }, - "timestamp": { - SchemaProps: spec.SchemaProps{ - Description: "Snapshot creation timestamp", - Type: []string{"string"}, - Format: "", - }, - }, - }, - }, - }, - } -} - -func schema_pkg_apis_dashboardsnapshot_v0alpha1_SnapshotSharingOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Description: "Each tenant, may have different sharing options This is currently set using custom.ini, but multi-tenant support will need to be managed differently", - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "snapshotEnabled": { - SchemaProps: spec.SchemaProps{ - Default: false, - Type: []string{"boolean"}, - Format: "", - }, - }, - "externalSnapshotURL": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "externalSnapshotName": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - "externalEnabled": { - SchemaProps: spec.SchemaProps{ - Type: []string{"boolean"}, - Format: "", - }, - }, - }, - Required: []string{"snapshotEnabled"}, - }, - }, - } -} diff --git a/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.openapi_violation_exceptions.list b/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.openapi_violation_exceptions.list deleted file mode 100644 index 58fc05ad930..00000000000 --- a/pkg/apis/dashboardsnapshot/v0alpha1/zz_generated.openapi_violation_exceptions.list +++ /dev/null @@ -1,3 +0,0 @@ -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1,DashboardCreateResponse,DeleteURL -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1,SnapshotInfo,ExternalURL -API rule violation: names_match,github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1,SnapshotSharingOptions,SnapshotsEnabled diff --git a/pkg/registry/apis/apis.go b/pkg/registry/apis/apis.go index 10f1d7e52be..7f01a25e500 100644 --- a/pkg/registry/apis/apis.go +++ b/pkg/registry/apis/apis.go @@ -3,7 +3,6 @@ package apiregistry import ( "github.com/grafana/grafana/pkg/registry/apis/collections" dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard" - "github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot" "github.com/grafana/grafana/pkg/registry/apis/datasource" "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/registry/apis/iam" @@ -21,7 +20,6 @@ type Service struct{} // and give each builder the chance to register itself with the main server func ProvideRegistryServiceSink( _ *dashboardinternal.DashboardsAPIBuilder, - _ *dashboardsnapshot.SnapshotsAPIBuilder, _ *datasource.DataSourceAPIBuilder, _ *folders.FolderAPIBuilder, _ *iam.IdentityAccessManagementAPIBuilder, diff --git a/pkg/registry/apis/dashboard/mutate.go b/pkg/registry/apis/dashboard/mutate.go index c193c17cf56..15cb481896f 100644 --- a/pkg/registry/apis/dashboard/mutate.go +++ b/pkg/registry/apis/dashboard/mutate.go @@ -33,6 +33,8 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute case dashboardV0.LIBRARY_PANEL_RESOURCE: return nil // nothing needed + case dashboardV0.SNAPSHOT_RESOURCE: + return nil } return fmt.Errorf("unexpected resource: %+v", a.GetResource()) diff --git a/pkg/registry/apis/dashboard/register.go b/pkg/registry/apis/dashboard/register.go index ad598f62f9f..d98407a1f57 100644 --- a/pkg/registry/apis/dashboard/register.go +++ b/pkg/registry/apis/dashboard/register.go @@ -40,6 +40,7 @@ import ( "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacysearcher" + "github.com/grafana/grafana/pkg/registry/apis/dashboard/snapshot" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver" grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" @@ -48,6 +49,7 @@ import ( "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboards" dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service" + "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/libraryelements" @@ -114,8 +116,10 @@ type DashboardsAPIBuilder struct { folderClientProvider client.K8sHandlerProvider libraryPanels libraryelements.Service // for legacy library panels publicDashboardService publicdashboards.Service - - isStandalone bool // skips any handling including anything to do with legacy storage + snapshotService dashboardsnapshots.Service + snapshotOptions dashv0.SnapshotSharingOptions + namespacer request.NamespaceMapper + isStandalone bool // skips any handling including anything to do with legacy storage } func RegisterAPIService( @@ -143,12 +147,20 @@ func RegisterAPIService( userService user.Service, libraryPanels libraryelements.Service, publicDashboardService publicdashboards.Service, + snapshotService dashboardsnapshots.Service, ) *DashboardsAPIBuilder { dbp := legacysql.NewDatabaseProvider(sql) namespacer := request.GetNamespaceMapper(cfg) legacyDashboardSearcher := legacysearcher.NewDashboardSearchClient(dashStore, sorter) folderClient := client.NewK8sHandler(dual, request.GetNamespaceMapper(cfg), folders.FolderResourceInfo.GroupVersionResource(), restConfigProvider.GetRestConfig, dashStore, userService, unified, sorter, features) + snapshotOptions := dashv0.SnapshotSharingOptions{ + SnapshotsEnabled: cfg.SnapshotEnabled, + ExternalSnapshotURL: cfg.ExternalSnapshotUrl, + ExternalSnapshotName: cfg.ExternalSnapshotName, + ExternalEnabled: cfg.ExternalEnabled, + } + builder := &DashboardsAPIBuilder{ dashboardService: dashboardService, dashboardPermissions: dashboardPermissions, @@ -167,7 +179,9 @@ func RegisterAPIService( folderClientProvider: newSimpleFolderClientProvider(folderClient), libraryPanels: libraryPanels, publicDashboardService: publicDashboardService, - + snapshotService: snapshotService, + snapshotOptions: snapshotOptions, + namespacer: namespacer, legacy: &DashboardStorage{ Access: legacy.NewDashboardSQLAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features), DashboardService: dashboardService, @@ -244,6 +258,7 @@ func (b *DashboardsAPIBuilder) AllowedV0Alpha1Resources() []string { return []string{ dashv0.DashboardKind().Plural(), dashv0.LIBRARY_PANEL_RESOURCE, + dashv0.SNAPSHOT_RESOURCE, } } @@ -267,6 +282,8 @@ func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attribu case dashv0.LIBRARY_PANEL_RESOURCE: return nil // OK for now + case dashv0.SNAPSHOT_RESOURCE: + return nil // OK for now } return fmt.Errorf("unsupported validation: %+v", a.GetResource()) @@ -535,6 +552,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver if err := b.storageForVersion(apiGroupInfo, opts, largeObjects, dashv0.DashboardResourceInfo, &dashv0.LibraryPanelResourceInfo, + &dashv0.SnapshotResourceInfo, func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) { dto := &dashv0.DashboardWithAccessInfo{} dash, ok := obj.(*dashv0.Dashboard) @@ -553,6 +571,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver if err := b.storageForVersion(apiGroupInfo, opts, largeObjects, dashv1.DashboardResourceInfo, nil, // do not register library panel + nil, func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) { dto := &dashv1.DashboardWithAccessInfo{} dash, ok := obj.(*dashv1.Dashboard) @@ -571,6 +590,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver if err := b.storageForVersion(apiGroupInfo, opts, largeObjects, dashv2alpha1.DashboardResourceInfo, nil, // do not register library panel + nil, func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) { dto := &dashv2alpha1.DashboardWithAccessInfo{} dash, ok := obj.(*dashv2alpha1.Dashboard) @@ -588,6 +608,7 @@ func (b *DashboardsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver if err := b.storageForVersion(apiGroupInfo, opts, largeObjects, dashv2beta1.DashboardResourceInfo, nil, // do not register library panel + nil, func(obj runtime.Object, access *internal.DashboardAccess) (v runtime.Object, err error) { dto := &dashv2beta1.DashboardWithAccessInfo{} dash, ok := obj.(*dashv2beta1.Dashboard) @@ -611,6 +632,7 @@ func (b *DashboardsAPIBuilder) storageForVersion( largeObjects apistore.LargeObjectSupport, dashboards utils.ResourceInfo, libraryPanels *utils.ResourceInfo, + snapshots *utils.ResourceInfo, newDTOFunc dtoBuilder, ) error { // Register the versioned storage @@ -685,6 +707,20 @@ func (b *DashboardsAPIBuilder) storageForVersion( } } + // Legacy only (for now) and only v0alpha1 + if snapshots != nil && dashboards.GroupVersion().Version == "v0alpha1" { + snapshotLegacyStore := &snapshot.SnapshotLegacyStore{ + ResourceInfo: *snapshots, + Service: b.snapshotService, + Namespacer: b.namespacer, + Options: b.snapshotOptions, + } + storage[snapshots.StoragePath()] = snapshotLegacyStore + storage[snapshots.StoragePath("dashboard")], err = snapshot.NewDashboardREST(dashboards, b.snapshotService) + if err != nil { + return err + } + } return nil } @@ -782,7 +818,12 @@ func (b *DashboardsAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.API } defs := b.GetOpenAPIDefinitions()(func(path string) spec.Ref { return spec.Ref{} }) - return b.search.GetAPIRoutes(defs) + searchAPIRoutes := b.search.GetAPIRoutes(defs) + snapshotAPIRoutes := snapshot.GetRoutes(b.snapshotService, b.snapshotOptions, defs) + + return &builder.APIRoutes{ + Namespace: append(searchAPIRoutes.Namespace, snapshotAPIRoutes.Namespace...), + } } // The default authorizer is fine because authorization happens in storage where we know the parent folder diff --git a/pkg/registry/apis/dashboardsnapshot/conversions.go b/pkg/registry/apis/dashboard/snapshot/conversions.go similarity index 60% rename from pkg/registry/apis/dashboardsnapshot/conversions.go rename to pkg/registry/apis/dashboard/snapshot/conversions.go index 09c2d283ab5..b9f62f2a438 100644 --- a/pkg/registry/apis/dashboardsnapshot/conversions.go +++ b/pkg/registry/apis/dashboard/snapshot/conversions.go @@ -1,4 +1,4 @@ -package dashboardsnapshot +package snapshot import ( "fmt" @@ -6,31 +6,38 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + dashV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/utils" - dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" ) -func convertDTOToSnapshot(v *dashboardsnapshots.DashboardSnapshotDTO, namespacer request.NamespaceMapper) *dashboardsnapshot.DashboardSnapshot { +func convertSnapshotDTOToK8sResource(v *dashboardsnapshots.DashboardSnapshotDTO, namespacer request.NamespaceMapper) *dashV0.Snapshot { expires := v.Expires.UnixMilli() if v.Expires.After(time.Date(2070, time.January, 0, 0, 0, 0, 0, time.UTC)) { expires = 0 // ignore things expiring long into the future } - snap := &dashboardsnapshot.DashboardSnapshot{ - TypeMeta: resourceInfo.TypeMeta(), + snap := &dashV0.Snapshot{ + TypeMeta: dashV0.SnapshotResourceInfo.TypeMeta(), ObjectMeta: metav1.ObjectMeta{ Name: v.Key, ResourceVersion: fmt.Sprintf("%d", v.Updated.UnixMilli()), CreationTimestamp: metav1.NewTime(v.Created), Namespace: namespacer(v.OrgID), }, - Spec: dashboardsnapshot.SnapshotInfo{ - Title: v.Name, - ExternalURL: v.ExternalURL, - Expires: expires, + Spec: dashV0.SnapshotSpec{ + Title: &v.Name, }, } + + // Only show external settings when it is external + if v.External { + snap.Spec.External = &v.External + snap.Spec.ExternalUrl = &v.ExternalURL + } + if expires > 0 { + snap.Spec.Expires = &expires + } if v.Updated != v.Created { meta, _ := utils.MetaAccessor(snap) meta.SetUpdatedTimestamp(&v.Updated) @@ -38,31 +45,33 @@ func convertDTOToSnapshot(v *dashboardsnapshots.DashboardSnapshotDTO, namespacer return snap } -func convertSnapshotToK8sResource(v *dashboardsnapshots.DashboardSnapshot, namespacer request.NamespaceMapper) *dashboardsnapshot.DashboardSnapshot { +func convertSnapshotToK8sResource(v *dashboardsnapshots.DashboardSnapshot, namespacer request.NamespaceMapper) *dashV0.Snapshot { expires := v.Expires.UnixMilli() if v.Expires.After(time.Date(2070, time.January, 0, 0, 0, 0, 0, time.UTC)) { expires = 0 // ignore things expiring long into the future } - info := dashboardsnapshot.SnapshotInfo{ - Title: v.Name, - ExternalURL: v.ExternalURL, - Expires: expires, - } - s := v.Dashboard.Get("snapshot") - if s != nil { - info.OriginalUrl, _ = s.Get("originalUrl").String() - info.Timestamp, _ = s.Get("timestamp").String() - } - snap := &dashboardsnapshot.DashboardSnapshot{ + snap := &dashV0.Snapshot{ ObjectMeta: metav1.ObjectMeta{ Name: v.Key, ResourceVersion: fmt.Sprintf("%d", v.Updated.UnixMilli()), CreationTimestamp: metav1.NewTime(v.Created), Namespace: namespacer(v.OrgID), }, - Spec: info, + Spec: dashV0.SnapshotSpec{ + Title: &v.Name, + }, } + + // Only show external settings when it is external + if v.External { + snap.Spec.External = &v.External + snap.Spec.ExternalUrl = &v.ExternalURL + } + if expires > 0 { + snap.Spec.Expires = &expires + } + if v.Updated != v.Created { meta, _ := utils.MetaAccessor(snap) meta.SetUpdatedTimestamp(&v.Updated) diff --git a/pkg/registry/apis/dashboardsnapshot/register.go b/pkg/registry/apis/dashboard/snapshot/routes.go similarity index 51% rename from pkg/registry/apis/dashboardsnapshot/register.go rename to pkg/registry/apis/dashboard/snapshot/routes.go index 8f9661ae3fa..c8175d6d9dd 100644 --- a/pkg/registry/apis/dashboardsnapshot/register.go +++ b/pkg/registry/apis/dashboard/snapshot/routes.go @@ -1,168 +1,36 @@ -package dashboardsnapshot +package snapshot import ( - "context" "encoding/json" "fmt" "net/http" "github.com/gorilla/mux" - "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" - "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/registry/rest" - genericapiserver "k8s.io/apiserver/pkg/server" - common "k8s.io/kube-openapi/pkg/common" + "k8s.io/kube-openapi/pkg/common" "k8s.io/kube-openapi/pkg/spec3" "k8s.io/kube-openapi/pkg/validation/spec" - claims "github.com/grafana/authlib/types" + authlib "github.com/grafana/authlib/types" + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" - dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/builder" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errhttp" "github.com/grafana/grafana/pkg/web" ) -var ( - _ builder.APIGroupBuilder = (*SnapshotsAPIBuilder)(nil) - _ builder.OpenAPIPostProcessor = (*SnapshotsAPIBuilder)(nil) - _ builder.APIGroupRouteProvider = (*SnapshotsAPIBuilder)(nil) -) +func GetRoutes(service dashboardsnapshots.Service, options dashv0.SnapshotSharingOptions, defs map[string]common.OpenAPIDefinition) *builder.APIRoutes { + prefix := dashv0.SnapshotResourceInfo.GroupResource().Resource + tags := []string{dashv0.SnapshotResourceInfo.GroupVersionKind().Kind} -var resourceInfo = dashboardsnapshot.DashboardSnapshotResourceInfo - -// This is used just so wire has something unique to return -type SnapshotsAPIBuilder struct { - service dashboardsnapshots.Service - namespacer request.NamespaceMapper - options sharingOptionsGetter - exporter *dashExporter - logger log.Logger -} - -func NewSnapshotsAPIBuilder( - p dashboardsnapshots.Service, - cfg *setting.Cfg, - exporter *dashExporter, -) *SnapshotsAPIBuilder { - return &SnapshotsAPIBuilder{ - service: p, - options: newSharingOptionsGetter(cfg), - namespacer: request.GetNamespaceMapper(cfg), - exporter: exporter, - logger: log.New("snapshots::RawHandlers"), - } -} - -func RegisterAPIService( - service dashboardsnapshots.Service, - apiregistration builder.APIRegistrar, - cfg *setting.Cfg, - features featuremgmt.FeatureToggles, - sql db.DB, - reg prometheus.Registerer, -) *SnapshotsAPIBuilder { - //nolint:staticcheck // not yet migrated to OpenFeature - if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) { - return nil // skip registration unless opting into experimental apis - } - builder := NewSnapshotsAPIBuilder(service, cfg, &dashExporter{ - service: service, - sql: sql, - }) - apiregistration.RegisterAPI(builder) - return builder -} - -func (b *SnapshotsAPIBuilder) GetGroupVersion() schema.GroupVersion { - return resourceInfo.GroupVersion() -} - -func addKnownTypes(scheme *runtime.Scheme, gv schema.GroupVersion) { - scheme.AddKnownTypes(gv, - &dashboardsnapshot.DashboardSnapshot{}, - &dashboardsnapshot.DashboardSnapshotList{}, - &dashboardsnapshot.SharingOptions{}, - &dashboardsnapshot.SharingOptionsList{}, - &dashboardsnapshot.FullDashboardSnapshot{}, - &dashboardsnapshot.DashboardSnapshotWithDeleteKey{}, - &metav1.Status{}, - ) -} - -func (b *SnapshotsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { - gv := resourceInfo.GroupVersion() - addKnownTypes(scheme, gv) - - // Link this version to the internal representation. - // This is used for server-side-apply (PATCH), and avoids the error: - // "no kind is registered for the type" - addKnownTypes(scheme, schema.GroupVersion{ - Group: gv.Group, - Version: runtime.APIVersionInternal, - }) - - // If multiple versions exist, then register conversions from zz_generated.conversion.go - // if err := playlist.RegisterConversions(scheme); err != nil { - // return err - // } - metav1.AddToGroupVersion(scheme, gv) - return scheme.SetVersionPriority(gv) -} - -func (b *SnapshotsAPIBuilder) AllowedV0Alpha1Resources() []string { - return nil -} - -func (b *SnapshotsAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, _ builder.APIGroupOptions) error { - storage := map[string]rest.Storage{} - - legacyStore := &legacyStorage{ - service: b.service, - namespacer: b.namespacer, - options: b.options, - } - legacyStore.tableConverter = resourceInfo.TableConverter() - storage[resourceInfo.StoragePath()] = legacyStore - storage[resourceInfo.StoragePath("body")] = &subBodyREST{ - service: b.service, - namespacer: b.namespacer, - } - - storage["options"] = &optionsStorage{ - getter: b.options, - tableConverter: legacyStore.tableConverter, - } - - apiGroupInfo.VersionedResourcesStorageMap[dashboardsnapshot.VERSION] = storage - return nil -} - -func (b *SnapshotsAPIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions { - return dashboardsnapshot.GetOpenAPIDefinitions -} - -// Register additional routes with the server -func (b *SnapshotsAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIRoutes { - prefix := dashboardsnapshot.DashboardSnapshotResourceInfo.GroupResource().Resource - defs := dashboardsnapshot.GetOpenAPIDefinitions(func(path string) spec.Ref { return spec.Ref{} }) createCmd := defs["github.com/grafana/grafana/apps/dashboard/pkg/apissnapshot/v0alpha1.DashboardCreateCommand"].Schema createExample := `{"dashboard":{"annotations":{"list":[{"name":"Annotations & Alerts","enable":true,"iconColor":"rgba(0, 211, 255, 1)","snapshotData":[],"type":"dashboard","builtIn":1,"hide":true}]},"editable":true,"fiscalYearStartMonth":0,"graphTooltip":0,"id":203,"links":[],"liveNow":false,"panels":[{"datasource":null,"fieldConfig":{"defaults":{"color":{"mode":"palette-classic"},"custom":{"axisBorderShow":false,"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":43,"gradientMode":"opacity","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unitScale":true},"overrides":[]},"gridPos":{"h":8,"w":12,"x":0,"y":0},"id":1,"options":{"legend":{"calcs":[],"displayMode":"list","placement":"bottom","showLegend":true},"tooltip":{"mode":"single","sort":"none"}},"pluginVersion":"10.4.0-pre","snapshotData":[{"fields":[{"config":{"color":{"mode":"palette-classic"},"custom":{"axisBorderShow":false,"axisCenteredZero":false,"axisColorMode":"text","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":43,"gradientMode":"opacity","hideFrom":{"legend":false,"tooltip":false,"viz":false},"lineInterpolation":"smooth","lineWidth":1,"pointSize":5,"showPoints":"auto","thresholdsStyle":{"mode":"off"}},"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unitScale":true},"name":"time","type":"time","values":[1706030536378,1706034856378,1706039176378,1706043496378,1706047816378,1706052136378]},{"config":{"color":{"mode":"palette-classic"},"custom":{"axisBorderShow":false,"axisCenteredZero":false,"axisColorMode":"text","axisLabel":"","axisPlacement":"auto","barAlignment":0,"drawStyle":"line","fillOpacity":43,"gradientMode":"opacity","hideFrom":{"legend":false,"tooltip":false,"viz":false},"insertNulls":false,"lineInterpolation":"smooth","lineWidth":1,"pointSize":5,"scaleDistribution":{"type":"linear"},"showPoints":"auto","spanNulls":false,"stacking":{"group":"A","mode":"none"},"thresholdsStyle":{"mode":"off"}},"mappings":[],"thresholds":{"mode":"absolute","steps":[{"color":"green","value":null},{"color":"red","value":80}]},"unitScale":true},"name":"A-series","type":"number","values":[1,20,90,30,50,0]}],"refId":"A"}],"targets":[],"title":"Simple example","type":"timeseries","links":[]}],"refresh":"","schemaVersion":39,"snapshot":{"timestamp":"2024-01-23T23:22:16.377Z"},"tags":[],"templating":{"list":[]},"time":{"from":"2024-01-23T17:22:20.380Z","to":"2024-01-23T23:22:20.380Z","raw":{"from":"now-6h","to":"now"}},"timepicker":{},"timezone":"","title":"simple and small","uid":"b22ec8db-399b-403b-b6c7-b0fb30ccb2a5","version":1,"weekStart":""},"name":"simple and small","expires":86400}` createRsp := defs["github.com/grafana/grafana/apps/dashboard/pkg/apissnapshot/v0alpha1.DashboardCreateResponse"].Schema - tags := []string{dashboardsnapshot.DashboardSnapshotResourceInfo.GroupVersionKind().Kind} - routes := &builder.APIRoutes{ + return &builder.APIRoutes{ Namespace: []builder.APIRouteHandler{ { Path: prefix + "/create", @@ -172,16 +40,16 @@ func (b *SnapshotsAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIR Extensions: map[string]any{ "x-grafana-action": "create", "x-kubernetes-group-version-kind": metav1.GroupVersionKind{ - Group: dashboardsnapshot.GROUP, - Version: dashboardsnapshot.VERSION, + Group: dashv0.GROUP, + Version: dashv0.VERSION, Kind: "DashboardCreateResponse", }, }, }, OperationProps: spec3.OperationProps{ Tags: tags, - Summary: "Full dashboard", - Description: "longer description here?", + OperationId: "createSnapshot", + Description: "Creates a new Snapshot", Parameters: []*spec3.Parameter{ { ParameterProps: spec3.ParameterProps{ @@ -233,7 +101,6 @@ func (b *SnapshotsAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIR return } wrap := &contextmodel.ReqContext{ - Logger: b.logger, Context: &web.Context{ Req: r, Resp: web.NewResponseWriter(r.Method, w), @@ -242,7 +109,7 @@ func (b *SnapshotsAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIR } vars := mux.Vars(r) - info, err := claims.ParseNamespace(vars["namespace"]) + info, err := authlib.ParseNamespace(vars["namespace"]) if err != nil { wrap.JsonApiErr(http.StatusBadRequest, "expected namespace", nil) return @@ -259,24 +126,18 @@ func (b *SnapshotsAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIR return } - opts, err := b.options(info.Value) - if err != nil { - wrap.JsonApiErr(http.StatusBadRequest, "error getting options", err) - return - } - // Use the existing snapshot service - dashboardsnapshots.CreateDashboardSnapshot(wrap, opts.Spec, cmd, b.service) + dashboardsnapshots.CreateDashboardSnapshot(wrap, options, cmd, service) }, }, { Path: prefix + "/delete/{deleteKey}", Spec: &spec3.PathProps{ - Summary: "an example at the root level", - Description: "longer description here?", + Description: "Delete snapshot by delete key", Delete: &spec3.Operation{ OperationProps: spec3.OperationProps{ - Tags: tags, + Tags: tags, + OperationId: "deleteWithKey", Parameters: []*spec3.Parameter{ { ParameterProps: spec3.ParameterProps{ @@ -296,7 +157,7 @@ func (b *SnapshotsAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIR vars := mux.Vars(r) key := vars["deleteKey"] - err := dashboardsnapshots.DeleteWithKey(ctx, key, b.service) + err := dashboardsnapshots.DeleteWithKey(ctx, key, service) if err != nil { errhttp.Write(ctx, fmt.Errorf("failed to delete external dashboard (%w)", err), w) return @@ -306,54 +167,5 @@ func (b *SnapshotsAPIBuilder) GetAPIRoutes(gv schema.GroupVersion) *builder.APIR }) }, }, - }, - } - - // dev environment to export all snapshots to a blob store - if b.exporter != nil && false { - routes.Root = append(routes.Root, b.exporter.getAPIRouteHandler()) - } - return routes -} - -func (b *SnapshotsAPIBuilder) GetAuthorizer() authorizer.Authorizer { - // TODO: this behavior must match the existing logic (it is currently more restrictive) - // - // https://github.com/grafana/grafana/blob/f63e43c113ac0cf8f78ed96ee2953874139bd2dc/pkg/middleware/auth.go#L203 - // func SnapshotPublicModeOrSignedIn(cfg *setting.Cfg) web.Handler { - // return func(c *contextmodel.ReqContext) { - // if cfg.SnapshotPublicMode { - // return - // } - - // if !c.IsSignedIn { - // notAuthorized(c) - // return - // } - // } - // } - - return authorizer.AuthorizerFunc( - func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { - // Everyone can view dashsnaps - if attr.GetVerb() == "get" && attr.GetResource() == dashboardsnapshot.DashboardSnapshotResourceInfo.GroupResource().Resource { - return authorizer.DecisionAllow, "", err - } - - // Fallback to the default behaviors (namespace matches org) - return authorizer.DecisionNoOpinion, "", err - }) -} - -func (b *SnapshotsAPIBuilder) PostProcessOpenAPI(oas *spec3.OpenAPI) (*spec3.OpenAPI, error) { - oas.Info.Description = "A dashboard snapshot shares an interactive dashboard publicly." - - // Set a description on the - sub := oas.Paths.Paths["/apis/dashboardsnapshot.grafana.app/v0alpha1/namespaces/{namespace}/dashboardsnapshots/{name}/body"] - if sub != nil && sub.Get != nil { - sub.Get.Summary = "Full dashboard" - sub.Get.Description = "Read the full dashboard body" - } - - return oas, nil + }} } diff --git a/pkg/registry/apis/dashboard/snapshot/snapshot_legacy_store.go b/pkg/registry/apis/dashboard/snapshot/snapshot_legacy_store.go new file mode 100644 index 00000000000..aafbc2b283d --- /dev/null +++ b/pkg/registry/apis/dashboard/snapshot/snapshot_legacy_store.go @@ -0,0 +1,149 @@ +package snapshot + +import ( + "context" + "fmt" + + "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/rest" + + dashV0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/identity" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/dashboardsnapshots" +) + +var ( + _ rest.Scoper = (*SnapshotLegacyStore)(nil) + _ rest.SingularNameProvider = (*SnapshotLegacyStore)(nil) + _ rest.Getter = (*SnapshotLegacyStore)(nil) + _ rest.Lister = (*SnapshotLegacyStore)(nil) + _ rest.GracefulDeleter = (*SnapshotLegacyStore)(nil) + _ rest.Storage = (*SnapshotLegacyStore)(nil) +) + +type SnapshotLegacyStore struct { + ResourceInfo utils.ResourceInfo + Service dashboardsnapshots.Service + Namespacer request.NamespaceMapper + Options dashV0.SnapshotSharingOptions +} + +func (s *SnapshotLegacyStore) New() runtime.Object { + return s.ResourceInfo.NewFunc() +} + +func (s *SnapshotLegacyStore) Destroy() {} + +func (s *SnapshotLegacyStore) NamespaceScoped() bool { + return true // namespace == org +} + +func (s *SnapshotLegacyStore) GetSingularName() string { + return s.ResourceInfo.GetSingularName() +} + +func (s *SnapshotLegacyStore) NewList() runtime.Object { + return s.ResourceInfo.NewListFunc() +} + +func (s *SnapshotLegacyStore) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return s.ResourceInfo.TableConverter().ConvertToTable(ctx, object, tableOptions) +} + +// GracefulDeleter +func (s *SnapshotLegacyStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + snap, err := s.Service.GetDashboardSnapshot(ctx, &dashboardsnapshots.GetDashboardSnapshotQuery{ + Key: name, + }) + if err != nil || snap == nil { + return nil, false, err + } + + // Delete the external one first + if snap.ExternalDeleteURL != "" { + err := dashboardsnapshots.DeleteExternalDashboardSnapshot(snap.ExternalDeleteURL) + if err != nil { + return nil, false, err + } + } + + err = s.Service.DeleteDashboardSnapshot(ctx, &dashboardsnapshots.DeleteDashboardSnapshotCommand{ + DeleteKey: snap.DeleteKey, + }) + if err != nil { + return nil, false, err + } + return nil, true, nil +} + +func (s *SnapshotLegacyStore) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { + orgId, err := request.OrgIDForList(ctx) + if err != nil { + return nil, err + } + + requester, err := identity.GetRequester(ctx) + if err != nil { + return nil, err + } + + limit := options.Limit + if limit < 1 { + limit = 1000 + } + + searchQuery := dashboardsnapshots.GetDashboardSnapshotsQuery{ + Name: "", // TODO: Should we support searching by name? In the levacy api is a string query param called query + Limit: int(limit), + OrgID: orgId, + SignedInUser: requester, + } + + res, err := s.Service.SearchDashboardSnapshots(ctx, &searchQuery) + if err != nil { + return nil, err + } + + list := &dashV0.SnapshotList{} + //convert + for idx := range res { + list.Items = append(list.Items, *convertSnapshotDTOToK8sResource(res[idx], s.Namespacer)) + } + return list, nil +} + +func (s *SnapshotLegacyStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + info, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + + err = s.checkEnabled(info.Value) + if err != nil { + return nil, err + } + query := dashboardsnapshots.GetDashboardSnapshotQuery{ + Key: name, + } + + res, err := s.Service.GetDashboardSnapshot(ctx, &query) + if err != nil { + return nil, err + } + + if res != nil { + return convertSnapshotToK8sResource(res, s.Namespacer), nil + } + return nil, s.ResourceInfo.NewNotFound(name) +} + +func (s *SnapshotLegacyStore) checkEnabled(ns string) error { + if !s.Options.SnapshotsEnabled { + return fmt.Errorf("snapshots not enabled") + } + return nil +} diff --git a/pkg/registry/apis/dashboard/snapshot/sub_dashboard.go b/pkg/registry/apis/dashboard/snapshot/sub_dashboard.go new file mode 100644 index 00000000000..b43159939a4 --- /dev/null +++ b/pkg/registry/apis/dashboard/snapshot/sub_dashboard.go @@ -0,0 +1,81 @@ +package snapshot + +import ( + "context" + "net/http" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/rest" + + dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" + "github.com/grafana/grafana/pkg/apimachinery/utils" + "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" + "github.com/grafana/grafana/pkg/services/dashboardsnapshots" +) + +// Currently only works with v0alpha1 +type dashboardREST struct { + Service dashboardsnapshots.Service +} + +func NewDashboardREST( + resourceInfo utils.ResourceInfo, + service dashboardsnapshots.Service, +) (rest.Storage, error) { + return &dashboardREST{ + Service: service, + }, nil +} + +var ( + _ rest.Connecter = (*dashboardREST)(nil) + _ rest.StorageMetadata = (*dashboardREST)(nil) +) + +func (r *dashboardREST) New() runtime.Object { + return &dashv0.Dashboard{} +} + +func (r *dashboardREST) Destroy() { +} + +func (r *dashboardREST) ConnectMethods() []string { + return []string{"GET"} +} + +func (r *dashboardREST) NewConnectOptions() (runtime.Object, bool, string) { + return nil, false, "" +} + +func (r *dashboardREST) ProducesMIMETypes(verb string) []string { + return nil +} + +func (r *dashboardREST) ProducesObject(verb string) interface{} { + return r.New() +} + +func (r *dashboardREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { + _, err := request.NamespaceInfoFrom(ctx, true) + if err != nil { + return nil, err + } + snap, err := r.Service.GetDashboardSnapshot(ctx, &dashboardsnapshots.GetDashboardSnapshotQuery{Key: name}) + if err != nil { + return nil, err + } + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // TODO... support conversions (not required in v0) + dash := &dashv0.Dashboard{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: name, + }, + Spec: v0alpha1.Unstructured{ + Object: snap.Dashboard.MustMap(), + }, + } + responder.Object(200, dash) + }), nil +} diff --git a/pkg/registry/apis/dashboardsnapshot/exporter.go b/pkg/registry/apis/dashboardsnapshot/exporter.go index fdf8caab9e1..6094406c3e0 100644 --- a/pkg/registry/apis/dashboardsnapshot/exporter.go +++ b/pkg/registry/apis/dashboardsnapshot/exporter.go @@ -1,130 +1,131 @@ package dashboardsnapshot -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "time" - - "gocloud.dev/blob" - "k8s.io/kube-openapi/pkg/spec3" - - "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/apiserver/builder" - "github.com/grafana/grafana/pkg/services/dashboardsnapshots" -) - -type dashExportStatus struct { - Count int - Index int - Started int64 - Updated int64 - Finished int64 - Error string -} - -type dashExporter struct { - status dashExportStatus - - service dashboardsnapshots.Service - sql db.DB -} - -func (d *dashExporter) getAPIRouteHandler() builder.APIRouteHandler { - return builder.APIRouteHandler{ - Path: "admin/export", - Spec: &spec3.PathProps{ - Summary: "an example at the root level", - Description: "longer description here?", - Post: &spec3.Operation{ - OperationProps: spec3.OperationProps{ - Tags: []string{"export"}, - Responses: &spec3.Responses{ - ResponsesProps: spec3.ResponsesProps{ - StatusCodeResponses: map[int]*spec3.Response{ - 200: { - ResponseProps: spec3.ResponseProps{ - Content: map[string]*spec3.MediaType{ - "application/json": {}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - Handler: func(w http.ResponseWriter, r *http.Request) { - // Only let it start once - if d.status.Started == 0 { - go d.doExport() - } - time.Sleep(time.Second) - _ = json.NewEncoder(w).Encode(d.status) - }, - } -} - -// NO way to stop!!!!!! -func (d *dashExporter) doExport() { - defer func() { - d.status.Finished = time.Now().UnixMilli() - }() - d.status = dashExportStatus{ - Started: time.Now().UnixMilli(), - } - if d.sql == nil { - d.status.Error = "missing dependencies" - return - } - - ctx := context.Background() - keys := []string{} - err := d.sql.GetSqlxSession().Select(ctx, - &keys, "SELECT key FROM dashboard_snapshot ORDER BY id asc") - if err != nil { - d.status.Error = err.Error() - return - } - d.status.Count = len(keys) - - bucket, err := blob.OpenBucket(ctx, "mem://?key=foo.txt&prefix=a/subfolder/") - if err != nil { - d.status.Error = err.Error() - return - } - defer func() { - _ = bucket.Close() - }() - - for idx, key := range keys { - d.status.Index = idx - snap, err := d.service.GetDashboardSnapshot(ctx, &dashboardsnapshots.GetDashboardSnapshotQuery{ - Key: key, - }) - if err != nil { - d.status.Error = err.Error() - return - } - - dash, err := snap.Dashboard.ToDB() - if err != nil { - d.status.Error = err.Error() - return - } - - fmt.Printf("TODO, export: %s (len: %d)\n", snap.Key, len(dash)) - - // w, err := bucket.NewWriter(ctx, "foo.txt", nil) - // if err != nil { - // d.status.Error = err.Error() - // return - // } - - time.Sleep(time.Second * 1) - d.status.Updated = time.Now().UnixMilli() - } - fmt.Printf("done!\n") -} +// +//import ( +// "context" +// "encoding/json" +// "fmt" +// "net/http" +// "time" +// +// "gocloud.dev/blob" +// "k8s.io/kube-openapi/pkg/spec3" +// +// "github.com/grafana/grafana/pkg/infra/db" +// "github.com/grafana/grafana/pkg/services/apiserver/builder" +// "github.com/grafana/grafana/pkg/services/dashboardsnapshots" +//) +// +//type dashExportStatus struct { +// Count int +// Index int +// Started int64 +// Updated int64 +// Finished int64 +// Error string +//} +// +//type dashExporter struct { +// status dashExportStatus +// +// service dashboardsnapshots.Service +// sql db.DB +//} +// +//func (d *dashExporter) getAPIRouteHandler() builder.APIRouteHandler { +// return builder.APIRouteHandler{ +// Path: "admin/export", +// Spec: &spec3.PathProps{ +// Summary: "an example at the root level", +// Description: "longer description here?", +// Post: &spec3.Operation{ +// OperationProps: spec3.OperationProps{ +// Tags: []string{"export"}, +// Responses: &spec3.Responses{ +// ResponsesProps: spec3.ResponsesProps{ +// StatusCodeResponses: map[int]*spec3.Response{ +// 200: { +// ResponseProps: spec3.ResponseProps{ +// Content: map[string]*spec3.MediaType{ +// "application/json": {}, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// }, +// Handler: func(w http.ResponseWriter, r *http.Request) { +// // Only let it start once +// if d.status.Started == 0 { +// go d.doExport() +// } +// time.Sleep(time.Second) +// _ = json.NewEncoder(w).Encode(d.status) +// }, +// } +//} +// +//// NO way to stop!!!!!! +//func (d *dashExporter) doExport() { +// defer func() { +// d.status.Finished = time.Now().UnixMilli() +// }() +// d.status = dashExportStatus{ +// Started: time.Now().UnixMilli(), +// } +// if d.sql == nil { +// d.status.Error = "missing dependencies" +// return +// } +// +// ctx := context.Background() +// keys := []string{} +// err := d.sql.GetSqlxSession().Select(ctx, +// &keys, "SELECT key FROM dashboard_snapshot ORDER BY id asc") +// if err != nil { +// d.status.Error = err.Error() +// return +// } +// d.status.Count = len(keys) +// +// bucket, err := blob.OpenBucket(ctx, "mem://?key=foo.txt&prefix=a/subfolder/") +// if err != nil { +// d.status.Error = err.Error() +// return +// } +// defer func() { +// _ = bucket.Close() +// }() +// +// for idx, key := range keys { +// d.status.Index = idx +// snap, err := d.service.GetDashboardSnapshot(ctx, &dashboardsnapshots.GetDashboardSnapshotQuery{ +// Key: key, +// }) +// if err != nil { +// d.status.Error = err.Error() +// return +// } +// +// dash, err := snap.Dashboard.ToDB() +// if err != nil { +// d.status.Error = err.Error() +// return +// } +// +// fmt.Printf("TODO, export: %s (len: %d)\n", snap.Key, len(dash)) +// +// // w, err := bucket.NewWriter(ctx, "foo.txt", nil) +// // if err != nil { +// // d.status.Error = err.Error() +// // return +// // } +// +// time.Sleep(time.Second * 1) +// d.status.Updated = time.Now().UnixMilli() +// } +// fmt.Printf("done!\n") +//} diff --git a/pkg/registry/apis/dashboardsnapshot/options_storage.go b/pkg/registry/apis/dashboardsnapshot/options_storage.go deleted file mode 100644 index 571010b9f5c..00000000000 --- a/pkg/registry/apis/dashboardsnapshot/options_storage.go +++ /dev/null @@ -1,91 +0,0 @@ -package dashboardsnapshot - -import ( - "context" - "fmt" - - "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/rest" - - dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/setting" -) - -var ( - _ rest.Scoper = (*optionsStorage)(nil) - _ rest.SingularNameProvider = (*optionsStorage)(nil) - _ rest.Getter = (*optionsStorage)(nil) - _ rest.Lister = (*optionsStorage)(nil) - _ rest.Storage = (*optionsStorage)(nil) -) - -type sharingOptionsGetter = func(namespace string) (*dashboardsnapshot.SharingOptions, error) - -func newSharingOptionsGetter(cfg *setting.Cfg) sharingOptionsGetter { - s := &dashboardsnapshot.SharingOptions{ - ObjectMeta: metav1.ObjectMeta{ - CreationTimestamp: metav1.Now(), - }, - Spec: dashboardsnapshot.SnapshotSharingOptions{ - SnapshotsEnabled: cfg.SnapshotEnabled, - ExternalSnapshotURL: cfg.ExternalSnapshotUrl, - ExternalSnapshotName: cfg.ExternalSnapshotName, - ExternalEnabled: cfg.ExternalEnabled, - }, - } - return func(namespace string) (*dashboardsnapshot.SharingOptions, error) { - return s, nil - } -} - -type optionsStorage struct { - getter sharingOptionsGetter - tableConverter rest.TableConvertor -} - -func (s *optionsStorage) New() runtime.Object { - return &dashboardsnapshot.SharingOptions{} -} - -func (s *optionsStorage) Destroy() {} - -func (s *optionsStorage) NamespaceScoped() bool { - return true -} - -func (s *optionsStorage) GetSingularName() string { - return "options" -} - -func (s *optionsStorage) NewList() runtime.Object { - return &dashboardsnapshot.SharingOptionsList{} -} - -func (s *optionsStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { - return s.tableConverter.ConvertToTable(ctx, object, tableOptions) -} - -func (s *optionsStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err != nil { - return nil, err - } - if info.OrgID < 0 { - return nil, fmt.Errorf("missing namespace") - } - v, err := s.getter(info.Value) - if err != nil { - return nil, err - } - list := &dashboardsnapshot.SharingOptionsList{ - Items: []dashboardsnapshot.SharingOptions{*v}, - } - return list, nil -} - -func (s *optionsStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - return s.getter(name) -} diff --git a/pkg/registry/apis/dashboardsnapshot/sql_storage.go b/pkg/registry/apis/dashboardsnapshot/sql_storage.go deleted file mode 100644 index 26fdb8fff57..00000000000 --- a/pkg/registry/apis/dashboardsnapshot/sql_storage.go +++ /dev/null @@ -1,147 +0,0 @@ -package dashboardsnapshot - -import ( - "context" - "fmt" - - "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/rest" - - "github.com/grafana/grafana/pkg/apimachinery/identity" - dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/dashboardsnapshots" -) - -var ( - _ rest.Scoper = (*legacyStorage)(nil) - _ rest.SingularNameProvider = (*legacyStorage)(nil) - _ rest.Getter = (*legacyStorage)(nil) - _ rest.Lister = (*legacyStorage)(nil) - _ rest.Storage = (*legacyStorage)(nil) - _ rest.GracefulDeleter = (*legacyStorage)(nil) -) - -type legacyStorage struct { - service dashboardsnapshots.Service - namespacer request.NamespaceMapper - tableConverter rest.TableConvertor - options sharingOptionsGetter -} - -func (s *legacyStorage) New() runtime.Object { - return resourceInfo.NewFunc() -} - -func (s *legacyStorage) Destroy() {} - -func (s *legacyStorage) NamespaceScoped() bool { - return true // namespace == org -} - -func (s *legacyStorage) GetSingularName() string { - return resourceInfo.GetSingularName() -} - -func (s *legacyStorage) NewList() runtime.Object { - return resourceInfo.NewListFunc() -} - -func (s *legacyStorage) checkEnabled(ns string) error { - opts, err := s.options(ns) - if err != nil { - return err - } - if !opts.Spec.SnapshotsEnabled { - return fmt.Errorf("snapshots not enabled") - } - return nil -} - -func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { - return s.tableConverter.ConvertToTable(ctx, object, tableOptions) -} - -func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err == nil { - err = s.checkEnabled(info.Value) - } - if err != nil { - return nil, err - } - - user, err := identity.GetRequester(ctx) - if err != nil { - return nil, err - } - - limit := 5000 - if options.Limit > 0 { - limit = int(options.Limit) - } - res, err := s.service.SearchDashboardSnapshots(ctx, &dashboardsnapshots.GetDashboardSnapshotsQuery{ - OrgID: info.OrgID, - SignedInUser: user, - Limit: limit, - }) - if err != nil { - return nil, err - } - - list := &dashboardsnapshot.DashboardSnapshotList{} - for _, v := range res { - list.Items = append(list.Items, *convertDTOToSnapshot(v, s.namespacer)) - } - return list, nil -} - -func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - info, err := request.NamespaceInfoFrom(ctx, true) - if err == nil { - err = s.checkEnabled(info.Value) - } - if err != nil { - return nil, err - } - - v, err := s.service.GetDashboardSnapshot(ctx, &dashboardsnapshots.GetDashboardSnapshotQuery{ - Key: name, - }) - if err != nil || v == nil { - // if errors.Is(err, playlistsvc.ErrPlaylistNotFound) || err == nil { - // err = k8serrors.NewNotFound(s.SingularQualifiedResource, name) - // } - return nil, err - } - - return convertSnapshotToK8sResource(v, s.namespacer), nil -} - -// GracefulDeleter -func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { - snap, err := s.service.GetDashboardSnapshot(ctx, &dashboardsnapshots.GetDashboardSnapshotQuery{ - Key: name, - }) - if err != nil || snap == nil { - return nil, false, err - } - - // Delete the external one first - if snap.ExternalDeleteURL != "" { - err := dashboardsnapshots.DeleteExternalDashboardSnapshot(snap.ExternalDeleteURL) - if err != nil { - return nil, false, err - } - } - - err = s.service.DeleteDashboardSnapshot(ctx, &dashboardsnapshots.DeleteDashboardSnapshotCommand{ - DeleteKey: snap.DeleteKey, - }) - if err != nil { - return nil, false, err - } - return nil, true, nil -} diff --git a/pkg/registry/apis/dashboardsnapshot/sub_body.go b/pkg/registry/apis/dashboardsnapshot/sub_body.go deleted file mode 100644 index 9d0a7349e43..00000000000 --- a/pkg/registry/apis/dashboardsnapshot/sub_body.go +++ /dev/null @@ -1,60 +0,0 @@ -package dashboardsnapshot - -import ( - "context" - "net/http" - - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/registry/rest" - - common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" - "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" - "github.com/grafana/grafana/pkg/services/dashboardsnapshots" -) - -type subBodyREST struct { - service dashboardsnapshots.Service - namespacer request.NamespaceMapper -} - -var _ = rest.Connecter(&subBodyREST{}) - -func (r *subBodyREST) New() runtime.Object { - return &dashboardsnapshot.FullDashboardSnapshot{} -} - -func (r *subBodyREST) Destroy() {} - -func (r *subBodyREST) ConnectMethods() []string { - return []string{"GET"} -} - -func (r *subBodyREST) NewConnectOptions() (runtime.Object, bool, string) { - return nil, false, "" -} - -func (r *subBodyREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - snap, err := r.service.GetDashboardSnapshot(ctx, &dashboardsnapshots.GetDashboardSnapshotQuery{ - Key: name, - }) - if err != nil { - responder.Error(err) - return - } - - data, err := snap.Dashboard.Map() - if err != nil { - responder.Error(err) - return - } - - r := convertSnapshotToK8sResource(snap, r.namespacer) - responder.Object(200, &dashboardsnapshot.FullDashboardSnapshot{ - ObjectMeta: r.ObjectMeta, - Info: r.Spec, - Dashboard: common.Unstructured{Object: data}, - }) - }), nil -} diff --git a/pkg/registry/apis/wireset.go b/pkg/registry/apis/wireset.go index d19b405c6c9..12153f812a1 100644 --- a/pkg/registry/apis/wireset.go +++ b/pkg/registry/apis/wireset.go @@ -5,7 +5,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/collections" dashboardinternal "github.com/grafana/grafana/pkg/registry/apis/dashboard" - "github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot" "github.com/grafana/grafana/pkg/registry/apis/datasource" "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/registry/apis/iam" @@ -60,7 +59,6 @@ var WireSet = wire.NewSet( // Each must be added here *and* in the ServiceSink above dashboardinternal.RegisterAPIService, - dashboardsnapshot.RegisterAPIService, datasource.RegisterAPIService, folders.RegisterAPIService, iam.RegisterAPIService, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 926c1434a50..a9dc71d4d3b 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -51,7 +51,6 @@ import ( "github.com/grafana/grafana/pkg/registry/apis/collections" "github.com/grafana/grafana/pkg/registry/apis/dashboard" "github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy" - "github.com/grafana/grafana/pkg/registry/apis/dashboardsnapshot" "github.com/grafana/grafana/pkg/registry/apis/datasource" "github.com/grafana/grafana/pkg/registry/apis/folders" "github.com/grafana/grafana/pkg/registry/apis/iam" @@ -866,8 +865,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService) apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) - dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl) - snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) + dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl, serviceImpl) dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer, sourcesService) if err != nil { return nil, err @@ -919,7 +917,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api if err != nil { return nil, err } - apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) + apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) if err != nil { return nil, err @@ -1520,8 +1518,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac identitySynchronizer := authnimpl.ProvideIdentitySynchronizer(authnimplService) ldapImpl := service12.ProvideService(cfg, featureToggles, ssosettingsimplService) apiService := api4.ProvideService(cfg, routeRegisterImpl, accessControl, userService, authinfoimplService, ossGroups, identitySynchronizer, orgService, ldapImpl, userAuthTokenService, bundleregistryService) - dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl) - snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer) + dashboardsAPIBuilder := dashboard.RegisterAPIService(cfg, featureToggles, apiserverService, dashboardService, dashboardProvisioningService, service15, dashboardServiceImpl, dashboardPermissionsService, accessControl, accessClient, provisioningServiceImpl, dashboardsStore, registerer, sqlStore, tracingService, resourceClient, dualwriteService, sortService, quotaService, libraryPanelService, eventualRestConfigProvider, userService, libraryElementService, publicDashboardServiceImpl, serviceImpl) dataSourceAPIBuilder, err := datasource.RegisterAPIService(featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer, sourcesService) if err != nil { return nil, err @@ -1573,7 +1570,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac if err != nil { return nil, err } - apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, snapshotsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) + apiregistryService := apiregistry.ProvideRegistryServiceSink(dashboardsAPIBuilder, dataSourceAPIBuilder, folderAPIBuilder, identityAccessManagementAPIBuilder, queryAPIBuilder, userStorageAPIBuilder, apiBuilder, collectionsAPIBuilder, provisioningAPIBuilder, ofrepAPIBuilder, dependencyRegisterer, provisioningDependencyRegisterer) teamPermissionsService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, acimplService, teamService, userService, actionSetService) if err != nil { return nil, err diff --git a/pkg/services/dashboardsnapshots/database/database_test.go b/pkg/services/dashboardsnapshots/database/database_test.go index 14b411303ba..151754ab9fe 100644 --- a/pkg/services/dashboardsnapshots/database/database_test.go +++ b/pkg/services/dashboardsnapshots/database/database_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + snapshot "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" @@ -123,7 +123,7 @@ func TestIntegrationDashboardSnapshotDBAccess(t *testing.T) { cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{ Key: "strangesnapshotwithuserid0", DeleteKey: "adeletekey", - DashboardCreateCommand: dashboardsnapshot.DashboardCreateCommand{ + DashboardCreateCommand: snapshot.DashboardCreateCommand{ Dashboard: &common.Unstructured{Object: map[string]any{ "hello": "mupp", }}, @@ -202,7 +202,7 @@ func createTestSnapshot(t *testing.T, dashStore *DashboardSnapshotStore, key str cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{ Key: key, DeleteKey: "delete" + key, - DashboardCreateCommand: dashboardsnapshot.DashboardCreateCommand{ + DashboardCreateCommand: snapshot.DashboardCreateCommand{ Expires: expires, Dashboard: &common.Unstructured{Object: map[string]any{ "hello": "mupp", diff --git a/pkg/services/dashboardsnapshots/models.go b/pkg/services/dashboardsnapshots/models.go index 1972552f398..3ebf6ae6d0b 100644 --- a/pkg/services/dashboardsnapshots/models.go +++ b/pkg/services/dashboardsnapshots/models.go @@ -3,8 +3,8 @@ package dashboardsnapshots import ( "time" + snapshot "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" - dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/components/simplejson" ) @@ -50,7 +50,7 @@ type DashboardSnapshotDTO struct { type CreateDashboardSnapshotCommand struct { // The "public" fields are defined in this struct while the private/SQL/response params are // defied in the rest of this command - dashboardsnapshot.DashboardCreateCommand + snapshot.DashboardCreateCommand ExternalURL string `json:"-"` ExternalDeleteURL string `json:"-"` diff --git a/pkg/services/dashboardsnapshots/service.go b/pkg/services/dashboardsnapshots/service.go index d08cba500a5..281afee38ac 100644 --- a/pkg/services/dashboardsnapshots/service.go +++ b/pkg/services/dashboardsnapshots/service.go @@ -9,9 +9,9 @@ import ( "net/http" "time" + snapshot "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" - dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" @@ -36,7 +36,7 @@ var client = &http.Client{ Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}, } -func CreateDashboardSnapshot(c *contextmodel.ReqContext, cfg dashboardsnapshot.SnapshotSharingOptions, cmd CreateDashboardSnapshotCommand, svc Service) { +func CreateDashboardSnapshot(c *contextmodel.ReqContext, cfg snapshot.SnapshotSharingOptions, cmd CreateDashboardSnapshotCommand, svc Service) { if !cfg.SnapshotsEnabled { c.JsonApiErr(http.StatusForbidden, "Dashboard Snapshots are disabled", nil) return @@ -125,7 +125,7 @@ func CreateDashboardSnapshot(c *contextmodel.ReqContext, cfg dashboardsnapshot.S return } - c.JSON(http.StatusOK, dashboardsnapshot.DashboardCreateResponse{ + c.JSON(http.StatusOK, snapshot.DashboardCreateResponse{ Key: result.Key, DeleteKey: result.DeleteKey, URL: snapshotUrl, diff --git a/pkg/services/dashboardsnapshots/service/service_test.go b/pkg/services/dashboardsnapshots/service/service_test.go index ff17485729a..0ccd0be06f1 100644 --- a/pkg/services/dashboardsnapshots/service/service_test.go +++ b/pkg/services/dashboardsnapshots/service/service_test.go @@ -7,8 +7,8 @@ import ( "github.com/stretchr/testify/require" + snapshot "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" - dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" @@ -53,7 +53,7 @@ func TestIntegrationDashboardSnapshotsService(t *testing.T) { cmd := dashboardsnapshots.CreateDashboardSnapshotCommand{ Key: dashboardKey, DeleteKey: dashboardKey, - DashboardCreateCommand: dashboardsnapshot.DashboardCreateCommand{ + DashboardCreateCommand: snapshot.DashboardCreateCommand{ Dashboard: dashboard, }, } diff --git a/pkg/services/dashboardsnapshots/service_test.go b/pkg/services/dashboardsnapshots/service_test.go index 94657daa597..c8e817b720e 100644 --- a/pkg/services/dashboardsnapshots/service_test.go +++ b/pkg/services/dashboardsnapshots/service_test.go @@ -10,9 +10,9 @@ import ( mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + snapshot "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1" common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1" "github.com/grafana/grafana/pkg/apimachinery/identity" - dashboardsnapshot "github.com/grafana/grafana/pkg/apis/dashboardsnapshot/v0alpha1" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" @@ -22,7 +22,7 @@ import ( func TestCreateDashboardSnapshot_DashboardNotFound(t *testing.T) { mockService := &MockService{} - cfg := dashboardsnapshot.SnapshotSharingOptions{ + cfg := snapshot.SnapshotSharingOptions{ SnapshotsEnabled: true, ExternalEnabled: false, } @@ -42,7 +42,7 @@ func TestCreateDashboardSnapshot_DashboardNotFound(t *testing.T) { _ = json.Unmarshal(dashboardBytes, dashboard) cmd := CreateDashboardSnapshotCommand{ - DashboardCreateCommand: dashboardsnapshot.DashboardCreateCommand{ + DashboardCreateCommand: snapshot.DashboardCreateCommand{ Dashboard: dashboard, Name: "Test Snapshot", }, diff --git a/pkg/tests/apis/dashboardsnapshot/snapshots_test.go b/pkg/tests/apis/dashboardsnapshot/snapshots_test.go deleted file mode 100644 index d83d89d9fab..00000000000 --- a/pkg/tests/apis/dashboardsnapshot/snapshots_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package dashboardsnapshots - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/tests/apis" - "github.com/grafana/grafana/pkg/tests/testinfra" - "github.com/grafana/grafana/pkg/tests/testsuite" - "github.com/grafana/grafana/pkg/util/testutil" -) - -func TestMain(m *testing.M) { - testsuite.Run(m) -} - -func TestIntegrationDashboardSnapshots(t *testing.T) { - testutil.SkipIntegrationTestInShortMode(t) - - helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{ - AppModeProduction: false, // required for experimental apis - DisableAnonymous: true, - EnableFeatureToggles: []string{ - featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // required to register dashboardsnapshot.grafana.app - }, - }) - - t.Run("Check discovery client", func(t *testing.T) { - disco := helper.GetGroupVersionInfoJSON("dashboardsnapshot.grafana.app") - - // fmt.Printf("%s", disco) - require.JSONEq(t, `[ - { - "freshness": "Current", - "resources": [ - { - "resource": "dashboardsnapshots", - "responseKind": { - "group": "", - "kind": "DashboardSnapshot", - "version": "" - }, - "scope": "Namespaced", - "singularResource": "dashboardsnapshot", - "subresources": [ - { - "responseKind": { - "group": "", - "kind": "FullDashboardSnapshot", - "version": "" - }, - "subresource": "body", - "verbs": [ - "get" - ] - } - ], - "verbs": [ - "delete", - "get", - "list" - ] - }, - { - "resource": "options", - "responseKind": { - "group": "", - "kind": "SharingOptions", - "version": "" - }, - "scope": "Namespaced", - "singularResource": "options", - "verbs": [ - "get", - "list" - ] - } - ], - "version": "v0alpha1" - } - ]`, disco) - }) -} diff --git a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json index 238db87aff4..3b7a79228bc 100644 --- a/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json +++ b/pkg/tests/apis/openapi_snapshots/dashboard.grafana.app-v0alpha1.json @@ -1935,6 +1935,447 @@ } } } + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/snapshots": { + "get": { + "tags": [ + "Snapshot" + ], + "description": "list objects of kind Snapshot", + "operationId": "listSnapshot", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.SnapshotList" + } + }, + "application/json;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.SnapshotList" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.SnapshotList" + } + }, + "application/vnd.kubernetes.protobuf;stream=watch": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.SnapshotList" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.SnapshotList" + } + } + } + } + }, + "x-kubernetes-action": "list", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Snapshot" + } + }, + "parameters": [ + { + "name": "allowWatchBookmarks", + "in": "query", + "description": "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "continue", + "in": "query", + "description": "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "fieldSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "labelSelector", + "in": "query", + "description": "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "limit", + "in": "query", + "description": "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersion", + "in": "query", + "description": "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "resourceVersionMatch", + "in": "query", + "description": "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "sendInitialEvents", + "in": "query", + "description": "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "timeoutSeconds", + "in": "query", + "description": "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "watch", + "in": "query", + "description": "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/snapshots/create": { + "post": { + "tags": [ + "Snapshot" + ], + "description": "Creates a new Snapshot", + "operationId": "createSnapshot", + "parameters": [ + { + "name": "namespace", + "in": "path", + "description": "workspace", + "required": true, + "schema": { + "type": "string" + }, + "example": "default" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": {}, + "example": "{\"dashboard\":{\"annotations\":{\"list\":[{\"name\":\"Annotations \u0026 Alerts\",\"enable\":true,\"iconColor\":\"rgba(0, 211, 255, 1)\",\"snapshotData\":[],\"type\":\"dashboard\",\"builtIn\":1,\"hide\":true}]},\"editable\":true,\"fiscalYearStartMonth\":0,\"graphTooltip\":0,\"id\":203,\"links\":[],\"liveNow\":false,\"panels\":[{\"datasource\":null,\"fieldConfig\":{\"defaults\":{\"color\":{\"mode\":\"palette-classic\"},\"custom\":{\"axisBorderShow\":false,\"axisCenteredZero\":false,\"axisColorMode\":\"text\",\"axisLabel\":\"\",\"axisPlacement\":\"auto\",\"barAlignment\":0,\"drawStyle\":\"line\",\"fillOpacity\":43,\"gradientMode\":\"opacity\",\"hideFrom\":{\"legend\":false,\"tooltip\":false,\"viz\":false},\"insertNulls\":false,\"lineInterpolation\":\"smooth\",\"lineWidth\":1,\"pointSize\":5,\"scaleDistribution\":{\"type\":\"linear\"},\"showPoints\":\"auto\",\"spanNulls\":false,\"stacking\":{\"group\":\"A\",\"mode\":\"none\"},\"thresholdsStyle\":{\"mode\":\"off\"}},\"mappings\":[],\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]},\"unitScale\":true},\"overrides\":[]},\"gridPos\":{\"h\":8,\"w\":12,\"x\":0,\"y\":0},\"id\":1,\"options\":{\"legend\":{\"calcs\":[],\"displayMode\":\"list\",\"placement\":\"bottom\",\"showLegend\":true},\"tooltip\":{\"mode\":\"single\",\"sort\":\"none\"}},\"pluginVersion\":\"10.4.0-pre\",\"snapshotData\":[{\"fields\":[{\"config\":{\"color\":{\"mode\":\"palette-classic\"},\"custom\":{\"axisBorderShow\":false,\"axisCenteredZero\":false,\"axisColorMode\":\"text\",\"axisPlacement\":\"auto\",\"barAlignment\":0,\"drawStyle\":\"line\",\"fillOpacity\":43,\"gradientMode\":\"opacity\",\"hideFrom\":{\"legend\":false,\"tooltip\":false,\"viz\":false},\"lineInterpolation\":\"smooth\",\"lineWidth\":1,\"pointSize\":5,\"showPoints\":\"auto\",\"thresholdsStyle\":{\"mode\":\"off\"}},\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]},\"unitScale\":true},\"name\":\"time\",\"type\":\"time\",\"values\":[1706030536378,1706034856378,1706039176378,1706043496378,1706047816378,1706052136378]},{\"config\":{\"color\":{\"mode\":\"palette-classic\"},\"custom\":{\"axisBorderShow\":false,\"axisCenteredZero\":false,\"axisColorMode\":\"text\",\"axisLabel\":\"\",\"axisPlacement\":\"auto\",\"barAlignment\":0,\"drawStyle\":\"line\",\"fillOpacity\":43,\"gradientMode\":\"opacity\",\"hideFrom\":{\"legend\":false,\"tooltip\":false,\"viz\":false},\"insertNulls\":false,\"lineInterpolation\":\"smooth\",\"lineWidth\":1,\"pointSize\":5,\"scaleDistribution\":{\"type\":\"linear\"},\"showPoints\":\"auto\",\"spanNulls\":false,\"stacking\":{\"group\":\"A\",\"mode\":\"none\"},\"thresholdsStyle\":{\"mode\":\"off\"}},\"mappings\":[],\"thresholds\":{\"mode\":\"absolute\",\"steps\":[{\"color\":\"green\",\"value\":null},{\"color\":\"red\",\"value\":80}]},\"unitScale\":true},\"name\":\"A-series\",\"type\":\"number\",\"values\":[1,20,90,30,50,0]}],\"refId\":\"A\"}],\"targets\":[],\"title\":\"Simple example\",\"type\":\"timeseries\",\"links\":[]}],\"refresh\":\"\",\"schemaVersion\":39,\"snapshot\":{\"timestamp\":\"2024-01-23T23:22:16.377Z\"},\"tags\":[],\"templating\":{\"list\":[]},\"time\":{\"from\":\"2024-01-23T17:22:20.380Z\",\"to\":\"2024-01-23T23:22:20.380Z\",\"raw\":{\"from\":\"now-6h\",\"to\":\"now\"}},\"timepicker\":{},\"timezone\":\"\",\"title\":\"simple and small\",\"uid\":\"b22ec8db-399b-403b-b6c7-b0fb30ccb2a5\",\"version\":1,\"weekStart\":\"\"},\"name\":\"simple and small\",\"expires\":86400}" + } + } + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + } + } + }, + "x-grafana-action": "create", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "DashboardCreateResponse" + } + } + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/snapshots/delete/{deleteKey}": { + "description": "Delete snapshot by delete key", + "delete": { + "tags": [ + "Snapshot" + ], + "operationId": "deleteWithKey", + "parameters": [ + { + "name": "deleteKey", + "in": "path", + "description": "unique key returned in create", + "required": true, + "schema": { + "type": "string" + } + } + ] + } + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/snapshots/{name}": { + "get": { + "tags": [ + "Snapshot" + ], + "description": "read the specified Snapshot", + "operationId": "getSnapshot", + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.Snapshot" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.Snapshot" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.Snapshot" + } + } + } + } + }, + "x-kubernetes-action": "get", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Snapshot" + } + }, + "delete": { + "tags": [ + "Snapshot" + ], + "description": "delete a Snapshot", + "operationId": "deleteSnapshot", + "parameters": [ + { + "name": "dryRun", + "in": "query", + "description": "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "gracePeriodSeconds", + "in": "query", + "description": "The duration in seconds before the object should be deleted. Value must be non-negative integer. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. Defaults to a per object value if not specified. zero means delete immediately.", + "schema": { + "type": "integer", + "uniqueItems": true + } + }, + { + "name": "ignoreStoreReadErrorWithClusterBreakingPotential", + "in": "query", + "description": "if set to true, it will trigger an unsafe deletion of the resource in case the normal deletion flow fails with a corrupt object error. A resource is considered corrupt if it can not be retrieved from the underlying storage successfully because of a) its data can not be transformed e.g. decryption failure, or b) it fails to decode into an object. NOTE: unsafe deletion ignores finalizer constraints, skips precondition checks, and removes the object from the storage. WARNING: This may potentially break the cluster if the workload associated with the resource being unsafe-deleted relies on normal deletion flow. Use only if you REALLY know what you are doing. The default value is false, and the user must opt in to enable it", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "orphanDependents", + "in": "query", + "description": "Deprecated: please use the PropagationPolicy, this field will be deprecated in 1.7. Should the dependent objects be orphaned. If true/false, the \"orphan\" finalizer will be added to/removed from the object's finalizers list. Either this field or PropagationPolicy may be set, but not both.", + "schema": { + "type": "boolean", + "uniqueItems": true + } + }, + { + "name": "propagationPolicy", + "in": "query", + "description": "Whether and how garbage collection will be performed. Either this field or OrphanDependents may be set, but not both. The default policy is decided by the existing finalizer set in the metadata.finalizers and the resource-specific default policy. Acceptable values are: 'Orphan' - orphan the dependents; 'Background' - allow the garbage collector to delete the dependents in the background; 'Foreground' - a cascading policy that deletes all dependents in the foreground.", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + }, + "202": { + "description": "Accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/vnd.kubernetes.protobuf": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + }, + "application/yaml": { + "schema": { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.Status" + } + } + } + } + }, + "x-kubernetes-action": "delete", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Snapshot" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Snapshot", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "pretty", + "in": "query", + "description": "If 'true', then the output is pretty printed. Defaults to 'false' unless the user-agent indicates a browser or command-line HTTP tool (curl and wget).", + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] + }, + "/apis/dashboard.grafana.app/v0alpha1/namespaces/{namespace}/snapshots/{name}/dashboard": { + "get": { + "tags": [ + "Snapshot" + ], + "description": "connect GET requests to dashboard of Snapshot", + "operationId": "getSnapshotDashboard", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.Dashboard" + } + } + } + } + }, + "x-kubernetes-action": "connect", + "x-kubernetes-group-version-kind": { + "group": "dashboard.grafana.app", + "version": "v0alpha1", + "kind": "Dashboard" + } + }, + "parameters": [ + { + "name": "name", + "in": "path", + "description": "name of the Dashboard", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + }, + { + "name": "namespace", + "in": "path", + "description": "object name and auth scope, such as for teams and projects", + "required": true, + "schema": { + "type": "string", + "uniqueItems": true + } + } + ] } }, "components": { @@ -2758,6 +3199,127 @@ } } }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.Snapshot": { + "type": "object", + "required": [ + "metadata", + "spec" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta" + } + ] + }, + "spec": { + "description": "Spec is the spec of the Snapshot", + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.SnapshotSpec" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "Snapshot", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.SnapshotList": { + "type": "object", + "required": [ + "metadata", + "items" + ], + "properties": { + "apiVersion": { + "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + "type": "string" + }, + "items": { + "type": "array", + "items": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.Snapshot" + } + ] + } + }, + "kind": { + "description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "metadata": { + "default": {}, + "allOf": [ + { + "$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta" + } + ] + } + }, + "x-kubernetes-group-version-kind": [ + { + "group": "dashboard.grafana.app", + "kind": "SnapshotList", + "version": "v0alpha1" + } + ] + }, + "com.github.grafana.grafana.apps.dashboard.pkg.apis.dashboard.v0alpha1.SnapshotSpec": { + "type": "object", + "properties": { + "dashboard": { + "description": "The raw dashboard (unstructured for now)", + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "expires": { + "description": "Optionally auto-remove the snapshot at a future date (Unix timestamp in seconds)", + "type": "integer", + "format": "int64" + }, + "external": { + "description": "When set to true, the snapshot exists in a remote server", + "type": "boolean" + }, + "externalUrl": { + "description": "The external URL where the snapshot can be seen", + "type": "string" + }, + "originalUrl": { + "description": "The URL that created the dashboard originally", + "type": "string" + }, + "timestamp": { + "description": "Snapshot creation timestamp", + "type": "string" + }, + "title": { + "description": "Snapshot title", + "type": "string" + } + } + }, "com.github.grafana.grafana.pkg.apimachinery.apis.common.v0alpha1.Unstructured": { "type": "object", "additionalProperties": true, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx index 5fcf4d43f18..85813a99f6d 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx @@ -257,6 +257,7 @@ export class PanelEditor extends SceneObjectBase { searchQuery: '', listMode: OptionFilter.All, isVizPickerOpen: isUnconfigured, + isNewPanel: this.state.isNewPanel, }), isInitializing: false, }); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx index d5b5b608dc4..30b52bf9260 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditorRenderer.tsx @@ -1,11 +1,12 @@ import { css, cx } from '@emotion/css'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { t } from '@grafana/i18n'; import { SceneComponentProps, VizPanel } from '@grafana/scenes'; -import { Button, Spinner, ToolbarButton, useStyles2 } from '@grafana/ui'; +import { Button, Spinner, ToolbarButton, useStyles2, useTheme2 } from '@grafana/ui'; +import { MIN_SUGGESTIONS_PANE_WIDTH } from 'app/features/panel/suggestions/constants'; import { useEditPaneCollapsed } from '../edit-pane/shared'; import { NavToolbarActions } from '../scene/NavToolbarActions'; @@ -25,6 +26,8 @@ export function PanelEditorRenderer({ model }: SceneComponentProps) const isScrollingLayout = useScrollReflowLimit(); + const theme = useTheme2(); + const panePadding = useMemo(() => +theme.spacing(2).replace(/px$/, ''), [theme]); const { containerProps, primaryProps, secondaryProps, splitterProps, splitterState, onToggleCollapse } = useSnappingSplitter({ direction: 'row', @@ -32,7 +35,7 @@ export function PanelEditorRenderer({ model }: SceneComponentProps) initialSize: 330, usePixels: true, collapsed: isInitiallyCollapsed, - collapseBelowPixels: 250, + collapseBelowPixels: MIN_SUGGESTIONS_PANE_WIDTH + panePadding, disabled: isScrollingLayout, }); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx index 4c5b7729745..3f54971f99d 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelOptionsPane.tsx @@ -12,8 +12,8 @@ import { SelectableValue, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { t } from '@grafana/i18n'; -import { locationService, reportInteraction } from '@grafana/runtime'; +import { t, Trans } from '@grafana/i18n'; +import { config, locationService, reportInteraction } from '@grafana/runtime'; import { DeepPartial, SceneComponentProps, @@ -23,7 +23,7 @@ import { VizPanel, sceneGraph, } from '@grafana/scenes'; -import { Button, FilterInput, ScrollContainer, Stack, ToolbarButton, useStyles2, Field } from '@grafana/ui'; +import { Button, FilterInput, ScrollContainer, Stack, ToolbarButton, useStyles2, Text } from '@grafana/ui'; import { OptionFilter } from 'app/features/dashboard/components/PanelEditor/OptionsPaneOptions'; import { getPanelPluginNotFound } from 'app/features/panel/components/PanelPluginError'; import { VizTypeChangeDetails } from 'app/features/panel/components/VizTypePicker/types'; @@ -39,6 +39,8 @@ export interface PanelOptionsPaneState extends SceneObjectState { searchQuery: string; listMode: OptionFilter; panelRef: SceneObjectRef; + isNewPanel?: boolean; + hasPickedViz?: boolean; } interface PluginOptionsCache { @@ -50,11 +52,15 @@ export class PanelOptionsPane extends SceneObjectBase { private _cachedPluginOptions: Record = {}; onToggleVizPicker = () => { + const newState = !this.state.isVizPickerOpen; reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.TOGGLE_DROPDOWN, - open: !this.state.isVizPickerOpen, + open: newState, + }); + this.setState({ + isVizPickerOpen: newState, + hasPickedViz: this.state.hasPickedViz || newState === false, }); - this.setState({ isVizPickerOpen: !this.state.isVizPickerOpen }); }; onChangePanelPlugin = (options: VizTypeChangeDetails) => { @@ -131,7 +137,7 @@ export class PanelOptionsPane extends SceneObjectBase { } function PanelOptionsPaneComponent({ model }: SceneComponentProps) { - const { isVizPickerOpen, searchQuery, listMode, panelRef } = model.useState(); + const { isVizPickerOpen, searchQuery, listMode, panelRef, isNewPanel, hasPickedViz } = model.useState(); const panel = panelRef.resolve(); const { pluginId } = panel.useState(); const { data } = sceneGraph.getData(panel).useState(); @@ -142,34 +148,65 @@ function PanelOptionsPaneComponent({ model }: SceneComponentProps { + let meta = getAllPanelPluginMeta().filter((p) => p.id === pluginId)[0]; + if (!meta) { + const notFound = getPanelPluginNotFound(`Panel plugin not found (${pluginId})`, true); + meta = notFound.meta; + } + return meta; + }, [pluginId]); + return ( <> {!isVizPickerOpen && ( <>
- - - - + + + {hasFieldConfig && ( + { + model.onSetListMode(onlyOverrides ? OptionFilter.All : OptionFilter.Overrides); + }} + /> + )} +
+ {isSearchingOptions && ( +
- )} -
+ + )} @@ -195,6 +232,7 @@ function PanelOptionsPaneComponent({ model }: SceneComponentProps )} @@ -205,63 +243,24 @@ function getStyles(theme: GrafanaTheme2) { return { top: css({ display: 'flex', - flexDirection: 'column', - padding: theme.spacing(1, 2, 2, 2), + flexDirection: 'row', + padding: theme.spacing(1, 2), gap: theme.spacing(2), + justifyContent: 'space-between', + alignItems: 'center', }), searchOptions: css({ minHeight: theme.spacing(4), }), searchWrapper: css({ - padding: theme.spacing(2, 2, 2, 0), + padding: theme.spacing(1, 2, 2, 2), }), rotateIcon: css({ rotate: '180deg', }), - }; -} - -interface VisualizationButtonProps { - pluginId: string; - onOpen: () => void; -} - -export function VisualizationButton({ pluginId, onOpen }: VisualizationButtonProps) { - const styles = useStyles2(getVizButtonStyles); - let pluginMeta: PanelPluginMeta | undefined = useMemo( - () => getAllPanelPluginMeta().filter((p) => p.id === pluginId)[0], - [pluginId] - ); - - if (!pluginMeta) { - const notFound = getPanelPluginNotFound(`Panel plugin not found (${pluginId})`, true); - pluginMeta = notFound.meta; - } - - return ( - - {pluginMeta.name} - - ); -} - -function getVizButtonStyles(theme: GrafanaTheme2) { - return { - vizButton: css({ - textAlign: 'left', + pluginIcon: css({ + height: '22px', + width: '22px', }), }; } diff --git a/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx b/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx index d5b236fdd58..cb0d9fbcd3a 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx @@ -4,10 +4,11 @@ import { useCallback, useMemo, useState } from 'react'; import { useSessionStorage } from 'react-use'; import { GrafanaTheme2, PanelData } from '@grafana/data'; -import { t } from '@grafana/i18n'; +import { selectors } from '@grafana/e2e-selectors'; +import { t, Trans } from '@grafana/i18n'; import { config, reportInteraction } from '@grafana/runtime'; import { VizPanel } from '@grafana/scenes'; -import { FilterInput, ScrollContainer, Tab, TabContent, TabsBar, useStyles2 } from '@grafana/ui'; +import { Button, FilterInput, ScrollContainer, Stack, Tab, TabContent, TabsBar, useStyles2 } from '@grafana/ui'; import { LS_VISUALIZATION_SELECT_TAB_KEY } from 'app/core/constants'; import { VisualizationSelectPaneTab } from 'app/features/dashboard/components/PanelEditor/types'; import { VisualizationSuggestions } from 'app/features/panel/components/VizTypePicker/VisualizationSuggestions'; @@ -20,6 +21,7 @@ import { INTERACTION_EVENT_NAME, INTERACTION_ITEM } from './interaction'; export interface Props { data?: PanelData; + showBackButton?: boolean; panel: VizPanel; onChange: (options: VizTypeChangeDetails) => void; onClose: () => void; @@ -39,7 +41,7 @@ const getTabs = (): Array<{ label: string; value: VisualizationSelectPaneTab }> : [allVisualizationsTab, suggestionsTab]; }; -export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) { +export function PanelVizTypePicker({ panel, data, onChange, onClose, showBackButton }: Props) { const styles = useStyles2(getStyles); const panelModel = useMemo(() => new PanelModelCompatibilityWrapper(panel), [panel]); @@ -60,9 +62,6 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) { }, 300), [] ); - const handleSearchChange = (value: string) => { - setSearchQuery(value); - }; /** TABS */ const tabs = useMemo(getTabs, []); @@ -84,15 +83,6 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) { return (
- {/*@TODO: Re-enable/move close button*/} - {/**/} {tabs.map((tab) => ( - + {listMode === VisualizationSelectPaneTab.Suggestions && ( )} {listMode === VisualizationSelectPaneTab.Visualizations && ( - <> -
+ + + {showBackButton && ( + + )} -
+ - + )}
@@ -138,7 +140,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'flex', flexDirection: 'column', flexGrow: 1, - padding: theme.spacing(2, 1), height: '100%', gap: theme.spacing(2), }), @@ -154,6 +155,9 @@ const getStyles = (theme: GrafanaTheme2) => ({ justifyContent: 'center', textAlign: 'center', }), + tabContent: css({ + paddingInline: theme.spacing(2), + }), closeButton: css({ marginLeft: 'auto', }), diff --git a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx index 5c72fc78d3f..ff552d6b89a 100644 --- a/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareButton/share-snapshot/ShareSnapshot.tsx @@ -57,7 +57,7 @@ function ShareSnapshotRenderer({ model }: SceneComponentProps) { }; const onDeleteSnapshotClick = async () => { - await deleteSnapshot(snapshotResult.value?.deleteUrl!); + await deleteSnapshot(snapshotResult.value?.key!); reset(); }; diff --git a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx index 6c02dd93830..9c4abbc3a6d 100644 --- a/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx +++ b/public/app/features/dashboard-scene/sharing/ShareSnapshotTab.tsx @@ -3,7 +3,6 @@ import useAsyncFn from 'react-use/lib/useAsyncFn'; import { SelectableValue } from '@grafana/data'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { Trans, t } from '@grafana/i18n'; -import { getBackendSrv } from '@grafana/runtime'; import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectRef, VizPanel } from '@grafana/scenes'; import { Dashboard } from '@grafana/schema'; import { Button, ClipboardButton, Field, Input, Modal, RadioButtonGroup, Stack } from '@grafana/ui'; @@ -180,8 +179,8 @@ export class ShareSnapshotTab extends SceneObjectBase imp } }; - public onSnapshotDelete = async (url: string) => { - const response = await getBackendSrv().get(url); + public onSnapshotDelete = async (key: string) => { + const response = await getDashboardSnapshotSrv().deleteSnapshot(key); dispatch( notifyApp(createSuccessNotification(t('snapshot.share.success-delete', 'Your snapshot has been deleted'))) ); @@ -196,8 +195,8 @@ function ShareSnapshotTabRenderer({ model }: SceneComponentProps { - return await getBackendSrv().get(url); + const [deleteSnapshotResult, deleteSnapshot] = useAsyncFn(async (key: string) => { + return await getDashboardSnapshotSrv().deleteSnapshot(key); }); // If snapshot has been deleted - show message and allow to close modal @@ -306,7 +305,7 @@ function ShareSnapshotTabRenderer({ model }: SceneComponentProps { - deleteSnapshot(snapshotResult.value!.deleteUrl); + deleteSnapshot(snapshotResult.value!.key); }} > Delete snapshot. diff --git a/public/app/features/dashboard/services/SnapshotSrv.ts b/public/app/features/dashboard/services/SnapshotSrv.ts index d751737f92d..276f9d717df 100644 --- a/public/app/features/dashboard/services/SnapshotSrv.ts +++ b/public/app/features/dashboard/services/SnapshotSrv.ts @@ -2,7 +2,7 @@ import { lastValueFrom, map } from 'rxjs'; import { config, getBackendSrv, FetchResponse } from '@grafana/runtime'; import { contextSrv } from 'app/core/services/context_srv'; -import { DashboardDataDTO, DashboardDTO } from 'app/types/dashboard'; +import { DashboardDTO, SnapshotSpec } from 'app/types/dashboard'; import { getAPINamespace } from '../../../api/utils'; @@ -68,6 +68,7 @@ interface K8sMetadata { interface K8sSnapshotInfo { title: string; + external: boolean; externalUrl?: string; expires?: number; } @@ -83,17 +84,17 @@ interface DashboardSnapshotList { interface K8sDashboardSnapshot { apiVersion: string; - kind: 'DashboardSnapshot'; + kind: 'Snapshot'; metadata: K8sMetadata; - dashboard: DashboardDataDTO; + spec: SnapshotSpec; } class K8sAPI implements DashboardSnapshotSrv { - readonly apiVersion = 'dashboardsnapshot.grafana.app/v0alpha1'; + readonly apiVersion = 'dashboard.grafana.app/v0alpha1'; readonly url: string; constructor() { - this.url = `/apis/${this.apiVersion}/namespaces/${getAPINamespace()}/dashboardsnapshots`; + this.url = `/apis/${this.apiVersion}/namespaces/${getAPINamespace()}/snapshots`; } async create(cmd: SnapshotCreateCommand) { @@ -106,7 +107,7 @@ class K8sAPI implements DashboardSnapshotSrv { return { key: r.metadata.name, name: r.spec.title, - external: r.spec.externalUrl != null, + external: r.spec.external, externalUrl: r.spec.externalUrl, }; }); @@ -133,14 +134,14 @@ class K8sAPI implements DashboardSnapshotSrv { return lastValueFrom( getBackendSrv() .fetch({ - url: this.url + '/' + uid + '/body', + url: this.url + '/' + uid, method: 'GET', headers: headers, }) .pipe( map((response: FetchResponse) => { return { - dashboard: response.data.dashboard, + dashboard: response.data.spec.dashboard, meta: { isSnapshot: true, canSave: false, diff --git a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestionCard.tsx b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestionCard.tsx index 7f04c8a9dea..cf9fafd0b9b 100644 --- a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestionCard.tsx +++ b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestionCard.tsx @@ -1,8 +1,8 @@ import { css, cx } from '@emotion/css'; import { cloneDeep } from 'lodash'; -import { CSSProperties, HTMLAttributes } from 'react'; +import { CSSProperties, HTMLAttributes, ReactNode } from 'react'; -import { GrafanaTheme2, PanelData, PanelPluginVisualizationSuggestion } from '@grafana/data'; +import { colorManipulator, GrafanaTheme2, PanelData, PanelPluginVisualizationSuggestion } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config } from '@grafana/runtime'; import { Tooltip, useStyles2 } from '@grafana/ui'; @@ -16,7 +16,14 @@ export interface Props extends HTMLAttributes { isSelected?: boolean; } -export function VisualizationSuggestionCard({ data, suggestion, width, isSelected = false, onClick }: Props) { +export function VisualizationSuggestionCard({ + data, + suggestion, + width, + isSelected = false, + className, + ...restProps +}: Props) { const styles = useStyles2(getStyles); const { innerStyles, outerStyles, renderWidth, renderHeight } = getPreviewDimensionsAndStyles(width); const cardOptions = suggestion.cardOptions ?? {}; @@ -24,35 +31,30 @@ export function VisualizationSuggestionCard({ data, suggestion, width, isSelecte const commonButtonProps = { 'aria-label': suggestion.name, - className: cx(styles.vizBox, isNewVizSuggestionsEnabled && isSelected && styles.selectedBox), + className: cx(className, styles.vizBox), 'data-testid': selectors.components.VisualizationPreview.card(suggestion.name), style: outerStyles, - onClick, + ...restProps, }; + let content: ReactNode; + if (cardOptions.imgSrc) { - return ( - - - + content = ( + ); - } + } else { + let preview = suggestion; + if (suggestion.cardOptions?.previewModifier) { + preview = cloneDeep(suggestion); + suggestion.cardOptions.previewModifier(preview); + } - let preview = suggestion; - if (suggestion.cardOptions?.previewModifier) { - preview = cloneDeep(suggestion); - suggestion.cardOptions.previewModifier(preview); - } - - return ( - - ); + + ); + } + + if (!isNewVizSuggestionsEnabled) { + return {content}; + } + + return content; } const getStyles = (theme: GrafanaTheme2) => { return { hoverPane: css({ position: 'absolute', - top: 0, - right: 0, - left: 0, - borderRadius: theme.spacing(2), - bottom: 0, + top: -4, + left: -4, + right: -2, + bottom: -2, + borderRadius: theme.spacing(0.5), + background: 'transparent', + [theme.transitions.handleMotion('no-preference', 'reduce')]: { + transition: theme.transitions.create(['background'], { + duration: theme.transitions.duration.short, + }), + }, + }), + hoverPaneSelected: css({ + background: colorManipulator.alpha(theme.colors.text.primary, 0.1), }), vizBox: css({ position: 'relative', @@ -97,10 +115,6 @@ const getStyles = (theme: GrafanaTheme2) => { background: theme.colors.background.secondary, }, }), - selectedBox: css({ - border: `2px solid ${theme.colors.primary.main}`, - boxShadow: `0 0 0 1px ${theme.colors.primary.main}`, - }), imgBox: css({ display: 'flex', flexDirection: 'column', diff --git a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx index 4505f886469..e60e91bcd96 100644 --- a/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx +++ b/public/app/features/panel/components/VizTypePicker/VisualizationSuggestions.tsx @@ -1,14 +1,21 @@ import { css } from '@emotion/css'; -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useMemo } from 'react'; import { useAsync, useMeasure } from 'react-use'; -import AutoSizer from 'react-virtualized-auto-sizer'; -import { GrafanaTheme2, PanelData, PanelModel, PanelPluginVisualizationSuggestion } from '@grafana/data'; +import { + GrafanaTheme2, + PanelData, + PanelModel, + PanelPluginMeta, + PanelPluginVisualizationSuggestion, +} from '@grafana/data'; import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; -import { Button, Icon, Text, useStyles2 } from '@grafana/ui'; +import { Alert, Button, Icon, Spinner, Text, useStyles2 } from '@grafana/ui'; import { UNCONFIGURED_PANEL_PLUGIN_ID } from 'app/features/dashboard-scene/scene/UnconfiguredPanel'; +import { getAllPanelPluginMeta } from '../../state/util'; +import { MIN_MULTI_COLUMN_SIZE } from '../../suggestions/constants'; import { getAllSuggestions } from '../../suggestions/getAllSuggestions'; import { hasData } from '../../suggestions/utils'; @@ -21,19 +28,35 @@ export interface Props { panel?: PanelModel; } -const MIN_COLUMN_SIZE = 260; - export function VisualizationSuggestions({ onChange, data, panel }: Props) { const styles = useStyles2(getStyles); - const { value: suggestions } = useAsync(async () => await getAllSuggestions(data), [data]); + const { value: suggestions, loading, error } = useAsync(() => getAllSuggestions(data), [data]); const [suggestionHash, setSuggestionHash] = useState(null); const [firstCardRef, { width }] = useMeasure(); const [firstCardHash, setFirstCardHash] = useState(null); - const isNewVizSuggestionsEnabled = config.featureToggles.newVizSuggestions; - const isUnconfiguredPanel = panel?.type === UNCONFIGURED_PANEL_PLUGIN_ID; + const suggestionsByVizType = useMemo(() => { + const meta = getAllPanelPluginMeta(); + const record: Record = {}; + for (const m of meta) { + record[m.id] = m; + } + + const result: Array<[PanelPluginMeta | undefined, PanelPluginVisualizationSuggestion[]]> = []; + let currentVizType: PanelPluginMeta | undefined = undefined; + for (const suggestion of suggestions || []) { + const vizType = record[suggestion.pluginId]; + if (!currentVizType || currentVizType.id !== vizType?.id) { + currentVizType = vizType; + result.push([vizType, []]); + } + result[result.length - 1][1].push(suggestion); + } + return result; + }, [suggestions]); + const applySuggestion = useCallback( (suggestion: PanelPluginVisualizationSuggestion, isPreview?: boolean) => { onChange({ @@ -66,19 +89,35 @@ export function VisualizationSuggestions({ onChange, data, panel }: Props) { } }, [suggestions, suggestionHash, firstCardHash, isNewVizSuggestionsEnabled, isUnconfiguredPanel, applySuggestion]); - const renderEmptyState = () => ( -
- - - - Run a query to start seeing suggested visualizations + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( + + + An error occurred when loading visualization suggestions. -
-
- ); + + ); + } if (isNewVizSuggestionsEnabled && (!data || !hasData(data))) { - return renderEmptyState(); + return ( +
+ + + + Run a query to start seeing suggested visualizations + + +
+ ); } if (!data) { @@ -86,28 +125,40 @@ export function VisualizationSuggestions({ onChange, data, panel }: Props) { } return ( - // This div is needed in some places to make AutoSizer work -
- - {() => ( -
-
- {suggestions?.map((suggestion, index) => { - const isCardSelected = isNewVizSuggestionsEnabled && suggestionHash === suggestion.hash; - +
+ {isNewVizSuggestionsEnabled + ? suggestionsByVizType.map(([vizType, vizTypeSuggestions]) => ( + <> +
+ + {vizType?.info && } + {vizType?.name || t('panel.visualization-suggestions.unknown-viz-type', 'Unknown visualization type')} + +
+ {vizTypeSuggestions?.map((suggestion, index) => { + const isCardSelected = suggestionHash === suggestion.hash; return ( -
+
{isCardSelected && ( @@ -117,21 +168,39 @@ export function VisualizationSuggestions({ onChange, data, panel }: Props) { suggestion={suggestion} width={width} isSelected={isCardSelected} - onClick={() => applySuggestion(suggestion, isNewVizSuggestionsEnabled)} + tabIndex={index} + onClick={() => applySuggestion(suggestion, true)} />
); })} + + )) + : suggestions?.map((suggestion, index) => ( +
+ applySuggestion(suggestion)} + />
-
- )} - + ))}
); } const getStyles = (theme: GrafanaTheme2) => { return { + loadingContainer: css({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + width: '100%', + marginTop: theme.spacing(6), + }), filterRow: css({ display: 'flex', flexDirection: 'row', @@ -147,7 +216,7 @@ const getStyles = (theme: GrafanaTheme2) => { grid: css({ display: 'grid', gridGap: theme.spacing(1), - gridTemplateColumns: `repeat(auto-fit, minmax(${MIN_COLUMN_SIZE}px, 1fr))`, + gridTemplateColumns: `repeat(auto-fit, minmax(${MIN_MULTI_COLUMN_SIZE}px, 1fr))`, marginBottom: theme.spacing(1), justifyContent: 'space-evenly', }), @@ -167,13 +236,29 @@ const getStyles = (theme: GrafanaTheme2) => { cardContainer: css({ position: 'relative', }), + vizTypeHeader: css({ + gridColumn: '1 / -1', + marginBottom: theme.spacing(0.5), + marginTop: theme.spacing(2), + '&:first-of-type': { + marginTop: 0, + }, + }), + vizTypeLogo: css({ + filter: 'grayscale(100%)', + maxHeight: `${theme.typography.body.lineHeight}em`, + width: `${theme.typography.body.lineHeight}em`, + alignItems: 'center', + display: 'inline-block', + marginRight: theme.spacing(1), + }), applySuggestionButton: css({ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', zIndex: 10, - padding: '0 16px', + padding: theme.spacing(0, 2), }), }; }; diff --git a/public/app/features/panel/suggestions/constants.ts b/public/app/features/panel/suggestions/constants.ts new file mode 100644 index 00000000000..347c5e2edd3 --- /dev/null +++ b/public/app/features/panel/suggestions/constants.ts @@ -0,0 +1,8 @@ +// we overall want the suggestions pane to be at least 260px wide (plus padding, which we will +// get from the theme). +export const MIN_SUGGESTIONS_PANE_WIDTH = 260; + +// when the layout expands to multi-column, this is the minimum width of each column. +// this value ensures that the columns are never larger than 440px - the suggestions look +// weird if they get too wide. +export const MIN_MULTI_COLUMN_SIZE = 220; diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 4b926a7a5db..e9e7cce5ea0 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -98,6 +98,10 @@ export interface AnnotationsPermissions { organization: AnnotationActions; } +export interface SnapshotSpec { + dashboard: DashboardDataDTO; +} + // FIXME: This should not override Dashboard types export interface DashboardDataDTO extends Dashboard { title: string; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 7dbb0f60561..2830415ebaa 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5212,7 +5212,6 @@ }, "only-overrides-button-tooltip": "Show only overrides", "placeholder-search-options": "Search options", - "visualization-button-label": "Visualization", "visualization-button-tooltip": "Search options" }, "panel-editor-table-view": { @@ -6251,6 +6250,9 @@ } }, "panel-viz-type-picker": { + "button": { + "close": "Back" + }, "placeholder-search-for": "Search for...", "radio-options": { "label": { @@ -6562,7 +6564,7 @@ }, "visualization-button": { "aria-label-change-visualization": "Change visualization", - "tooltip-click-to-change-visualization": "Click to change visualization" + "text": "Change" }, "viz-and-data-pane": { "aria-label-open-query-pane": "Open query pane", @@ -11117,6 +11119,11 @@ }, "visualization-suggestions": { "apply-suggestion-aria-label": "Apply {{suggestionName}} visualization", + "error-loading-suggestions": { + "message": "An error occurred when loading visualization suggestions.", + "title": "Error" + }, + "unknown-viz-type": "Unknown visualization type", "use-this-suggestion": "Use this suggestion" }, "viz-type-picker": {