LibraryPanels: Support CRUD via apiserver (#113035)
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type LibraryPanel struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// Standard object's metadata
|
||||
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
|
||||
// +optional
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Panel properties
|
||||
Spec LibraryPanelSpec `json:"spec"`
|
||||
|
||||
// Status will show errors
|
||||
Status *LibraryPanelStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type LibraryPanelList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// +optional
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []LibraryPanel `json:"items"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
type LibraryPanelSpec struct {
|
||||
// The panel type
|
||||
Type string `json:"type"`
|
||||
|
||||
// The panel type
|
||||
PluginVersion string `json:"pluginVersion,omitempty"`
|
||||
|
||||
// The title of the library panel
|
||||
Title string `json:"title,omitempty"`
|
||||
|
||||
// The title of the panel when displayed in the dashboard
|
||||
PanelTitle string `json:"panelTitle,omitempty"`
|
||||
|
||||
// Library panel description
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
// The options schema depends on the panel type
|
||||
Options common.Unstructured `json:"options"`
|
||||
|
||||
// The fieldConfig schema depends on the panel type
|
||||
FieldConfig common.Unstructured `json:"fieldConfig"`
|
||||
|
||||
// The default datasource type
|
||||
Datasource *data.DataSourceRef `json:"datasource,omitempty"`
|
||||
|
||||
// The grid position
|
||||
GridPos GridPos `json:"gridPos,omitempty"`
|
||||
|
||||
// Whether the panel is transparent
|
||||
Transparent bool `json:"transparent,omitempty"`
|
||||
|
||||
// The links for the panel
|
||||
Links []common.Unstructured `json:"links,omitempty"`
|
||||
|
||||
// The datasource queries
|
||||
// +listType=atomic
|
||||
Targets []data.DataQuery `json:"targets,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
type GridPos struct {
|
||||
W int `json:"w"`
|
||||
H int `json:"h"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
type LibraryPanelStatus struct {
|
||||
// Translation warnings (mostly things that were in SQL columns but not found in the saved body)
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
|
||||
// The properties previously stored in SQL that are not included in this model
|
||||
Missing common.Unstructured `json:"missing,omitempty"`
|
||||
}
|
||||
@@ -1,95 +1,5 @@
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type LibraryPanel struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// Standard object's metadata
|
||||
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
|
||||
// +optional
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Panel properties
|
||||
Spec LibraryPanelSpec `json:"spec"`
|
||||
|
||||
// Status will show errors
|
||||
Status *LibraryPanelStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type LibraryPanelList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
// +optional
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []LibraryPanel `json:"items"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
type LibraryPanelSpec struct {
|
||||
// The panel type
|
||||
Type string `json:"type"`
|
||||
|
||||
// The panel type
|
||||
PluginVersion string `json:"pluginVersion,omitempty"`
|
||||
|
||||
// The title of the library panel
|
||||
Title string `json:"title,omitempty"`
|
||||
|
||||
// The title of the panel when displayed in the dashboard
|
||||
PanelTitle string `json:"panelTitle,omitempty"`
|
||||
|
||||
// Library panel description
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
// The options schema depends on the panel type
|
||||
Options common.Unstructured `json:"options"`
|
||||
|
||||
// The fieldConfig schema depends on the panel type
|
||||
FieldConfig common.Unstructured `json:"fieldConfig"`
|
||||
|
||||
// The default datasource type
|
||||
Datasource *data.DataSourceRef `json:"datasource,omitempty"`
|
||||
|
||||
// The grid position
|
||||
GridPos GridPos `json:"gridPos,omitempty"`
|
||||
|
||||
// Whether the panel is transparent
|
||||
Transparent bool `json:"transparent,omitempty"`
|
||||
|
||||
// The links for the panel
|
||||
Links []common.Unstructured `json:"links,omitempty"`
|
||||
|
||||
// The datasource queries
|
||||
// +listType=set
|
||||
Targets []data.DataQuery `json:"targets,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
type GridPos struct {
|
||||
W int `json:"w"`
|
||||
H int `json:"h"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
type LibraryPanelStatus struct {
|
||||
// Translation warnings (mostly things that were in SQL columns but not found in the saved body)
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
|
||||
// The properties previously stored in SQL that are not included in this model
|
||||
Missing common.Unstructured `json:"missing,omitempty"`
|
||||
}
|
||||
|
||||
// This is like the legacy DTO where access and metadata are all returned in a single call
|
||||
// +k8s:deepcopy-gen=true
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
@@ -882,7 +882,7 @@ func schema_pkg_apis_dashboard_v0alpha1_LibraryPanelSpec(ref common.ReferenceCal
|
||||
"targets": {
|
||||
VendorExtensible: spec.VendorExtensible{
|
||||
Extensions: spec.Extensions{
|
||||
"x-kubernetes-list-type": "set",
|
||||
"x-kubernetes-list-type": "atomic",
|
||||
},
|
||||
},
|
||||
SchemaProps: spec.SchemaProps{
|
||||
|
||||
@@ -52,7 +52,7 @@ func (u *Unstructured) SetUnstructuredContent(content map[string]any) {
|
||||
|
||||
// MarshalJSON ensures that the unstructured object produces proper
|
||||
// JSON when passed to Go's standard JSON library.
|
||||
func (u *Unstructured) MarshalJSON() ([]byte, error) {
|
||||
func (u Unstructured) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(u.Object)
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,10 @@ var serviceIdentityPermissions = getWildcardPermissions(
|
||||
"datasources:query",
|
||||
"datasources:read",
|
||||
"datasources:delete",
|
||||
"library.panels:create", // ActionLibraryPanelsCreate
|
||||
"library.panels:read", // ActionLibraryPanelsRead
|
||||
"library.panels:write", // ActionLibraryPanelsWrite
|
||||
"library.panels:delete", // ActionLibraryPanelsDelete
|
||||
"alert.provisioning:write",
|
||||
"alert.provisioning.secrets:read",
|
||||
"users:read", // accesscontrol.ActionUsersRead,
|
||||
|
||||
@@ -5,16 +5,17 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"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"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/dashboard/legacy"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -29,9 +30,9 @@ var (
|
||||
)
|
||||
|
||||
type LibraryPanelStore struct {
|
||||
Access legacy.DashboardAccess
|
||||
ResourceInfo utils.ResourceInfo
|
||||
AccessControl accesscontrol.AccessControl
|
||||
Access legacy.DashboardAccess
|
||||
ResourceInfo utils.ResourceInfo
|
||||
service libraryelements.Service
|
||||
}
|
||||
|
||||
func (s *LibraryPanelStore) New() runtime.Object {
|
||||
@@ -57,15 +58,70 @@ func (s *LibraryPanelStore) ConvertToTable(ctx context.Context, object runtime.O
|
||||
}
|
||||
|
||||
func (s *LibraryPanelStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||
return nil, fmt.Errorf("method not yet implemented")
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd, err := libraryelements.ToCreateLibraryElementCommand(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// NOTE: this includes all access control checks
|
||||
out, err := s.service.CreateElement(ctx, user, *cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out.UID == "" {
|
||||
return nil, fmt.Errorf("created library panel has empty UID")
|
||||
}
|
||||
return s.Get(ctx, out.UID, &metav1.GetOptions{})
|
||||
}
|
||||
|
||||
func (s *LibraryPanelStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
return nil, false, fmt.Errorf("method not yet implemented")
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
old, err := s.Get(ctx, name, &metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// NOTE: this includes all access control checks
|
||||
obj, err := objInfo.UpdatedObject(ctx, old)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
cmd, err := libraryelements.ToPatchLibraryElementCommand(obj)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
out, err := s.service.PatchLibraryElement(ctx, user, *cmd, name)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if out.UID == "" {
|
||||
return nil, false, fmt.Errorf("created library panel has empty UID")
|
||||
}
|
||||
obj, err = s.Get(ctx, out.UID, &metav1.GetOptions{})
|
||||
return obj, false, err
|
||||
}
|
||||
|
||||
func (s *LibraryPanelStore) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
return nil, false, fmt.Errorf("method not yet implemented")
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// NOTE: this includes all access control checks
|
||||
_, err = s.service.DeleteLibraryElement(ctx, user, name)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
func (s *LibraryPanelStore) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
|
||||
|
||||
@@ -26,6 +26,19 @@ func (b *DashboardsAPIBuilder) Mutate(ctx context.Context, a admission.Attribute
|
||||
if op != admission.Create && op != admission.Update {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch a.GetResource().Resource {
|
||||
case dashboardV0.DASHBOARD_RESOURCE:
|
||||
return b.mutateDashboard(ctx, a)
|
||||
|
||||
case dashboardV0.LIBRARY_PANEL_RESOURCE:
|
||||
return nil // nothing needed
|
||||
}
|
||||
|
||||
return fmt.Errorf("unexpected resource: %+v", a.GetResource())
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) mutateDashboard(ctx context.Context, a admission.Attributes) (err error) {
|
||||
var internalID int64
|
||||
obj := a.GetObject()
|
||||
meta, err := utils.MetaAccessor(obj)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
|
||||
dashv0 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
@@ -180,10 +179,10 @@ func TestDashboardAPIBuilder_Mutate(t *testing.T) {
|
||||
err := b.Mutate(context.Background(), admission.NewAttributesRecord(
|
||||
tt.inputObj,
|
||||
nil,
|
||||
schema.GroupVersionKind{},
|
||||
dashv1.DashboardResourceInfo.GroupVersionKind(),
|
||||
"",
|
||||
"test",
|
||||
schema.GroupVersionResource{},
|
||||
dashv1.DashboardResourceInfo.GroupVersionResource(),
|
||||
"",
|
||||
tt.operation,
|
||||
operationOptions,
|
||||
|
||||
@@ -50,6 +50,7 @@ import (
|
||||
dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements"
|
||||
"github.com/grafana/grafana/pkg/services/librarypanels"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
@@ -68,6 +69,8 @@ var (
|
||||
_ builder.APIGroupVersionsProvider = (*DashboardsAPIBuilder)(nil)
|
||||
_ builder.OpenAPIPostProcessor = (*DashboardsAPIBuilder)(nil)
|
||||
_ builder.APIGroupRouteProvider = (*DashboardsAPIBuilder)(nil)
|
||||
_ builder.APIGroupMutation = (*DashboardsAPIBuilder)(nil)
|
||||
_ builder.APIGroupValidation = (*DashboardsAPIBuilder)(nil)
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -108,6 +111,7 @@ type DashboardsAPIBuilder struct {
|
||||
minRefreshInterval string
|
||||
dualWriter dualwrite.Service
|
||||
folderClientProvider client.K8sHandlerProvider
|
||||
libraryPanels libraryelements.Service // for legacy library panels
|
||||
|
||||
isStandalone bool // skips any handling including anything to do with legacy storage
|
||||
}
|
||||
@@ -135,6 +139,7 @@ func RegisterAPIService(
|
||||
libraryPanelSvc librarypanels.Service,
|
||||
restConfigProvider apiserver.RestConfigProvider,
|
||||
userService user.Service,
|
||||
libraryPanels libraryelements.Service,
|
||||
) *DashboardsAPIBuilder {
|
||||
dbp := legacysql.NewDatabaseProvider(sql)
|
||||
namespacer := request.GetNamespaceMapper(cfg)
|
||||
@@ -157,6 +162,7 @@ func RegisterAPIService(
|
||||
minRefreshInterval: cfg.MinRefreshInterval,
|
||||
dualWriter: dual,
|
||||
folderClientProvider: newSimpleFolderClientProvider(folderClient),
|
||||
libraryPanels: libraryPanels,
|
||||
|
||||
legacy: &DashboardStorage{
|
||||
Access: legacy.NewDashboardAccess(dbp, namespacer, dashStore, provisioning, libraryPanelSvc, sorter, dashboardPermissionsSvc, accessControl, features),
|
||||
@@ -229,26 +235,35 @@ func (b *DashboardsAPIBuilder) InstallSchema(scheme *runtime.Scheme) error {
|
||||
}
|
||||
|
||||
func (b *DashboardsAPIBuilder) AllowedV0Alpha1Resources() []string {
|
||||
return []string{dashv0.DashboardKind().Plural()}
|
||||
return []string{
|
||||
dashv0.DashboardKind().Plural(),
|
||||
dashv0.LIBRARY_PANEL_RESOURCE,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates dashboard operations for the apiserver
|
||||
func (b *DashboardsAPIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
|
||||
op := a.GetOperation()
|
||||
|
||||
// Handle different operations
|
||||
switch op {
|
||||
case admission.Delete:
|
||||
return b.validateDelete(ctx, a)
|
||||
case admission.Create:
|
||||
return b.validateCreate(ctx, a, o)
|
||||
case admission.Update:
|
||||
return b.validateUpdate(ctx, a, o)
|
||||
case admission.Connect:
|
||||
return nil
|
||||
switch a.GetResource().Resource {
|
||||
case dashv0.DASHBOARD_RESOURCE:
|
||||
// Handle different operations
|
||||
switch op {
|
||||
case admission.Delete:
|
||||
return b.validateDelete(ctx, a)
|
||||
case admission.Create:
|
||||
return b.validateCreate(ctx, a, o)
|
||||
case admission.Update:
|
||||
return b.validateUpdate(ctx, a, o)
|
||||
case admission.Connect:
|
||||
return nil
|
||||
}
|
||||
|
||||
case dashv0.LIBRARY_PANEL_RESOURCE:
|
||||
return nil // OK for now
|
||||
}
|
||||
|
||||
return nil
|
||||
return fmt.Errorf("unsupported validation: %+v", a.GetResource())
|
||||
}
|
||||
|
||||
// validateDelete checks if a dashboard can be deleted
|
||||
@@ -642,12 +657,13 @@ func (b *DashboardsAPIBuilder) storageForVersion(
|
||||
return err
|
||||
}
|
||||
|
||||
// Expose read only library panels
|
||||
if libraryPanels != nil {
|
||||
// Expose read library panels
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if libraryPanels != nil && b.features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) {
|
||||
legacyLibraryStore := &LibraryPanelStore{
|
||||
Access: b.legacy.Access,
|
||||
ResourceInfo: *libraryPanels,
|
||||
AccessControl: b.accessControl,
|
||||
Access: b.legacy.Access,
|
||||
ResourceInfo: *libraryPanels,
|
||||
service: b.libraryPanels,
|
||||
}
|
||||
|
||||
unifiedLibraryStore, err := grafanaregistry.NewRegistryStore(opts.Scheme, *libraryPanels, opts.OptsGetter)
|
||||
|
||||
@@ -840,7 +840,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)
|
||||
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)
|
||||
snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer)
|
||||
dataSourceAPIBuilder, err := datasource.RegisterAPIService(configProvider, featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer)
|
||||
if err != nil {
|
||||
@@ -1470,7 +1470,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)
|
||||
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)
|
||||
snapshotsAPIBuilder := dashboardsnapshot.RegisterAPIService(serviceImpl, apiserverService, cfg, featureToggles, sqlStore, registerer)
|
||||
dataSourceAPIBuilder, err := datasource.RegisterAPIService(configProvider, featureToggles, apiserverService, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, accessControl, registerer)
|
||||
if err != nil {
|
||||
|
||||
@@ -87,7 +87,7 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon
|
||||
}
|
||||
}
|
||||
|
||||
element, err := l.createLibraryElement(c.Req.Context(), c.SignedInUser, cmd)
|
||||
element, err := l.CreateElement(c.Req.Context(), c.SignedInUser, cmd)
|
||||
if err != nil {
|
||||
return l.toLibraryElementError(err, "Failed to create library element")
|
||||
}
|
||||
@@ -124,7 +124,7 @@ func (l *LibraryElementService) createHandler(c *contextmodel.ReqContext) respon
|
||||
// 404: notFoundError
|
||||
// 500: internalServerError
|
||||
func (l *LibraryElementService) deleteHandler(c *contextmodel.ReqContext) response.Response {
|
||||
id, err := l.deleteLibraryElement(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"])
|
||||
id, err := l.DeleteLibraryElement(c.Req.Context(), c.SignedInUser, web.Params(c.Req)[":uid"])
|
||||
if err != nil {
|
||||
return l.toLibraryElementError(err, "Failed to delete library element")
|
||||
}
|
||||
@@ -252,7 +252,7 @@ func (l *LibraryElementService) patchHandler(c *contextmodel.ReqContext) respons
|
||||
}
|
||||
}
|
||||
|
||||
element, err := l.patchLibraryElement(c.Req.Context(), c.SignedInUser, cmd, web.Params(c.Req)[":uid"])
|
||||
element, err := l.PatchLibraryElement(c.Req.Context(), c.SignedInUser, cmd, web.Params(c.Req)[":uid"])
|
||||
if err != nil {
|
||||
return l.toLibraryElementError(err, "Failed to update library element")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func ToCreateLibraryElementCommand(raw runtime.Object) (*model.CreateLibraryElementCommand, error) {
|
||||
obj, err := utils.MetaAccessor(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
folder := obj.GetFolder()
|
||||
cmd := &model.CreateLibraryElementCommand{
|
||||
UID: obj.GetName(),
|
||||
FolderUID: &folder,
|
||||
Kind: 1, // the only kind... LibraryPanel
|
||||
Name: obj.FindTitle("library panel"),
|
||||
}
|
||||
if cmd.UID == "" {
|
||||
if obj.GetGenerateName() == "" {
|
||||
return nil, fmt.Errorf("expecting either name or generateName property")
|
||||
}
|
||||
cmd.UID = obj.GetGenerateName() + util.GenerateShortUID()
|
||||
}
|
||||
cmd.Model, err = toRawMessage(raw)
|
||||
return cmd, err
|
||||
}
|
||||
|
||||
func ToPatchLibraryElementCommand(raw runtime.Object) (*model.PatchLibraryElementCommand, error) {
|
||||
obj, err := utils.MetaAccessor(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
folder := obj.GetFolder()
|
||||
cmd := &model.PatchLibraryElementCommand{
|
||||
UID: obj.GetName(),
|
||||
FolderUID: &folder,
|
||||
Kind: 1, // the only kind... LibraryPanel
|
||||
Name: obj.FindTitle("library panel"),
|
||||
}
|
||||
cmd.Model, err = toRawMessage(raw)
|
||||
return cmd, err
|
||||
}
|
||||
|
||||
func toRawMessage(raw runtime.Object) (json.RawMessage, error) {
|
||||
switch obj := raw.(type) {
|
||||
case *v0alpha1.LibraryPanel:
|
||||
return json.Marshal(obj.Spec)
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported library panel type: %T", raw)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package libraryelements
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
data "github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||
"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/utils"
|
||||
"github.com/grafana/grafana/pkg/services/libraryelements/model"
|
||||
)
|
||||
|
||||
func TestConversionsCommands(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
input runtime.Object
|
||||
expectedCreate *model.CreateLibraryElementCommand
|
||||
expectedPatch *model.PatchLibraryElementCommand
|
||||
}{
|
||||
{
|
||||
name: "basic conversion",
|
||||
input: &v0alpha1.LibraryPanel{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "uid",
|
||||
Annotations: map[string]string{
|
||||
utils.AnnoKeyFolder: "aaa",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.LibraryPanelSpec{
|
||||
Type: "timeseries",
|
||||
PluginVersion: "1.2.3",
|
||||
Title: "title",
|
||||
Description: "descr",
|
||||
Options: common.Unstructured{
|
||||
Object: map[string]any{
|
||||
"hello": "options",
|
||||
},
|
||||
},
|
||||
FieldConfig: common.Unstructured{
|
||||
Object: map[string]any{
|
||||
"hello": "fieldConfig",
|
||||
},
|
||||
},
|
||||
PanelTitle: "panel title",
|
||||
GridPos: v0alpha1.GridPos{
|
||||
W: 1, H: 2, X: 3, Y: 4,
|
||||
},
|
||||
Transparent: true,
|
||||
Links: []common.Unstructured{{
|
||||
Object: map[string]any{
|
||||
"link1": "hello",
|
||||
},
|
||||
}},
|
||||
Datasource: &data.DataSourceRef{
|
||||
UID: "uid",
|
||||
Type: "ttt",
|
||||
APIVersion: "v0alpha1",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedCreate: &model.CreateLibraryElementCommand{
|
||||
FolderUID: ptr.To("aaa"),
|
||||
UID: "uid",
|
||||
Name: "title",
|
||||
Kind: 1,
|
||||
Model: json.RawMessage(`{"type":"timeseries","pluginVersion":"1.2.3","title":"title","panelTitle":"panel title","description":"descr","options":{"hello":"options"},"fieldConfig":{"hello":"fieldConfig"},"datasource":{"type":"ttt","uid":"uid","apiVersion":"v0alpha1"},"gridPos":{"w":1,"h":2,"x":3,"y":4},"transparent":true,"links":[{"link1":"hello"}]}`),
|
||||
},
|
||||
expectedPatch: &model.PatchLibraryElementCommand{
|
||||
FolderUID: ptr.To("aaa"),
|
||||
UID: "uid",
|
||||
Name: "title",
|
||||
Kind: 1,
|
||||
Version: 0,
|
||||
Model: json.RawMessage(`{"type":"timeseries","pluginVersion":"1.2.3","title":"title","panelTitle":"panel title","description":"descr","options":{"hello":"options"},"fieldConfig":{"hello":"fieldConfig"},"datasource":{"type":"ttt","uid":"uid","apiVersion":"v0alpha1"},"gridPos":{"w":1,"h":2,"x":3,"y":4},"transparent":true,"links":[{"link1":"hello"}]}`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
create, err := ToCreateLibraryElementCommand(tt.input)
|
||||
require.NoError(t, err)
|
||||
if diff := cmp.Diff(tt.expectedCreate, create); diff != "" {
|
||||
require.FailNowf(t, "Create mismatch (-want +got):%s", diff)
|
||||
}
|
||||
|
||||
patch, err := ToPatchLibraryElementCommand(tt.input)
|
||||
require.NoError(t, err)
|
||||
if diff := cmp.Diff(tt.expectedPatch, patch); diff != "" {
|
||||
require.FailNowf(t, "Path mismatch (-want +got):%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ func (l *LibraryElementService) GetLibraryElement(c context.Context, signedInUse
|
||||
}
|
||||
|
||||
// createLibraryElement adds a library element.
|
||||
func (l *LibraryElementService) createLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) {
|
||||
func (l *LibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) {
|
||||
if err := l.requireSupportedElementKind(cmd.Kind); err != nil {
|
||||
return model.LibraryElementDTO{}, err
|
||||
}
|
||||
@@ -169,7 +169,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn
|
||||
err = l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
||||
allowed, err := l.AccessControl.Evaluate(c, signedInUser, ac.EvalPermission(ActionLibraryPanelsCreate, dashboards.ScopeFoldersProvider.GetResourceScopeUID(folderUID)))
|
||||
if !allowed {
|
||||
return fmt.Errorf("insufficient permissions for creating library panel in folder with UID %s", folderUID)
|
||||
return fmt.Errorf("insufficient permissions for creating library panel in folder with UID: '%s'", folderUID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -216,7 +216,7 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn
|
||||
}
|
||||
|
||||
// deleteLibraryElement deletes a library element.
|
||||
func (l *LibraryElementService) deleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) {
|
||||
func (l *LibraryElementService) DeleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) {
|
||||
var elementID int64
|
||||
err := l.SQLStore.WithTransactionalDbSession(c, func(session *db.Session) error {
|
||||
element, err := l.GetLibraryElement(c, signedInUser, session, uid)
|
||||
@@ -578,7 +578,7 @@ func (l *LibraryElementService) handleFolderIDPatches(ctx context.Context, eleme
|
||||
}
|
||||
|
||||
// patchLibraryElement updates a Library Element.
|
||||
func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) {
|
||||
func (l *LibraryElementService) PatchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) {
|
||||
var dto model.LibraryElementDTO
|
||||
if err := l.requireSupportedElementKind(cmd.Kind); err != nil {
|
||||
return model.LibraryElementDTO{}, err
|
||||
|
||||
@@ -17,6 +17,16 @@ type LibraryElementService struct {
|
||||
idCounter int64
|
||||
}
|
||||
|
||||
// DeleteLibraryElement implements libraryelements.Service.
|
||||
func (l *LibraryElementService) DeleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// PatchLibraryElement implements libraryelements.Service.
|
||||
func (l *LibraryElementService) PatchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
var _ libraryelements.Service = (*LibraryElementService)(nil)
|
||||
|
||||
func (l *LibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) {
|
||||
@@ -68,6 +78,11 @@ func (l *LibraryElementService) CreateElement(c context.Context, signedInUser id
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
// PatchElement implements libraryelements.Service.
|
||||
func (l *LibraryElementService) PatchElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func (l *LibraryElementService) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) {
|
||||
l.mx.RLock()
|
||||
defer l.mx.RUnlock()
|
||||
|
||||
@@ -2,13 +2,13 @@ package libraryelements
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user/usertest"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestUnstructuredToLegacyLibraryPanelDTO(t *testing.T) {
|
||||
@@ -155,26 +154,22 @@ func TestUnstructuredToLegacyLibraryPanelDTO(t *testing.T) {
|
||||
require.Equal(t, testUser.Login, result.Meta.UpdatedBy.Name)
|
||||
require.Equal(t, dtos.GetGravatarUrl(cfg, testUser.Email), result.Meta.UpdatedBy.AvatarUrl)
|
||||
|
||||
var modelMap map[string]interface{}
|
||||
err = json.Unmarshal(result.Model, &modelMap)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "testdata", modelMap["datasource"].(map[string]interface{})["type"])
|
||||
require.Equal(t, "test-datasource", modelMap["datasource"].(map[string]interface{})["uid"])
|
||||
require.Equal(t, "Test description", modelMap["description"])
|
||||
require.Equal(t, float64(123), modelMap["id"])
|
||||
require.Equal(t, "text", modelMap["type"])
|
||||
require.Equal(t, "Test Panel Title", modelMap["title"])
|
||||
require.Equal(t, "Test content", modelMap["options"].(map[string]interface{})["content"])
|
||||
require.Equal(t, true, modelMap["transparent"])
|
||||
require.Equal(t, "Test Library Panel", modelMap["libraryPanel"].(map[string]interface{})["name"])
|
||||
require.Equal(t, "test-panel-uid", modelMap["libraryPanel"].(map[string]interface{})["uid"])
|
||||
links := modelMap["links"].([]interface{})
|
||||
require.Len(t, links, 1)
|
||||
require.Equal(t, "Test Link", links[0].(map[string]interface{})["title"])
|
||||
|
||||
targets := modelMap["targets"].([]interface{})
|
||||
require.Len(t, targets, 1)
|
||||
require.Equal(t, "A", targets[0].(map[string]interface{})["refId"])
|
||||
// fmt.Printf("%s\n", result.Model)
|
||||
require.JSONEq(t, `{
|
||||
"datasource": { "type": "testdata", "uid": "test-datasource" },
|
||||
"description": "Test description",
|
||||
"fieldConfig": { "defaults": { "color": { "mode": "palette-classic" } } },
|
||||
"gridPos": { "w": 12, "h": 8, "x": 0, "y": 0 },
|
||||
"id": 123,
|
||||
"libraryPanel": { "name": "Test Library Panel", "uid": "test-panel-uid" },
|
||||
"links": [{ "title": "Test Link", "url": "https://example.com" }],
|
||||
"options": { "content": "Test content" },
|
||||
"pluginVersion": "1.0.0",
|
||||
"targets": [{ "refId": "A", "expr": "test_query" }],
|
||||
"title": "Test Panel Title",
|
||||
"transparent": true,
|
||||
"type": "text"
|
||||
}`, string(result.Model))
|
||||
|
||||
dashboardsSvc.AssertExpectations(t)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, routeRegister routing.Rout
|
||||
// Service is a service for operating on library elements.
|
||||
type Service interface {
|
||||
CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error)
|
||||
PatchLibraryElement(c context.Context, signedInUser identity.Requester, cmd model.PatchLibraryElementCommand, uid string) (model.LibraryElementDTO, error)
|
||||
DeleteLibraryElement(c context.Context, signedInUser identity.Requester, uid string) (int64, error)
|
||||
GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error)
|
||||
GetElementsForDashboard(c context.Context, dashboardID int64) (map[string]model.LibraryElementDTO, error)
|
||||
ConnectElementsToDashboard(c context.Context, signedInUser identity.Requester, elementUIDs []string, dashboardID int64) error
|
||||
@@ -63,11 +65,6 @@ type LibraryElementService struct {
|
||||
|
||||
var _ Service = (*LibraryElementService)(nil)
|
||||
|
||||
// CreateElement creates a Library Element.
|
||||
func (l *LibraryElementService) CreateElement(c context.Context, signedInUser identity.Requester, cmd model.CreateLibraryElementCommand) (model.LibraryElementDTO, error) {
|
||||
return l.createLibraryElement(c, signedInUser, cmd)
|
||||
}
|
||||
|
||||
// GetElement gets an element from a UID.
|
||||
func (l *LibraryElementService) GetElement(c context.Context, signedInUser identity.Requester, cmd model.GetLibraryElementCommand) (model.LibraryElementDTO, error) {
|
||||
return l.getLibraryElementByUid(c, signedInUser, cmd)
|
||||
|
||||
@@ -103,7 +103,7 @@ func TestIntegration_GetLibraryElement(t *testing.T) {
|
||||
newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc)
|
||||
sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll}
|
||||
sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll}
|
||||
result, err := sc.service.createLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{
|
||||
result, err := sc.service.CreateElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{
|
||||
FolderID: newFolder.ID, // nolint:staticcheck
|
||||
FolderUID: &newFolder.UID,
|
||||
Name: "Testing Library Panel With Deleted Folder",
|
||||
|
||||
@@ -150,7 +150,7 @@ func TestIntegration_GetLibraryPanelConnections(t *testing.T) {
|
||||
newFolder := createFolder(t, sc, "NewFolder", sc.folderSvc)
|
||||
sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersRead] = []string{dashboards.ScopeFoldersAll}
|
||||
sc.reqContext.Permissions[sc.reqContext.OrgID][dashboards.ActionFoldersDelete] = []string{dashboards.ScopeFoldersAll}
|
||||
_, err = sc.service.createLibraryElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{
|
||||
_, err = sc.service.CreateElement(sc.reqContext.Req.Context(), sc.reqContext.SignedInUser, model.CreateLibraryElementCommand{
|
||||
FolderID: newFolder.ID, // nolint:staticcheck
|
||||
FolderUID: &newFolder.UID,
|
||||
Name: "Testing Library Panel With Deleted Folder",
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"kind": "LibraryPanel",
|
||||
"apiVersion": "dashboard.grafana.app/v0alpha1",
|
||||
"metadata": {
|
||||
"generateName": "x"
|
||||
},
|
||||
"spec": {
|
||||
"type": "timeseries",
|
||||
"pluginVersion": "12.3.0-pre",
|
||||
"title": "example library panel",
|
||||
"panelTitle": "simple",
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "cejobd88i85j4d"
|
||||
},
|
||||
"gridPos": {
|
||||
"w": 12,
|
||||
"h": 8,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "cejobd88i85j4d"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"kind": "LibraryPanel",
|
||||
"apiVersion": "dashboard.grafana.app/v0alpha1",
|
||||
"metadata": {
|
||||
"name": "test1"
|
||||
},
|
||||
"spec": {
|
||||
"type": "timeseries",
|
||||
"pluginVersion": "12.3.0-pre",
|
||||
"title": "example library panel (test1)",
|
||||
"panelTitle": "simple",
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"barWidthFactor": 0.6,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"showValues": false,
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "cejobd88i85j4d"
|
||||
},
|
||||
"gridPos": {
|
||||
"w": 12,
|
||||
"h": 8,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"refId": "A",
|
||||
"datasource": {
|
||||
"type": "grafana-testdata-datasource",
|
||||
"uid": "cejobd88i85j4d"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -2694,7 +2694,7 @@
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/com.github.grafana.grafana-plugin-sdk-go.experimental.apis.data.v0alpha1.DataQuery"
|
||||
},
|
||||
"x-kubernetes-list-type": "set"
|
||||
"x-kubernetes-list-type": "atomic"
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the library panel",
|
||||
|
||||
Reference in New Issue
Block a user