Playlist: Use a different go struct for sql service vs k8s (#76393)

pull/76310/head
Ryan McKinley 2 years ago committed by GitHub
parent 7562607319
commit 29cf60988b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 117
      docs/sources/developers/kinds/core/playlist/schema-reference.md
  2. 47
      kinds/playlist/playlist_kind.cue
  3. 9
      packages/grafana-schema/src/index.gen.ts
  4. 58
      packages/grafana-schema/src/raw/playlist/x/playlist_types.gen.ts
  5. 47
      pkg/apis/playlist/v0alpha1/conversions.go
  6. 64
      pkg/apis/playlist/v0alpha1/conversions_test.go
  7. 42
      pkg/apis/playlist/v0alpha1/legacy_storage.go
  8. 115
      pkg/apis/playlist/v0alpha1/openapi.go
  9. 5
      pkg/apis/playlist/v0alpha1/register.go
  10. 51
      pkg/apis/playlist/v0alpha1/types.go
  11. 42
      pkg/apis/playlist/v0alpha1/types_test.go
  12. 32
      pkg/apis/playlist/v0alpha1/zz_generated.deepcopy.go
  13. 188
      pkg/apis/playlist/v0alpha1/zz_generated.openapi.go
  14. 39
      pkg/kinds/playlist/playlist_gen.go
  15. 79
      pkg/kinds/playlist/playlist_kind_gen.go
  16. 42
      pkg/kinds/playlist/playlist_metadata_gen.go
  17. 57
      pkg/kinds/playlist/playlist_spec_gen.go
  18. 74
      pkg/kinds/playlist/playlist_status_gen.go
  19. 27
      pkg/kindsysreport/codegen/report.json
  20. 14
      pkg/registry/corekind/base_gen.go
  21. 53
      pkg/services/playlist/model.go
  22. 22
      pkg/services/playlist/playlistimpl/playlist.go
  23. 68
      pkg/services/store/kind/playlist/summary.go
  24. 40
      pkg/services/store/kind/playlist/summary_test.go
  25. 5
      pkg/services/store/kind/registry.go
  26. 10
      pkg/services/store/kind/registry_test.go
  27. 14
      public/app/features/playlist/api.ts

@ -1,117 +0,0 @@
---
keywords:
- grafana
- schema
labels:
products:
- cloud
- enterprise
- oss
title: Playlist kind
---
> Both documentation generation and kinds schemas are in active development and subject to change without prior notice.
## Playlist
#### Maturity: [merged](../../../maturity/#merged)
#### Version: 0.0
A playlist is a series of dashboards that is automatically rotated in the browser, on a configurable interval.
| Property | Type | Required | Default | Description |
|------------|---------------------|----------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `metadata` | [object](#metadata) | **Yes** | | metadata contains embedded CommonMetadata and can be extended with custom string fields<br/>TODO: use CommonMetadata instead of redefining here; currently needs to be defined here<br/>without external reference as using the CommonMetadata reference breaks thema codegen. |
| `spec` | [object](#spec) | **Yes** | | |
| `status` | [object](#status) | **Yes** | | |
### Metadata
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.
It extends [_kubeObjectMetadata](#_kubeobjectmetadata).
| Property | Type | Required | Default | Description |
|---------------------|------------------------|----------|---------|-----------------------------------------------------------------------------------------------------------------------------------------|
| `createdBy` | string | **Yes** | | |
| `creationTimestamp` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* |
| `extraFields` | [object](#extrafields) | **Yes** | | extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata |
| `finalizers` | string[] | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* |
| `labels` | map[string]string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* |
| `resourceVersion` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* |
| `uid` | string | **Yes** | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* |
| `updateTimestamp` | string | **Yes** | | |
| `updatedBy` | string | **Yes** | | |
| `deletionTimestamp` | string | No | | *(Inherited from [_kubeObjectMetadata](#_kubeobjectmetadata))* |
### _kubeObjectMetadata
_kubeObjectMetadata is metadata found in a kubernetes object's metadata field.
It is not exhaustive and only includes fields which may be relevant to a kind's implementation,
As it is also intended to be generic enough to function with any API Server.
| Property | Type | Required | Default | Description |
|---------------------|-------------------|----------|---------|-------------|
| `creationTimestamp` | string | **Yes** | | |
| `finalizers` | string[] | **Yes** | | |
| `labels` | map[string]string | **Yes** | | |
| `resourceVersion` | string | **Yes** | | |
| `uid` | string | **Yes** | | |
| `deletionTimestamp` | string | No | | |
### ExtraFields
extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
### Spec
| Property | Type | Required | Default | Description |
|------------|---------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `interval` | string | **Yes** | `5m` | Interval sets the time between switching views in a playlist.<br/>FIXME: Is this based on a standardized format or what options are available? Can datemath be used? |
| `name` | string | **Yes** | | Name of the playlist. |
| `uid` | string | **Yes** | | Unique playlist identifier. Generated on creation, either by the<br/>creator of the playlist of by the application. |
| `items` | [PlaylistItem](#playlistitem)[] | No | | The ordered list of items that the playlist will iterate over.<br/>FIXME! This should not be optional, but changing it makes the godegen awkward |
### PlaylistItem
| Property | Type | Required | Default | Description |
|----------|--------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `type` | string | **Yes** | | Type of the item.<br/>Possible values are: `dashboard_by_uid`, `dashboard_by_id`, `dashboard_by_tag`. |
| `value` | string | **Yes** | | Value depends on type and describes the playlist item.<br/><br/> - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This<br/> is not portable as the numerical identifier is non-deterministic between different instances.<br/> Will be replaced by dashboard_by_uid in the future. (deprecated)<br/> - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All<br/> dashboards behind the tag will be added to the playlist.<br/> - dashboard_by_uid: The value is the dashboard UID |
| `title` | string | No | | Title is an unused property -- it will be removed in the future |
### Status
| Property | Type | Required | Default | Description |
|--------------------|------------------------------------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `additionalFields` | [object](#additionalfields) | No | | additionalFields is reserved for future use |
| `operatorStates` | map[string][status.#OperatorState](#status.#operatorstate) | No | | operatorStates is a map of operator ID to operator state evaluations.<br/>Any operator which consumes this kind SHOULD add its state evaluation information to this field. |
### AdditionalFields
additionalFields is reserved for future use
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
### Status.#OperatorState
| Property | Type | Required | Default | Description |
|--------------------|--------------------|----------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `lastEvaluation` | string | **Yes** | | lastEvaluation is the ResourceVersion last evaluated |
| `state` | string | **Yes** | | state describes the state of the lastEvaluation.<br/>It is limited to three possible states for machine evaluation.<br/>Possible values are: `success`, `in_progress`, `failed`. |
| `descriptiveState` | string | No | | descriptiveState is an optional more descriptive state field which has no requirements on format |
| `details` | [object](#details) | No | | details contains any extra information that is operator-specific |
### Details
details contains any extra information that is operator-specific
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|

@ -1,47 +0,0 @@
package kind
name: "Playlist"
maturity: "merged"
description: "A playlist is a series of dashboards that is automatically rotated in the browser, on a configurable interval."
lineage: schemas: [{
version: [0, 0]
schema: {
spec: {
// Unique playlist identifier. Generated on creation, either by the
// creator of the playlist of by the application.
uid: string
// Name of the playlist.
name: string
// Interval sets the time between switching views in a playlist.
// FIXME: Is this based on a standardized format or what options are available? Can datemath be used?
interval: string | *"5m"
// The ordered list of items that the playlist will iterate over.
// FIXME! This should not be optional, but changing it makes the godegen awkward
items?: [...#PlaylistItem]
} @cuetsy(kind="interface")
///////////////////////////////////////
// Definitions (referenced above) are declared below
#PlaylistItem: {
// Type of the item.
type: "dashboard_by_uid" | "dashboard_by_id" | "dashboard_by_tag"
// Value depends on type and describes the playlist item.
//
// - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
// is not portable as the numerical identifier is non-deterministic between different instances.
// Will be replaced by dashboard_by_uid in the future. (deprecated)
// - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
// dashboards behind the tag will be added to the playlist.
// - dashboard_by_uid: The value is the dashboard UID
value: string
// Title is an unused property -- it will be removed in the future
title?: string
} @cuetsy(kind="interface")
}
}]

@ -124,15 +124,6 @@ export type {
// TODO generate code such that tsc enforces type compatibility between raw and veneer decls
export type { LibraryPanel } from './veneer/librarypanel.types';
// Raw generated types from Playlist kind.
export type {
Playlist,
PlaylistItem
} from './raw/playlist/x/playlist_types.gen';
// Raw generated enums and default consts from playlist kind.
export { defaultPlaylist } from './raw/playlist/x/playlist_types.gen';
// Raw generated types from Preferences kind.
export type {
Preferences,

@ -1,58 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// TSResourceJenny
// LatestMajorsOrXJenny
//
// Run 'make gen-cue' from repository root to regenerate.
export interface PlaylistItem {
/**
* Title is an unused property -- it will be removed in the future
*/
title?: string;
/**
* Type of the item.
*/
type: ('dashboard_by_uid' | 'dashboard_by_id' | 'dashboard_by_tag');
/**
* Value depends on type and describes the playlist item.
*
* - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
* is not portable as the numerical identifier is non-deterministic between different instances.
* Will be replaced by dashboard_by_uid in the future. (deprecated)
* - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
* dashboards behind the tag will be added to the playlist.
* - dashboard_by_uid: The value is the dashboard UID
*/
value: string;
}
export interface Playlist {
/**
* Interval sets the time between switching views in a playlist.
* FIXME: Is this based on a standardized format or what options are available? Can datemath be used?
*/
interval: string;
/**
* The ordered list of items that the playlist will iterate over.
* FIXME! This should not be optional, but changing it makes the godegen awkward
*/
items?: Array<PlaylistItem>;
/**
* Name of the playlist.
*/
name: string;
/**
* Unique playlist identifier. Generated on creation, either by the
* creator of the playlist of by the application.
*/
uid: string;
}
export const defaultPlaylist: Partial<Playlist> = {
interval: '5m',
items: [],
};

@ -0,0 +1,47 @@
package v0alpha1
import (
"fmt"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"github.com/grafana/grafana/pkg/services/playlist"
)
type namespaceMapper = func(orgId int64) string
func orgNamespaceMapper(orgId int64) string {
if orgId == 1 {
return "default"
}
return fmt.Sprintf("org-%d", orgId)
}
func convertToK8sResource(v *playlist.PlaylistDTO, namespacer namespaceMapper) *Playlist {
spec := Spec{
Title: v.Name,
Interval: v.Interval,
}
for _, item := range v.Items {
spec.Items = append(spec.Items, Item{
Type: ItemType(item.Type),
Value: item.Value,
})
}
return &Playlist{
TypeMeta: metav1.TypeMeta{
Kind: "Playlist",
APIVersion: APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: v.Uid,
UID: types.UID(v.Uid),
ResourceVersion: fmt.Sprintf("%d", v.UpdatedAt),
CreationTimestamp: metav1.NewTime(time.UnixMilli(v.CreatedAt)),
Namespace: namespacer(v.OrgID),
},
Spec: spec,
}
}

@ -0,0 +1,64 @@
package v0alpha1
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/playlist"
)
func TestPlaylistConversion(t *testing.T) {
src := &playlist.PlaylistDTO{
OrgID: 3,
Uid: "abc", // becomes k8s name
Name: "MyPlaylists", // becomes title
Interval: "10s",
CreatedAt: 12345,
UpdatedAt: 54321,
Items: []playlist.PlaylistItemDTO{
{Type: "dashboard_by_uid", Value: "UID0"},
{Type: "dashboard_by_tag", Value: "tagA"},
{Type: "dashboard_by_id", Value: "123"}, // deprecated
},
}
dst := convertToK8sResource(src, orgNamespaceMapper)
require.Equal(t, "abc", src.Uid)
require.Equal(t, "abc", dst.Name)
require.Equal(t, src.Name, dst.Spec.Title)
out, err := json.MarshalIndent(dst, "", " ")
require.NoError(t, err)
//fmt.Printf("%s", string(out))
require.JSONEq(t, `{
"kind": "Playlist",
"apiVersion": "playlist.x.grafana.com/v0alpha1",
"metadata": {
"name": "abc",
"namespace": "org-3",
"uid": "abc",
"resourceVersion": "54321",
"creationTimestamp": "1970-01-01T00:00:12Z"
},
"spec": {
"title": "MyPlaylists",
"interval": "10s",
"items": [
{
"type": "dashboard_by_uid",
"value": "UID0"
},
{
"type": "dashboard_by_tag",
"value": "tagA"
},
{
"type": "dashboard_by_id",
"value": "123"
}
]
}
}`, string(out))
}

@ -9,7 +9,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
playlistkind "github.com/grafana/grafana/pkg/kinds/playlist"
grafanarequest "github.com/grafana/grafana/pkg/services/grafana-apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/playlist"
)
@ -81,21 +80,14 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
},
}
for _, v := range res {
p := Playlist{
TypeMeta: metav1.TypeMeta{
Kind: "Playlist",
APIVersion: APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: v.UID,
},
Spec: playlistkind.Spec{
Name: v.Name,
Uid: v.UID,
Interval: v.Interval,
},
p, err := s.service.Get(ctx, &playlist.GetPlaylistByUidQuery{
UID: v.UID,
OrgId: orgId, // required
})
if err != nil {
return nil, err
}
list.Items = append(list.Items, p)
list.Items = append(list.Items, *convertToK8sResource(p, orgNamespaceMapper))
}
if len(list.Items) == limit {
list.Continue = "<more>" // TODO?
@ -109,30 +101,16 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge
orgId = 1 // TODO: default org ID 1 for now
}
p, err := s.service.Get(ctx, &playlist.GetPlaylistByUidQuery{
dto, err := s.service.Get(ctx, &playlist.GetPlaylistByUidQuery{
UID: name,
OrgId: orgId,
})
if err != nil {
return nil, err
}
if p == nil {
if dto == nil {
return nil, fmt.Errorf("not found?")
}
return &Playlist{
TypeMeta: metav1.TypeMeta{
Kind: "Playlist",
APIVersion: APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: p.Uid,
},
Spec: playlistkind.Spec{
Name: p.Name,
Uid: p.Uid,
Interval: p.Interval,
Items: p.Items,
},
}, nil
return convertToK8sResource(dto, orgNamespaceMapper), nil
}

@ -1,115 +0,0 @@
package v0alpha1
import (
common "k8s.io/kube-openapi/pkg/common"
spec "k8s.io/kube-openapi/pkg/validation/spec"
)
// NOTE: this must match the golang fully qualifid name!
const kindKey = "github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Playlist"
func getOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return map[string]common.OpenAPIDefinition{
kindKey: schema_pkg_Playlist(ref),
kindKey + "List": schema_pkg_PlaylistList(ref),
}
}
func schema_pkg_Playlist(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Playlist",
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"),
},
},
// TODO! add a real spec here
// "spec": {
// SchemaProps: spec.SchemaProps{
// Default: map[string]interface{}{},
// Ref: ref("github.com/grafana/google-sheets-datasource/pkg/apis/googlesheets/v1.DatasourceSpec"),
// },
// },
// "status": {
// SchemaProps: spec.SchemaProps{
// Default: map[string]interface{}{},
// Ref: ref("github.com/grafana/google-sheets-datasource/pkg/apis/googlesheets/v1.DatasourceStatus"),
// },
// },
},
},
},
Dependencies: []string{
"k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta",
},
}
}
func schema_pkg_PlaylistList(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "PlaylistList",
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(kindKey),
},
},
},
},
},
},
Required: []string{"items"},
},
},
Dependencies: []string{
kindKey,
"k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}

@ -15,11 +15,6 @@ import (
"github.com/grafana/grafana/pkg/services/playlist"
)
// GroupName is the group name for this API.
const GroupName = "playlist.x.grafana.com"
const VersionID = "v0alpha1" //
const APIVersion = GroupName + "/" + VersionID
var _ grafanaapiserver.APIGroupBuilder = (*PlaylistAPIBuilder)(nil)
// This is used just so wire has something unique to return

@ -2,10 +2,13 @@ package v0alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/grafana/grafana/pkg/kinds/playlist"
)
// GroupName is the group name for this API.
const GroupName = "playlist.x.grafana.com"
const VersionID = "v0alpha1" //
const APIVersion = GroupName + "/" + VersionID
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Playlist struct {
metav1.TypeMeta `json:",inline"`
@ -14,7 +17,7 @@ type Playlist struct {
// +optional
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec playlist.Spec `json:"spec,omitempty"`
Spec Spec `json:"spec,omitempty"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@ -23,5 +26,45 @@ type PlaylistList struct {
// +optional
metav1.ListMeta `json:"metadata,omitempty"`
Items []Playlist `json:"playlists,omitempty"`
Items []Playlist `json:"items,omitempty"`
}
// Spec defines model for Spec.
type Spec struct {
// Name of the playlist.
Title string `json:"title"`
// Interval sets the time between switching views in a playlist.
Interval string `json:"interval"`
// The ordered list of items that the playlist will iterate over.
Items []Item `json:"items,omitempty"`
}
// Defines values for ItemType.
const (
ItemTypeDashboardByTag ItemType = "dashboard_by_tag"
ItemTypeDashboardByUid ItemType = "dashboard_by_uid"
// deprecated -- should use UID
ItemTypeDashboardById ItemType = "dashboard_by_id"
)
// Item defines model for Item.
type Item struct {
// Type of the item.
Type ItemType `json:"type"`
// Value depends on type and describes the playlist item.
//
// - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
// is not portable as the numerical identifier is non-deterministic between different instances.
// Will be replaced by dashboard_by_uid in the future. (deprecated)
// - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
// dashboards behind the tag will be added to the playlist.
// - dashboard_by_uid: The value is the dashboard UID
Value string `json:"value"`
}
// Type of the item.
type ItemType string

@ -0,0 +1,42 @@
package v0alpha1
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestPlaylistClone(t *testing.T) {
src := Playlist{
TypeMeta: metav1.TypeMeta{
Kind: "Playlist",
APIVersion: APIVersion,
},
ObjectMeta: metav1.ObjectMeta{
Name: "TheUID",
ResourceVersion: "12345",
CreationTimestamp: metav1.NewTime(time.Now()),
Annotations: map[string]string{
"grafana.com/updatedTime": time.Now().Format(time.RFC3339),
},
},
Spec: Spec{
Title: "A title",
Interval: "20s",
Items: []Item{
{Type: ItemTypeDashboardByTag, Value: "graph-ng"},
},
},
}
copy := src.DeepCopyObject()
json0, err := json.Marshal(src)
require.NoError(t, err)
json1, err := json.Marshal(copy)
require.NoError(t, err)
require.JSONEq(t, string(json0), string(json1))
}

@ -1,6 +1,8 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by deepcopy-gen. DO NOT EDIT.
package v0alpha1
@ -9,11 +11,28 @@ 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 *Item) DeepCopyInto(out *Item) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Item.
func (in *Item) DeepCopy() *Item {
if in == nil {
return nil
}
out := new(Item)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Playlist) DeepCopyInto(out *Playlist) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
@ -69,17 +88,22 @@ func (in *PlaylistList) DeepCopyObject() runtime.Object {
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *legacyStorage) DeepCopyInto(out *legacyStorage) {
func (in *Spec) DeepCopyInto(out *Spec) {
*out = *in
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Item, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Storage.
func (in *legacyStorage) DeepCopy() *legacyStorage {
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Spec.
func (in *Spec) DeepCopy() *Spec {
if in == nil {
return nil
}
out := new(legacyStorage)
out := new(Spec)
in.DeepCopyInto(out)
return out
}

@ -0,0 +1,188 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
// SPDX-License-Identifier: AGPL-3.0-only
// Code generated by openapi-gen. DO NOT EDIT.
// This file was autogenerated by openapi-gen. Do not edit it manually!
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/playlist/v0alpha1.Item": schema_pkg_apis_playlist_v0alpha1_Item(ref),
"github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Playlist": schema_pkg_apis_playlist_v0alpha1_Playlist(ref),
"github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.PlaylistList": schema_pkg_apis_playlist_v0alpha1_PlaylistList(ref),
"github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Spec": schema_pkg_apis_playlist_v0alpha1_Spec(ref),
}
}
func schema_pkg_apis_playlist_v0alpha1_Item(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Item defines model for Item.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"type": {
SchemaProps: spec.SchemaProps{
Description: "Type of the item.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"value": {
SchemaProps: spec.SchemaProps{
Description: "Value depends on type and describes the playlist item.\n\n - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This\n is not portable as the numerical identifier is non-deterministic between different instances.\n Will be replaced by dashboard_by_uid in the future. (deprecated)\n - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All\n dashboards behind the tag will be added to the playlist.\n - dashboard_by_uid: The value is the dashboard UID",
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"type", "value"},
},
},
}
}
func schema_pkg_apis_playlist_v0alpha1_Playlist(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{
Description: "Standard object's metadata More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata",
Default: map[string]interface{}{},
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
},
},
"spec": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Spec"),
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Spec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
}
}
func schema_pkg_apis_playlist_v0alpha1_PlaylistList(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/playlist/v0alpha1.Playlist"),
},
},
},
},
},
},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Playlist", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
}
}
func schema_pkg_apis_playlist_v0alpha1_Spec(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Description: "Spec defines model for Spec.",
Type: []string{"object"},
Properties: map[string]spec.Schema{
"title": {
SchemaProps: spec.SchemaProps{
Description: "Name of the playlist.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"interval": {
SchemaProps: spec.SchemaProps{
Description: "Interval sets the time between switching views in a playlist.",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"items": {
SchemaProps: spec.SchemaProps{
Description: "The ordered list of items that the playlist will iterate over.",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Item"),
},
},
},
},
},
},
Required: []string{"title", "interval"},
},
},
Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/playlist/v0alpha1.Item"},
}
}

@ -1,39 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// GoTypesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package playlist
import (
"github.com/grafana/grafana/pkg/kinds"
)
// Resource is the kubernetes style representation of Playlist. (TODO be better)
type K8sResource = kinds.GrafanaResource[Spec, Status]
// NewResource creates a new instance of the resource with a given name (UID)
func NewK8sResource(name string, s *Spec) K8sResource {
return K8sResource{
Kind: "Playlist",
APIVersion: "v0-0-alpha",
Metadata: kinds.GrafanaResourceMetadata{
Name: name,
Annotations: make(map[string]string),
Labels: make(map[string]string),
},
Spec: s,
}
}
// Resource is the wire representation of Playlist.
// It currently will soon be merged into the k8s flavor (TODO be better)
type Resource struct {
Metadata Metadata `json:"metadata"`
Spec Spec `json:"spec"`
Status Status `json:"status"`
}

@ -1,79 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// CoreKindJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package playlist
import (
"github.com/grafana/kindsys"
"github.com/grafana/thema"
"github.com/grafana/thema/vmux"
"github.com/grafana/grafana/pkg/cuectx"
)
// rootrel is the relative path from the grafana repository root to the
// directory containing the .cue files in which this kind is defined. Necessary
// for runtime errors related to the definition and/or lineage to provide
// a real path to the correct .cue file.
const rootrel string = "kinds/playlist"
// TODO standard generated docs
type Kind struct {
kindsys.Core
lin thema.ConvergentLineage[*Resource]
jcodec vmux.Codec
valmux vmux.ValueMux[*Resource]
}
// type guard - ensure generated Kind type satisfies the kindsys.Core interface
var _ kindsys.Core = &Kind{}
// TODO standard generated docs
func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
def, err := cuectx.LoadCoreKindDef(rootrel, rt.Context(), nil)
if err != nil {
return nil, err
}
k := &Kind{}
k.Core, err = kindsys.BindCore(rt, def, opts...)
if err != nil {
return nil, err
}
// Get the thema.Schema that the meta says is in the current version (which
// codegen ensures is always the latest)
cursch := thema.SchemaP(k.Core.Lineage(), def.Properties.CurrentVersion)
tsch, err := thema.BindType(cursch, &Resource{})
if err != nil {
// Should be unreachable, modulo bugs in the Thema->Go code generator
return nil, err
}
k.jcodec = vmux.NewJSONCodec("playlist.json")
k.lin = tsch.ConvergentLineage()
k.valmux = vmux.NewValueMux(k.lin.TypedSchema(), k.jcodec)
return k, nil
}
// ConvergentLineage returns the same [thema.Lineage] as Lineage, but bound (see [thema.BindType])
// to the the Playlist [Resource] type generated from the current schema, v0.0.
func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*Resource] {
return k.lin
}
// JSONValueMux is a version multiplexer that maps a []byte containing JSON data
// at any schematized dashboard version to an instance of Playlist [Resource].
//
// Validation and translation errors emitted from this func will identify the
// input bytes as "dashboard.json".
//
// This is a thin wrapper around Thema's [vmux.ValueMux].
func (k *Kind) JSONValueMux(b []byte) (*Resource, thema.TranslationLacunas, error) {
return k.valmux(b)
}

@ -1,42 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// GoResourceTypes
//
// Run 'make gen-cue' from repository root to regenerate.
package playlist
import (
"time"
)
// Metadata defines model for Metadata.
type Metadata struct {
CreatedBy string `json:"createdBy"`
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
// extraFields is reserved for any fields that are pulled from the API server metadata but do not have concrete fields in the CUE metadata
ExtraFields map[string]any `json:"extraFields"`
Finalizers []string `json:"finalizers"`
Labels map[string]string `json:"labels"`
ResourceVersion string `json:"resourceVersion"`
Uid string `json:"uid"`
UpdateTimestamp time.Time `json:"updateTimestamp"`
UpdatedBy string `json:"updatedBy"`
}
// _kubeObjectMetadata is metadata found in a kubernetes object's metadata field.
// It is not exhaustive and only includes fields which may be relevant to a kind's implementation,
// As it is also intended to be generic enough to function with any API Server.
type KubeObjectMetadata struct {
CreationTimestamp time.Time `json:"creationTimestamp"`
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
Finalizers []string `json:"finalizers"`
Labels map[string]string `json:"labels"`
ResourceVersion string `json:"resourceVersion"`
Uid string `json:"uid"`
}

@ -1,57 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// GoResourceTypes
//
// Run 'make gen-cue' from repository root to regenerate.
package playlist
// Defines values for ItemType.
const (
ItemTypeDashboardById ItemType = "dashboard_by_id"
ItemTypeDashboardByTag ItemType = "dashboard_by_tag"
ItemTypeDashboardByUid ItemType = "dashboard_by_uid"
)
// Item defines model for Item.
type Item struct {
// Title is an unused property -- it will be removed in the future
Title *string `json:"title,omitempty"`
// Type of the item.
Type ItemType `json:"type"`
// Value depends on type and describes the playlist item.
//
// - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
// is not portable as the numerical identifier is non-deterministic between different instances.
// Will be replaced by dashboard_by_uid in the future. (deprecated)
// - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
// dashboards behind the tag will be added to the playlist.
// - dashboard_by_uid: The value is the dashboard UID
Value string `json:"value"`
}
// Type of the item.
type ItemType string
// Spec defines model for Spec.
type Spec struct {
// Interval sets the time between switching views in a playlist.
// FIXME: Is this based on a standardized format or what options are available? Can datemath be used?
Interval string `json:"interval"`
// The ordered list of items that the playlist will iterate over.
// FIXME! This should not be optional, but changing it makes the godegen awkward
Items []Item `json:"items,omitempty"`
// Name of the playlist.
Name string `json:"name"`
// Unique playlist identifier. Generated on creation, either by the
// creator of the playlist of by the application.
Uid string `json:"uid"`
}

@ -1,74 +0,0 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// GoResourceTypes
//
// Run 'make gen-cue' from repository root to regenerate.
package playlist
// Defines values for OperatorStateState.
const (
OperatorStateStateFailed OperatorStateState = "failed"
OperatorStateStateInProgress OperatorStateState = "in_progress"
OperatorStateStateSuccess OperatorStateState = "success"
)
// Defines values for StatusOperatorStateState.
const (
StatusOperatorStateStateFailed StatusOperatorStateState = "failed"
StatusOperatorStateStateInProgress StatusOperatorStateState = "in_progress"
StatusOperatorStateStateSuccess StatusOperatorStateState = "success"
)
// OperatorState defines model for OperatorState.
type OperatorState struct {
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]any `json:"details,omitempty"`
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State OperatorStateState `json:"state"`
}
// OperatorStateState state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
type OperatorStateState string
// Status defines model for Status.
type Status struct {
// additionalFields is reserved for future use
AdditionalFields map[string]any `json:"additionalFields,omitempty"`
// operatorStates is a map of operator ID to operator state evaluations.
// Any operator which consumes this kind SHOULD add its state evaluation information to this field.
OperatorStates map[string]StatusOperatorState `json:"operatorStates,omitempty"`
}
// StatusOperatorState defines model for status.#OperatorState.
type StatusOperatorState struct {
// descriptiveState is an optional more descriptive state field which has no requirements on format
DescriptiveState *string `json:"descriptiveState,omitempty"`
// details contains any extra information that is operator-specific
Details map[string]any `json:"details,omitempty"`
// lastEvaluation is the ResourceVersion last evaluated
LastEvaluation string `json:"lastEvaluation"`
// state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
State StatusOperatorStateState `json:"state"`
}
// StatusOperatorStateState state describes the state of the lastEvaluation.
// It is limited to three possible states for machine evaluation.
type StatusOperatorStateState string

@ -1322,31 +1322,26 @@
},
"playlist": {
"category": "core",
"codeowners": [
"grafana/grafana-as-code",
"grafana/grafana-frontend-platform",
"grafana/plugins-platform-frontend"
],
"codeowners": [],
"crd": {
"dummySchema": false,
"group": "playlist.core.grafana.com",
"scope": "Namespaced"
"group": "",
"scope": ""
},
"currentVersion": [
0,
0
],
"description": "A playlist is a series of dashboards that is automatically rotated in the browser, on a configurable interval.",
"grafanaMaturityCount": 0,
"lineageIsGroup": false,
"links": {
"docs": "https://grafana.com/docs/grafana/next/developers/kinds/core/playlist/schema-reference",
"go": "https://github.com/grafana/grafana/tree/main/pkg/kinds/playlist",
"schema": "https://github.com/grafana/grafana/tree/main/kinds/playlist/playlist_kind.cue",
"ts": "https://github.com/grafana/grafana/tree/main/packages/grafana-schema/src/raw/playlist/x/playlist_types.gen.ts"
"docs": "n/a",
"go": "n/a",
"schema": "n/a",
"ts": "n/a"
},
"machineName": "playlist",
"maturity": "merged",
"maturity": "planned",
"name": "Playlist",
"pluralMachineName": "playlists",
"pluralName": "Playlists"
@ -2277,7 +2272,6 @@
"folder",
"googlecloudmonitoringdataquery",
"heatmappanelcfg",
"playlist",
"preferences",
"publicdashboard",
"role",
@ -2286,7 +2280,7 @@
"timeseriespanelcfg",
"trendpanelcfg"
],
"count": 14
"count": 13
},
"planned": {
"name": "planned",
@ -2319,6 +2313,7 @@
"mysqldataquery",
"mysqldatasourcecfg",
"parcadatasourcecfg",
"playlist",
"postgresqldataquery",
"postgresqldatasourcecfg",
"prometheusdatasourcecfg",
@ -2335,7 +2330,7 @@
"zipkindataquery",
"zipkindatasourcecfg"
],
"count": 43
"count": 44
},
"stable": {
"name": "stable",

@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/kinds/dashboard"
"github.com/grafana/grafana/pkg/kinds/folder"
"github.com/grafana/grafana/pkg/kinds/librarypanel"
"github.com/grafana/grafana/pkg/kinds/playlist"
"github.com/grafana/grafana/pkg/kinds/preferences"
"github.com/grafana/grafana/pkg/kinds/publicdashboard"
"github.com/grafana/grafana/pkg/kinds/role"
@ -47,7 +46,6 @@ type Base struct {
dashboard *dashboard.Kind
folder *folder.Kind
librarypanel *librarypanel.Kind
playlist *playlist.Kind
preferences *preferences.Kind
publicdashboard *publicdashboard.Kind
role *role.Kind
@ -61,7 +59,6 @@ var (
_ kindsys.Core = &dashboard.Kind{}
_ kindsys.Core = &folder.Kind{}
_ kindsys.Core = &librarypanel.Kind{}
_ kindsys.Core = &playlist.Kind{}
_ kindsys.Core = &preferences.Kind{}
_ kindsys.Core = &publicdashboard.Kind{}
_ kindsys.Core = &role.Kind{}
@ -89,11 +86,6 @@ func (b *Base) LibraryPanel() *librarypanel.Kind {
return b.librarypanel
}
// Playlist returns the [kindsys.Interface] implementation for the playlist kind.
func (b *Base) Playlist() *playlist.Kind {
return b.playlist
}
// Preferences returns the [kindsys.Interface] implementation for the preferences kind.
func (b *Base) Preferences() *preferences.Kind {
return b.preferences
@ -147,12 +139,6 @@ func doNewBase(rt *thema.Runtime) *Base {
}
reg.all = append(reg.all, reg.librarypanel)
reg.playlist, err = playlist.NewKind(rt)
if err != nil {
panic(fmt.Sprintf("error while initializing the playlist Kind: %s", err))
}
reg.all = append(reg.all, reg.playlist)
reg.preferences, err = preferences.NewKind(rt)
if err != nil {
panic(fmt.Sprintf("error while initializing the preferences Kind: %s", err))

@ -2,8 +2,6 @@ package playlist
import (
"errors"
"github.com/grafana/grafana/pkg/kinds/playlist"
)
// Typed errors
@ -27,9 +25,47 @@ type Playlist struct {
UpdatedAt int64 `json:"-" db:"updated_at"`
}
type PlaylistDTO = playlist.Spec
type PlaylistItemDTO = playlist.Item
type PlaylistItemType = playlist.ItemType
type PlaylistDTO struct {
// Unique playlist identifier. Generated on creation, either by the
// creator of the playlist of by the application.
Uid string `json:"uid"`
// Name of the playlist.
Name string `json:"name"`
// Interval sets the time between switching views in a playlist.
Interval string `json:"interval"`
// The ordered list of items that the playlist will iterate over.
Items []PlaylistItemDTO `json:"items,omitempty"`
// Returned for k8s
CreatedAt int64 `json:"-"`
// Returned for k8s
UpdatedAt int64 `json:"-"`
// Returned for k8s
OrgID int64 `json:"-"`
}
type PlaylistItemDTO struct {
// Title is an unused property -- it will be removed in the future
Title *string `json:"title,omitempty"`
// Type of the item.
Type string `json:"type"`
// Value depends on type and describes the playlist item.
//
// - dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
// is not portable as the numerical identifier is non-deterministic between different instances.
// Will be replaced by dashboard_by_uid in the future. (deprecated)
// - dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
// dashboards behind the tag will be added to the playlist.
// - dashboard_by_uid: The value is the dashboard UID
Value string `json:"value"`
}
type PlaylistItem struct {
Id int64 `db:"id"`
@ -88,10 +124,3 @@ type GetPlaylistItemsByUidQuery struct {
PlaylistUID string
OrgId int64
}
func PlaylistToResource(p PlaylistDTO) playlist.K8sResource {
copy := p
r := playlist.NewK8sResource(p.Uid, &copy)
copy.Uid = "" // remove it from the payload
return r
}

@ -4,9 +4,7 @@ import (
"context"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/playlist"
"github.com/grafana/grafana/pkg/services/store/entity"
)
type Service struct {
@ -15,11 +13,10 @@ type Service struct {
var _ playlist.Service = &Service{}
func ProvideService(db db.DB, toggles featuremgmt.FeatureToggles, objserver entity.EntityStoreServer) playlist.Service {
sqlstore := &sqlStore{
func ProvideService(db db.DB) playlist.Service {
return &Service{store: &sqlStore{
db: db,
}
return &Service{store: sqlstore}
}}
}
func (s *Service) Create(ctx context.Context, cmd *playlist.CreatePlaylistCommand) (*playlist.Playlist, error) {
@ -48,7 +45,7 @@ func (s *Service) Get(ctx context.Context, q *playlist.GetPlaylistByUidQuery) (*
}
items := make([]playlist.PlaylistItemDTO, len(rawItems))
for i := 0; i < len(rawItems); i++ {
items[i].Type = playlist.PlaylistItemType(rawItems[i].Type)
items[i].Type = rawItems[i].Type
items[i].Value = rawItems[i].Value
// Add the unused title to the result
@ -58,10 +55,13 @@ func (s *Service) Get(ctx context.Context, q *playlist.GetPlaylistByUidQuery) (*
}
}
return &playlist.PlaylistDTO{
Uid: v.UID,
Name: v.Name,
Interval: v.Interval,
Items: items,
Uid: v.UID,
Name: v.Name,
Interval: v.Interval,
Items: items,
CreatedAt: v.CreatedAt,
UpdatedAt: v.UpdatedAt,
OrgID: v.OrgId,
}, nil
}

@ -1,68 +0,0 @@
package playlist
import (
"context"
"encoding/json"
"fmt"
"github.com/grafana/grafana/pkg/kinds/playlist"
"github.com/grafana/grafana/pkg/services/store/entity"
)
func GetEntityKindInfo() entity.EntityKindInfo {
return entity.EntityKindInfo{
ID: entity.StandardKindPlaylist,
Name: "Playlist",
Description: "Cycle though a collection of dashboards automatically",
}
}
func GetEntitySummaryBuilder() entity.EntitySummaryBuilder {
return summaryBuilder
}
func summaryBuilder(ctx context.Context, uid string, body []byte) (*entity.EntitySummary, []byte, error) {
obj := &playlist.Spec{}
err := json.Unmarshal(body, obj)
if err != nil {
return nil, nil, err // unable to read object
}
// TODO: fix model so this is not possible
if obj.Items == nil {
temp := make([]playlist.Item, 0)
obj.Items = temp
}
obj.Uid = uid // make sure they are consistent
summary := &entity.EntitySummary{
UID: uid,
Name: obj.Name,
Description: fmt.Sprintf("%d items, refreshed every %s", len(obj.Items), obj.Interval),
}
for _, item := range obj.Items {
switch item.Type {
case playlist.ItemTypeDashboardByUid:
summary.References = append(summary.References, &entity.EntityExternalReference{
Family: entity.StandardKindDashboard,
Identifier: item.Value,
})
case playlist.ItemTypeDashboardByTag:
if summary.Labels == nil {
summary.Labels = make(map[string]string, 0)
}
summary.Labels[item.Value] = ""
case playlist.ItemTypeDashboardById:
// obviously insufficient long term... but good to have an example :)
summary.Error = &entity.EntityErrorInfo{
Message: "Playlist uses deprecated internal id system",
}
}
}
out, err := json.MarshalIndent(obj, "", " ")
return summary, out, err
}

@ -1,40 +0,0 @@
package playlist
import (
"context"
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/kinds/playlist"
)
func TestPlaylistSummary(t *testing.T) {
builder := GetEntitySummaryBuilder()
// Do not parse invalid input
_, _, err := builder(context.Background(), "abc", []byte("{invalid json"))
require.Error(t, err)
playlist := playlist.Spec{
Interval: "30s",
Name: "test",
Items: []playlist.Item{
{Type: playlist.ItemTypeDashboardByUid, Value: "D1"},
{Type: playlist.ItemTypeDashboardByTag, Value: "tagA"},
{Type: playlist.ItemTypeDashboardByUid, Value: "D3"},
},
}
out, err := json.Marshal(playlist)
require.NoError(t, err)
require.NotNil(t, out)
// Do not parse invalid input
summary, body, err := builder(context.Background(), "abc", out)
require.NoError(t, err)
require.Equal(t, "test", summary.Name)
require.Equal(t, 2, len(summary.References))
require.Equal(t, map[string]string{"tagA": ""}, summary.Labels)
require.True(t, json.Valid(body))
}

@ -12,7 +12,6 @@ import (
"github.com/grafana/grafana/pkg/services/store/kind/folder"
"github.com/grafana/grafana/pkg/services/store/kind/geojson"
"github.com/grafana/grafana/pkg/services/store/kind/jsonobj"
"github.com/grafana/grafana/pkg/services/store/kind/playlist"
"github.com/grafana/grafana/pkg/services/store/kind/png"
"github.com/grafana/grafana/pkg/services/store/kind/preferences"
"github.com/grafana/grafana/pkg/services/store/kind/snapshot"
@ -30,10 +29,6 @@ type KindRegistry interface {
func NewKindRegistry() KindRegistry {
kinds := make(map[string]*kindValues)
kinds[entity.StandardKindPlaylist] = &kindValues{
info: playlist.GetEntityKindInfo(),
builder: playlist.GetEntitySummaryBuilder(),
}
kinds[entity.StandardKindDashboard] = &kindValues{
info: dashboard.GetEntityKindInfo(),
builder: dashboard.GetEntitySummaryBuilder(),

@ -5,7 +5,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/store/entity"
"github.com/grafana/grafana/pkg/services/store/kind/dummy"
)
@ -24,21 +23,14 @@ func TestKindRegistry(t *testing.T) {
"frame",
"geojson",
"jsonobj",
"playlist",
"png",
"preferences",
"snapshot",
"test",
}, ids)
// Check playlist exists
info, err := registry.GetInfo(entity.StandardKindPlaylist)
require.NoError(t, err)
require.Equal(t, "Playlist", info.Name)
require.False(t, info.IsRaw)
// Check that we registered a test item
info, err = registry.GetInfo("test")
info, err := registry.GetInfo("test")
require.NoError(t, err)
require.Equal(t, "test", info.Name)
require.True(t, info.IsRaw)

@ -46,7 +46,7 @@ interface K8sPlaylist {
name: string;
};
spec: {
name: string;
title: string;
interval: string;
items: PlaylistItem[];
};
@ -58,10 +58,7 @@ class K8sAPI implements PlaylistAPI {
async getAllPlaylist(): Promise<Playlist[]> {
const result = await getBackendSrv().get<K8sPlaylistList>(this.url);
console.log('getAllPlaylist', result);
const v = result.playlists.map(k8sResourceAsPlaylist);
console.log('after', v);
return v;
return result.playlists.map(k8sResourceAsPlaylist);
}
async getPlaylist(uid: string): Promise<Playlist> {
@ -118,9 +115,12 @@ class K8sAPI implements PlaylistAPI {
// the main difference is that k8s uses metdata.name as the uid
// to avoid future confusion, the display name is now called "title"
function k8sResourceAsPlaylist(r: K8sPlaylist): Playlist {
const { spec, metadata } = r;
return {
...r.spec,
uid: r.metadata.name, // replace the uid from the k8s name
uid: metadata.name, // use the k8s name as uid
name: spec.title,
interval: spec.interval,
items: spec.items,
};
}

Loading…
Cancel
Save