From 9ec7b202b6f056567959ae06b8c335b6b3fb8385 Mon Sep 17 00:00:00 2001 From: sam boyer Date: Fri, 30 Sep 2022 06:33:11 -0400 Subject: [PATCH] playlist: introduce coremodel schema, swap in for backend DTO types (#56048) * Add simplest possible playlist schema * Add @grafana(decisionNeeded) attributes * playlistid, not playlist_id --- packages/grafana-schema/src/index.gen.ts | 6 + .../src/raw/playlist/x/playlist.gen.ts | 69 ++++++++ pkg/api/playlist.go | 19 +-- pkg/coremodel/playlist/coremodel.cue | 54 +++++++ pkg/coremodel/playlist/playlist_gen.go | 147 ++++++++++++++++++ .../coremodel/registry/registry_gen.go | 15 ++ pkg/services/playlist/model.go | 19 +-- .../playlist/playlistimpl/sqlx_store.go | 4 +- .../playlist/playlistimpl/xorm_store.go | 15 +- 9 files changed, 315 insertions(+), 33 deletions(-) create mode 100644 packages/grafana-schema/src/raw/playlist/x/playlist.gen.ts create mode 100644 pkg/coremodel/playlist/coremodel.cue create mode 100644 pkg/coremodel/playlist/playlist_gen.go diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts index a34df96996a..4ec9a38b62f 100644 --- a/packages/grafana-schema/src/index.gen.ts +++ b/packages/grafana-schema/src/index.gen.ts @@ -74,3 +74,9 @@ export { defaultFieldConfigSource, defaultFieldConfig } from './veneer/dashboard.types'; + +// Raw generated types from playlist entity type. +export type { Playlist } from './raw/playlist/x/playlist.gen'; + +// Raw generated default consts from playlist entity type. +export { defaultPlaylist } from './raw/playlist/x/playlist.gen'; diff --git a/packages/grafana-schema/src/raw/playlist/x/playlist.gen.ts b/packages/grafana-schema/src/raw/playlist/x/playlist.gen.ts new file mode 100644 index 00000000000..bfd1c192b1b --- /dev/null +++ b/packages/grafana-schema/src/raw/playlist/x/playlist.gen.ts @@ -0,0 +1,69 @@ +// This file is autogenerated. DO NOT EDIT. +// +// Generated by pkg/framework/coremodel/gen.go +// +// Derived from the Thema lineage declared in pkg/coremodel/playlist/coremodel.cue +// +// Run `make gen-cue` from repository root to regenerate. + +export interface Playlist { + /** + * Unique playlist identifier for internal use, set by Grafana. + */ + id: number; + /** + * 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. + */ + items?: Array<{ + /** + * FIXME: The prefixDropper removes playlist from playlist_id, that doesn't work for us since it'll mean we'll have Id twice. + * ID of the playlist item for internal use by Grafana. Deprecated. + */ + id: number; + /** + * PlaylistID for the playlist containing the item. Deprecated. + */ + playlistid: number; + /** + * 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. + * - 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. + */ + value: string; + /** + * Title is the human-readable identifier for the playlist item. + */ + title: string; + /** + * Order is the position in the list for the item. Deprecated. + */ + order: number; + }>; + /** + * 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 = { + interval: '5m', + items: [], +}; diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index 281f93af860..3909929a7ca 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" + cmplaylist "github.com/grafana/grafana/pkg/coremodel/playlist" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/playlist" "github.com/grafana/grafana/pkg/web" @@ -83,13 +84,13 @@ func (hs *HTTPServer) GetPlaylist(c *models.ReqContext) response.Response { playlistDTOs, _ := hs.LoadPlaylistItemDTOs(c.Req.Context(), uid, c.OrgID) dto := &playlist.PlaylistDTO{ - Id: p.Id, - UID: p.UID, - Name: p.Name, - Interval: p.Interval, - OrgId: p.OrgId, - Items: playlistDTOs, + OrgId: p.OrgId, } + dto.Id = p.Id + dto.Uid = p.UID + dto.Name = p.Name + dto.Interval = p.Interval + dto.Items = &playlistDTOs return response.JSON(http.StatusOK, dto) } @@ -106,8 +107,8 @@ func (hs *HTTPServer) LoadPlaylistItemDTOs(ctx context.Context, uid string, orgI for _, item := range playlistitems { playlistDTOs = append(playlistDTOs, playlist.PlaylistItemDTO{ Id: item.Id, - PlaylistId: item.PlaylistId, - Type: item.Type, + Playlistid: item.PlaylistId, + Type: cmplaylist.PlaylistItemType(item.Type), Value: item.Value, Order: item.Order, Title: item.Title, @@ -244,7 +245,7 @@ func (hs *HTTPServer) UpdatePlaylist(c *models.ReqContext) response.Response { return response.Error(500, "Failed to save playlist", err) } - p.Items = playlistDTOs + p.Items = &playlistDTOs return response.JSON(http.StatusOK, p) } diff --git a/pkg/coremodel/playlist/coremodel.cue b/pkg/coremodel/playlist/coremodel.cue new file mode 100644 index 00000000000..15a1a3c2422 --- /dev/null +++ b/pkg/coremodel/playlist/coremodel.cue @@ -0,0 +1,54 @@ +package playlist + +import ( + "github.com/grafana/thema" +) + +thema.#Lineage +name: "playlist" +seqs: [ + { + schemas: [ + {//0.0 + // Unique playlist identifier for internal use, set by Grafana. + id: int64 @grafana(decisionNeeded) + // 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. + items?: [...#PlaylistItem] + + /////////////////////////////////////// + // Definitions (referenced above) are declared below + + #PlaylistItem: { + // FIXME: The prefixDropper removes playlist from playlist_id, that doesn't work for us since it'll mean we'll have Id twice. + // ID of the playlist item for internal use by Grafana. Deprecated. + id: int64 @grafana(decisionNeeded) + // PlaylistID for the playlist containing the item. Deprecated. + playlistid: int64 @grafana(decisionNeeded) + + // 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. + // - 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. + value: string + // Title is the human-readable identifier for the playlist item. + title: string @grafana(decisionNeeded) + // Order is the position in the list for the item. Deprecated. + order: int64 | *0 @grafana(decisionNeeded) + } + } + ] + } +] diff --git a/pkg/coremodel/playlist/playlist_gen.go b/pkg/coremodel/playlist/playlist_gen.go new file mode 100644 index 00000000000..d885aef5c8b --- /dev/null +++ b/pkg/coremodel/playlist/playlist_gen.go @@ -0,0 +1,147 @@ +// This file is autogenerated. DO NOT EDIT. +// +// Generated by pkg/framework/coremodel/gen.go +// +// Derived from the Thema lineage declared in pkg/coremodel/playlist/coremodel.cue +// +// Run `make gen-cue` from repository root to regenerate. + +package playlist + +import ( + "embed" + "path/filepath" + + "github.com/grafana/grafana/pkg/cuectx" + "github.com/grafana/grafana/pkg/framework/coremodel" + "github.com/grafana/thema" +) + +// Defines values for PlaylistItemType. +const ( + PlaylistItemTypeDashboardById PlaylistItemType = "dashboard_by_id" + + PlaylistItemTypeDashboardByTag PlaylistItemType = "dashboard_by_tag" + + PlaylistItemTypeDashboardByUid PlaylistItemType = "dashboard_by_uid" +) + +// Model is the Go representation of a playlist. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type Model struct { + // Unique playlist identifier for internal use, set by Grafana. + Id int64 `json:"id"` + + // 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. + Items *[]PlaylistItem `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"` +} + +// PlaylistItem is the Go representation of a playlist.Item. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type PlaylistItem struct { + // FIXME: The prefixDropper removes playlist from playlist_id, that doesn't work for us since it'll mean we'll have Id twice. + // ID of the playlist item for internal use by Grafana. Deprecated. + Id int64 `json:"id"` + + // Order is the position in the list for the item. Deprecated. + Order int `json:"order"` + + // ID for the playlist containing the item. Deprecated. + Playlistid int64 `json:"playlistid"` + + // Title is the human-readable identifier for the playlist item. + Title string `json:"title"` + + // Type of the item. + Type PlaylistItemType `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. + // - 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. + Value string `json:"value"` +} + +// Type of the item. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type PlaylistItemType string + +//go:embed coremodel.cue +var cueFS embed.FS + +// The current version of the coremodel schema, as declared in coremodel.cue. +// This version determines what schema version is returned from [Coremodel.CurrentSchema], +// and which schema version is used for code generation within the grafana/grafana repository. +// +// The code generator ensures that this is always the latest Thema schema version. +var currentVersion = thema.SV(0, 0) + +// Lineage returns the Thema lineage representing a Grafana playlist. +// +// The lineage is the canonical specification of the current playlist schema, +// all prior schema versions, and the mappings that allow migration between +// schema versions. +func Lineage(lib thema.Library, opts ...thema.BindOption) (thema.Lineage, error) { + return cuectx.LoadGrafanaInstancesWithThema(filepath.Join("pkg", "coremodel", "playlist"), cueFS, lib, opts...) +} + +var _ thema.LineageFactory = Lineage +var _ coremodel.Interface = &Coremodel{} + +// Coremodel contains the foundational schema declaration for playlists. +// It implements coremodel.Interface. +type Coremodel struct { + lin thema.Lineage +} + +// Lineage returns the canonical playlist Lineage. +func (c *Coremodel) Lineage() thema.Lineage { + return c.lin +} + +// CurrentSchema returns the current (latest) playlist Thema schema. +func (c *Coremodel) CurrentSchema() thema.Schema { + return thema.SchemaP(c.lin, currentVersion) +} + +// GoType returns a pointer to an empty Go struct that corresponds to +// the current Thema schema. +func (c *Coremodel) GoType() interface{} { + return &Model{} +} + +// New returns a new instance of the playlist coremodel. +// +// Note that this function does not cache, and initially loading a Thema lineage +// can be expensive. As such, the Grafana backend should prefer to access this +// coremodel through a registry (pkg/framework/coremodel/registry), which does cache. +func New(lib thema.Library) (*Coremodel, error) { + lin, err := Lineage(lib) + if err != nil { + return nil, err + } + + return &Coremodel{ + lin: lin, + }, nil +} diff --git a/pkg/framework/coremodel/registry/registry_gen.go b/pkg/framework/coremodel/registry/registry_gen.go index c198ef8647f..74ac7b9d88d 100644 --- a/pkg/framework/coremodel/registry/registry_gen.go +++ b/pkg/framework/coremodel/registry/registry_gen.go @@ -10,6 +10,7 @@ import ( "fmt" "github.com/grafana/grafana/pkg/coremodel/dashboard" + "github.com/grafana/grafana/pkg/coremodel/playlist" "github.com/grafana/grafana/pkg/coremodel/pluginmeta" "github.com/grafana/grafana/pkg/framework/coremodel" "github.com/grafana/thema" @@ -27,12 +28,14 @@ import ( type Base struct { all []coremodel.Interface dashboard *dashboard.Coremodel + playlist *playlist.Coremodel pluginmeta *pluginmeta.Coremodel } // type guards var ( _ coremodel.Interface = &dashboard.Coremodel{} + _ coremodel.Interface = &playlist.Coremodel{} _ coremodel.Interface = &pluginmeta.Coremodel{} ) @@ -42,6 +45,12 @@ func (b *Base) Dashboard() *dashboard.Coremodel { return b.dashboard } +// Playlist returns the playlist coremodel. The return value is guaranteed to +// implement coremodel.Interface. +func (b *Base) Playlist() *playlist.Coremodel { + return b.playlist +} + // Pluginmeta returns the pluginmeta coremodel. The return value is guaranteed to // implement coremodel.Interface. func (b *Base) Pluginmeta() *pluginmeta.Coremodel { @@ -58,6 +67,12 @@ func doProvideBase(lib thema.Library) *Base { } reg.all = append(reg.all, reg.dashboard) + reg.playlist, err = playlist.New(lib) + if err != nil { + panic(fmt.Sprintf("error while initializing playlist coremodel: %s", err)) + } + reg.all = append(reg.all, reg.playlist) + reg.pluginmeta, err = pluginmeta.New(lib) if err != nil { panic(fmt.Sprintf("error while initializing pluginmeta coremodel: %s", err)) diff --git a/pkg/services/playlist/model.go b/pkg/services/playlist/model.go index d65e47479a7..40e7305dd93 100644 --- a/pkg/services/playlist/model.go +++ b/pkg/services/playlist/model.go @@ -2,6 +2,8 @@ package playlist import ( "errors" + + "github.com/grafana/grafana/pkg/coremodel/playlist" ) // Typed errors @@ -21,22 +23,11 @@ type Playlist struct { } type PlaylistDTO struct { - Id int64 `json:"id"` - UID string `json:"uid"` - Name string `json:"name"` - Interval string `json:"interval"` - OrgId int64 `json:"-"` - Items []PlaylistItemDTO `json:"items"` + playlist.Model + OrgId int64 `json:"-"` } -type PlaylistItemDTO struct { - Id int64 `json:"id"` - PlaylistId int64 `json:"playlistid"` - Type string `json:"type"` - Title string `json:"title"` - Value string `json:"value"` - Order int `json:"order"` -} +type PlaylistItemDTO = playlist.PlaylistItem type PlaylistItem struct { Id int64 `db:"id"` diff --git a/pkg/services/playlist/playlistimpl/sqlx_store.go b/pkg/services/playlist/playlistimpl/sqlx_store.go index 230efb54bd6..34a4d1fe882 100644 --- a/pkg/services/playlist/playlistimpl/sqlx_store.go +++ b/pkg/services/playlist/playlistimpl/sqlx_store.go @@ -42,7 +42,7 @@ func (s *sqlxStore) Insert(ctx context.Context, cmd *playlist.CreatePlaylistComm for _, item := range cmd.Items { playlistItems = append(playlistItems, playlist.PlaylistItem{ PlaylistId: p.Id, - Type: item.Type, + Type: string(item.Type), Value: item.Value, Order: item.Order, Title: item.Title, @@ -94,7 +94,7 @@ func (s *sqlxStore) Update(ctx context.Context, cmd *playlist.UpdatePlaylistComm for index, item := range cmd.Items { playlistItems = append(playlistItems, playlist.PlaylistItem{ PlaylistId: p.Id, - Type: item.Type, + Type: string(item.Type), Value: item.Value, Order: index + 1, Title: item.Title, diff --git a/pkg/services/playlist/playlistimpl/xorm_store.go b/pkg/services/playlist/playlistimpl/xorm_store.go index 1a51bd25b51..bb75c1b9900 100644 --- a/pkg/services/playlist/playlistimpl/xorm_store.go +++ b/pkg/services/playlist/playlistimpl/xorm_store.go @@ -38,7 +38,7 @@ func (s *sqlStore) Insert(ctx context.Context, cmd *playlist.CreatePlaylistComma for _, item := range cmd.Items { playlistItems = append(playlistItems, playlist.PlaylistItem{ PlaylistId: p.Id, - Type: item.Type, + Type: string(item.Type), Value: item.Value, Order: item.Order, Title: item.Title, @@ -70,13 +70,12 @@ func (s *sqlStore) Update(ctx context.Context, cmd *playlist.UpdatePlaylistComma p.Id = existingPlaylist.Id dto = playlist.PlaylistDTO{ - - Id: p.Id, - UID: p.UID, - OrgId: p.OrgId, - Name: p.Name, - Interval: p.Interval, + OrgId: p.OrgId, } + dto.Id = p.Id + dto.Uid = p.UID + dto.Name = p.Name + dto.Interval = p.Interval _, err = sess.Where("id=?", p.Id).Cols("name", "interval").Update(&p) if err != nil { @@ -95,7 +94,7 @@ func (s *sqlStore) Update(ctx context.Context, cmd *playlist.UpdatePlaylistComma for index, item := range cmd.Items { playlistItems = append(playlistItems, models.PlaylistItem{ PlaylistId: p.Id, - Type: item.Type, + Type: string(item.Type), Value: item.Value, Order: index + 1, Title: item.Title,