Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b6a434383 | |||
| 6d6112b627 | |||
| 41f9162472 | |||
| 7e991886e0 | |||
| 85925d0765 | |||
| 7790698aaa | |||
| 5499ad8023 | |||
| 90c4ab9d96 | |||
| fd31f087ee | |||
| 3ee834922b | |||
| 2e2ce8fddd | |||
| 8214dbc758 | |||
| 98d454401c | |||
| fcf1a47222 | |||
| 8a5b6804dd | |||
| f0028f692b | |||
| d71474246c | |||
| 0275939762 | |||
| c15b1b6f10 | |||
| 32b9bebc75 | |||
| 7fce2d9516 | |||
| 2da171595a | |||
| dd77107ed4 | |||
| aaa5d02a3e | |||
| db9afe31e4 | |||
| 9447015e54 | |||
| abe10b2bb6 | |||
| 009716a408 | |||
| e0c28cfa4c | |||
| 18c4f5b875 | |||
| 400f3a91d0 | |||
| d6b04d28b6 | |||
| 0400d536c7 | |||
| 694e88b95b | |||
| ad73303328 | |||
| 3dcd809aaf | |||
| 6b7fac65b1 | |||
| 2d17de2395 | |||
| 5b685373aa | |||
| 4d29e5bf6a | |||
| 7a0e64196b | |||
| f1e24f528e | |||
| 198f4dbf93 |
@@ -10,6 +10,20 @@ manifest: {
|
||||
|
||||
v0alpha1: {
|
||||
kinds: [annotationv0alpha1]
|
||||
routes: {
|
||||
namespaced: {
|
||||
"/tags": {
|
||||
"GET": {
|
||||
response: {
|
||||
tags: [...{
|
||||
tag: string
|
||||
count: number
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
codegen: {
|
||||
ts: {
|
||||
enabled: true
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type GetTagsBody struct {
|
||||
Tags []V0alpha1GetTagsBodyTags `json:"tags"`
|
||||
}
|
||||
|
||||
// NewGetTagsBody creates a new GetTagsBody object.
|
||||
func NewGetTagsBody() *GetTagsBody {
|
||||
return &GetTagsBody{
|
||||
Tags: []V0alpha1GetTagsBodyTags{},
|
||||
}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type V0alpha1GetTagsBodyTags struct {
|
||||
Tag string `json:"tag"`
|
||||
Count float64 `json:"count"`
|
||||
}
|
||||
|
||||
// NewV0alpha1GetTagsBodyTags creates a new V0alpha1GetTagsBodyTags object.
|
||||
func NewV0alpha1GetTagsBodyTags() *V0alpha1GetTagsBodyTags {
|
||||
return &V0alpha1GetTagsBodyTags{}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v0alpha1
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type GetTags struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
GetTagsBody `json:",inline"`
|
||||
}
|
||||
|
||||
func NewGetTags() *GetTags {
|
||||
return &GetTags{}
|
||||
}
|
||||
|
||||
func (t *GetTagsBody) DeepCopyInto(dst *GetTagsBody) {
|
||||
_ = resource.CopyObjectInto(dst, t)
|
||||
}
|
||||
|
||||
func (o *GetTags) DeepCopyObject() runtime.Object {
|
||||
dst := NewGetTags()
|
||||
o.DeepCopyInto(dst)
|
||||
return dst
|
||||
}
|
||||
|
||||
func (o *GetTags) DeepCopyInto(dst *GetTags) {
|
||||
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
|
||||
dst.TypeMeta.Kind = o.TypeMeta.Kind
|
||||
o.GetTagsBody.DeepCopyInto(&dst.GetTagsBody)
|
||||
}
|
||||
|
||||
var _ runtime.Object = NewGetTags()
|
||||
+57
-4
@@ -43,9 +43,60 @@ var appManifestData = app.ManifestData{
|
||||
},
|
||||
},
|
||||
Routes: app.ManifestVersionRoutes{
|
||||
Namespaced: map[string]spec3.PathProps{},
|
||||
Cluster: map[string]spec3.PathProps{},
|
||||
Schemas: map[string]spec.Schema{},
|
||||
Namespaced: map[string]spec3.PathProps{
|
||||
"/tags": {
|
||||
Get: &spec3.Operation{
|
||||
OperationProps: spec3.OperationProps{
|
||||
|
||||
OperationId: "getTags",
|
||||
|
||||
Responses: &spec3.Responses{
|
||||
ResponsesProps: spec3.ResponsesProps{
|
||||
Default: &spec3.Response{
|
||||
ResponseProps: spec3.ResponseProps{
|
||||
Description: "Default OK response",
|
||||
Content: map[string]*spec3.MediaType{
|
||||
"application/json": {
|
||||
MediaTypeProps: spec3.MediaTypeProps{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
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",
|
||||
},
|
||||
},
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
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",
|
||||
},
|
||||
},
|
||||
"tags": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{
|
||||
"tags",
|
||||
"apiVersion",
|
||||
"kind",
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Cluster: map[string]spec3.PathProps{},
|
||||
Schemas: map[string]spec.Schema{},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -70,7 +121,9 @@ func ManifestGoTypeAssociator(kind, version string) (goType resource.Kind, exist
|
||||
return goType, exists
|
||||
}
|
||||
|
||||
var customRouteToGoResponseType = map[string]any{}
|
||||
var customRouteToGoResponseType = map[string]any{
|
||||
"v0alpha1||<namespace>/tags|GET": v0alpha1.GetTags{},
|
||||
}
|
||||
|
||||
// ManifestCustomRouteResponsesAssociator returns the associated response go type for a given kind, version, custom route path, and method, if one exists.
|
||||
// kind may be empty for custom routes which are not kind subroutes. Leading slashes are removed from subroute paths.
|
||||
|
||||
@@ -30,6 +30,23 @@ func New(cfg app.Config) (app.App, error) {
|
||||
},
|
||||
}
|
||||
|
||||
// Add custom route handlers if a TagHandler is provided in SpecificConfig.
|
||||
// The handler is created/owned by the registry layer and passed in via
|
||||
// SpecificConfig to avoid the apps package depending on the registry.
|
||||
if cfg.SpecificConfig != nil {
|
||||
if annotationConfig, ok := cfg.SpecificConfig.(*AnnotationConfig); ok && annotationConfig.TagHandler != nil {
|
||||
simpleConfig.VersionedCustomRoutes = map[string]simple.AppVersionRouteHandlers{
|
||||
"v0alpha1": {
|
||||
{
|
||||
Namespaced: true,
|
||||
Path: "tags",
|
||||
Method: "GET",
|
||||
}: annotationConfig.TagHandler,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a, err := simple.NewApp(simpleConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
)
|
||||
|
||||
// AnnotationConfig is the app-specific config for the annotation app. The
|
||||
// registry can pass a TagHandler implementation here to wire the /tags
|
||||
// resource route into the app without importing registry types.
|
||||
type AnnotationConfig struct {
|
||||
// TagHandler is the handler function for the GET /tags custom route.
|
||||
// The function signature matches app.CustomRouteHandler from the app-sdk.
|
||||
TagHandler func(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error
|
||||
}
|
||||
@@ -7,4 +7,4 @@ generate: install-app-sdk update-app-sdk
|
||||
--gogenpath=./pkg/apis \
|
||||
--grouping=group \
|
||||
--genoperatorstate=false \
|
||||
--defencoding=none
|
||||
--defencoding=none
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package preferences
|
||||
|
||||
datasourcestacksV1alpha1: {
|
||||
kind: "DataSourceStack"
|
||||
pluralName: "DataSourceStacks"
|
||||
scope: "Namespaced"
|
||||
schema: {
|
||||
spec: {
|
||||
template: TemplateSpec
|
||||
modes: [...ModeSpec]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TemplateSpec: {
|
||||
[string]: DataSourceStackTemplateItem
|
||||
}
|
||||
|
||||
DataSourceStackTemplateItem: {
|
||||
group: string // type
|
||||
name: string // variable name / display name
|
||||
}
|
||||
|
||||
ModeSpec: {
|
||||
name: string
|
||||
uid: string
|
||||
definition: Mode
|
||||
}
|
||||
|
||||
Mode: [string]: ModeItem
|
||||
|
||||
ModeItem: {
|
||||
dataSourceRef: string // grafana data source uid
|
||||
}
|
||||
@@ -6,12 +6,13 @@ manifest: {
|
||||
versions: {
|
||||
"v1alpha1": {
|
||||
codegen: {
|
||||
ts: {enabled: false}
|
||||
ts: {enabled: true}
|
||||
go: {enabled: true}
|
||||
}
|
||||
kinds: [
|
||||
starsV1alpha1,
|
||||
datasourcestacksV1alpha1
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
type DataSourceStackClient struct {
|
||||
client *resource.TypedClient[*DataSourceStack, *DataSourceStackList]
|
||||
}
|
||||
|
||||
func NewDataSourceStackClient(client resource.Client) *DataSourceStackClient {
|
||||
return &DataSourceStackClient{
|
||||
client: resource.NewTypedClient[*DataSourceStack, *DataSourceStackList](client, DataSourceStackKind()),
|
||||
}
|
||||
}
|
||||
|
||||
func NewDataSourceStackClientFromGenerator(generator resource.ClientGenerator) (*DataSourceStackClient, error) {
|
||||
c, err := generator.ClientFor(DataSourceStackKind())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewDataSourceStackClient(c), nil
|
||||
}
|
||||
|
||||
func (c *DataSourceStackClient) Get(ctx context.Context, identifier resource.Identifier) (*DataSourceStack, error) {
|
||||
return c.client.Get(ctx, identifier)
|
||||
}
|
||||
|
||||
func (c *DataSourceStackClient) List(ctx context.Context, namespace string, opts resource.ListOptions) (*DataSourceStackList, error) {
|
||||
return c.client.List(ctx, namespace, opts)
|
||||
}
|
||||
|
||||
func (c *DataSourceStackClient) ListAll(ctx context.Context, namespace string, opts resource.ListOptions) (*DataSourceStackList, error) {
|
||||
resp, err := c.client.List(ctx, namespace, resource.ListOptions{
|
||||
ResourceVersion: opts.ResourceVersion,
|
||||
Limit: opts.Limit,
|
||||
LabelFilters: opts.LabelFilters,
|
||||
FieldSelectors: opts.FieldSelectors,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for resp.GetContinue() != "" {
|
||||
page, err := c.client.List(ctx, namespace, resource.ListOptions{
|
||||
Continue: resp.GetContinue(),
|
||||
ResourceVersion: opts.ResourceVersion,
|
||||
Limit: opts.Limit,
|
||||
LabelFilters: opts.LabelFilters,
|
||||
FieldSelectors: opts.FieldSelectors,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.SetContinue(page.GetContinue())
|
||||
resp.SetResourceVersion(page.GetResourceVersion())
|
||||
resp.SetItems(append(resp.GetItems(), page.GetItems()...))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (c *DataSourceStackClient) Create(ctx context.Context, obj *DataSourceStack, opts resource.CreateOptions) (*DataSourceStack, error) {
|
||||
// Make sure apiVersion and kind are set
|
||||
obj.APIVersion = GroupVersion.Identifier()
|
||||
obj.Kind = DataSourceStackKind().Kind()
|
||||
return c.client.Create(ctx, obj, opts)
|
||||
}
|
||||
|
||||
func (c *DataSourceStackClient) Update(ctx context.Context, obj *DataSourceStack, opts resource.UpdateOptions) (*DataSourceStack, error) {
|
||||
return c.client.Update(ctx, obj, opts)
|
||||
}
|
||||
|
||||
func (c *DataSourceStackClient) Patch(ctx context.Context, identifier resource.Identifier, req resource.PatchRequest, opts resource.PatchOptions) (*DataSourceStack, error) {
|
||||
return c.client.Patch(ctx, identifier, req, opts)
|
||||
}
|
||||
|
||||
func (c *DataSourceStackClient) Delete(ctx context.Context, identifier resource.Identifier, opts resource.DeleteOptions) error {
|
||||
return c.client.Delete(ctx, identifier, opts)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
// DataSourceStackJSONCodec is an implementation of resource.Codec for kubernetes JSON encoding
|
||||
type DataSourceStackJSONCodec struct{}
|
||||
|
||||
// Read reads JSON-encoded bytes from `reader` and unmarshals them into `into`
|
||||
func (*DataSourceStackJSONCodec) Read(reader io.Reader, into resource.Object) error {
|
||||
return json.NewDecoder(reader).Decode(into)
|
||||
}
|
||||
|
||||
// Write writes JSON-encoded bytes into `writer` marshaled from `from`
|
||||
func (*DataSourceStackJSONCodec) Write(writer io.Writer, from resource.Object) error {
|
||||
return json.NewEncoder(writer).Encode(from)
|
||||
}
|
||||
|
||||
// Interface compliance checks
|
||||
var _ resource.Codec = &DataSourceStackJSONCodec{}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
time "time"
|
||||
)
|
||||
|
||||
// metadata contains embedded CommonMetadata and can be extended with custom string fields
|
||||
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
|
||||
// without external reference as using the CommonMetadata reference breaks thema codegen.
|
||||
type DataSourceStackMetadata struct {
|
||||
UpdateTimestamp time.Time `json:"updateTimestamp"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
Uid string `json:"uid"`
|
||||
CreationTimestamp time.Time `json:"creationTimestamp"`
|
||||
DeletionTimestamp *time.Time `json:"deletionTimestamp,omitempty"`
|
||||
Finalizers []string `json:"finalizers"`
|
||||
ResourceVersion string `json:"resourceVersion"`
|
||||
Generation int64 `json:"generation"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
// NewDataSourceStackMetadata creates a new DataSourceStackMetadata object.
|
||||
func NewDataSourceStackMetadata() *DataSourceStackMetadata {
|
||||
return &DataSourceStackMetadata{
|
||||
Finalizers: []string{},
|
||||
Labels: map[string]string{},
|
||||
}
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"time"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type DataSourceStack struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata" yaml:"metadata"`
|
||||
|
||||
// Spec is the spec of the DataSourceStack
|
||||
Spec DataSourceStackSpec `json:"spec" yaml:"spec"`
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) GetSpec() any {
|
||||
return o.Spec
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) SetSpec(spec any) error {
|
||||
cast, ok := spec.(DataSourceStackSpec)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot set spec type %#v, not of type Spec", spec)
|
||||
}
|
||||
o.Spec = cast
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) GetSubresources() map[string]any {
|
||||
return map[string]any{}
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) GetSubresource(name string) (any, bool) {
|
||||
switch name {
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) SetSubresource(name string, value any) error {
|
||||
switch name {
|
||||
default:
|
||||
return fmt.Errorf("subresource '%s' does not exist", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) GetStaticMetadata() resource.StaticMetadata {
|
||||
gvk := o.GroupVersionKind()
|
||||
return resource.StaticMetadata{
|
||||
Name: o.ObjectMeta.Name,
|
||||
Namespace: o.ObjectMeta.Namespace,
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: gvk.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) SetStaticMetadata(metadata resource.StaticMetadata) {
|
||||
o.Name = metadata.Name
|
||||
o.Namespace = metadata.Namespace
|
||||
o.SetGroupVersionKind(schema.GroupVersionKind{
|
||||
Group: metadata.Group,
|
||||
Version: metadata.Version,
|
||||
Kind: metadata.Kind,
|
||||
})
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) GetCommonMetadata() resource.CommonMetadata {
|
||||
dt := o.DeletionTimestamp
|
||||
var deletionTimestamp *time.Time
|
||||
if dt != nil {
|
||||
deletionTimestamp = &dt.Time
|
||||
}
|
||||
// Legacy ExtraFields support
|
||||
extraFields := make(map[string]any)
|
||||
if o.Annotations != nil {
|
||||
extraFields["annotations"] = o.Annotations
|
||||
}
|
||||
if o.ManagedFields != nil {
|
||||
extraFields["managedFields"] = o.ManagedFields
|
||||
}
|
||||
if o.OwnerReferences != nil {
|
||||
extraFields["ownerReferences"] = o.OwnerReferences
|
||||
}
|
||||
return resource.CommonMetadata{
|
||||
UID: string(o.UID),
|
||||
ResourceVersion: o.ResourceVersion,
|
||||
Generation: o.Generation,
|
||||
Labels: o.Labels,
|
||||
CreationTimestamp: o.CreationTimestamp.Time,
|
||||
DeletionTimestamp: deletionTimestamp,
|
||||
Finalizers: o.Finalizers,
|
||||
UpdateTimestamp: o.GetUpdateTimestamp(),
|
||||
CreatedBy: o.GetCreatedBy(),
|
||||
UpdatedBy: o.GetUpdatedBy(),
|
||||
ExtraFields: extraFields,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) SetCommonMetadata(metadata resource.CommonMetadata) {
|
||||
o.UID = types.UID(metadata.UID)
|
||||
o.ResourceVersion = metadata.ResourceVersion
|
||||
o.Generation = metadata.Generation
|
||||
o.Labels = metadata.Labels
|
||||
o.CreationTimestamp = metav1.NewTime(metadata.CreationTimestamp)
|
||||
if metadata.DeletionTimestamp != nil {
|
||||
dt := metav1.NewTime(*metadata.DeletionTimestamp)
|
||||
o.DeletionTimestamp = &dt
|
||||
} else {
|
||||
o.DeletionTimestamp = nil
|
||||
}
|
||||
o.Finalizers = metadata.Finalizers
|
||||
if o.Annotations == nil {
|
||||
o.Annotations = make(map[string]string)
|
||||
}
|
||||
if !metadata.UpdateTimestamp.IsZero() {
|
||||
o.SetUpdateTimestamp(metadata.UpdateTimestamp)
|
||||
}
|
||||
if metadata.CreatedBy != "" {
|
||||
o.SetCreatedBy(metadata.CreatedBy)
|
||||
}
|
||||
if metadata.UpdatedBy != "" {
|
||||
o.SetUpdatedBy(metadata.UpdatedBy)
|
||||
}
|
||||
// Legacy support for setting Annotations, ManagedFields, and OwnerReferences via ExtraFields
|
||||
if metadata.ExtraFields != nil {
|
||||
if annotations, ok := metadata.ExtraFields["annotations"]; ok {
|
||||
if cast, ok := annotations.(map[string]string); ok {
|
||||
o.Annotations = cast
|
||||
}
|
||||
}
|
||||
if managedFields, ok := metadata.ExtraFields["managedFields"]; ok {
|
||||
if cast, ok := managedFields.([]metav1.ManagedFieldsEntry); ok {
|
||||
o.ManagedFields = cast
|
||||
}
|
||||
}
|
||||
if ownerReferences, ok := metadata.ExtraFields["ownerReferences"]; ok {
|
||||
if cast, ok := ownerReferences.([]metav1.OwnerReference); ok {
|
||||
o.OwnerReferences = cast
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) GetCreatedBy() string {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
return o.ObjectMeta.Annotations["grafana.com/createdBy"]
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) SetCreatedBy(createdBy string) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/createdBy"] = createdBy
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) GetUpdateTimestamp() time.Time {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
parsed, _ := time.Parse(time.RFC3339, o.ObjectMeta.Annotations["grafana.com/updateTimestamp"])
|
||||
return parsed
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) SetUpdateTimestamp(updateTimestamp time.Time) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/updateTimestamp"] = updateTimestamp.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) GetUpdatedBy() string {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
return o.ObjectMeta.Annotations["grafana.com/updatedBy"]
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) SetUpdatedBy(updatedBy string) {
|
||||
if o.ObjectMeta.Annotations == nil {
|
||||
o.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
o.ObjectMeta.Annotations["grafana.com/updatedBy"] = updatedBy
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) Copy() resource.Object {
|
||||
return resource.CopyObject(o)
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) DeepCopyObject() runtime.Object {
|
||||
return o.Copy()
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) DeepCopy() *DataSourceStack {
|
||||
cpy := &DataSourceStack{}
|
||||
o.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *DataSourceStack) DeepCopyInto(dst *DataSourceStack) {
|
||||
dst.TypeMeta.APIVersion = o.TypeMeta.APIVersion
|
||||
dst.TypeMeta.Kind = o.TypeMeta.Kind
|
||||
o.ObjectMeta.DeepCopyInto(&dst.ObjectMeta)
|
||||
o.Spec.DeepCopyInto(&dst.Spec)
|
||||
}
|
||||
|
||||
// Interface compliance compile-time check
|
||||
var _ resource.Object = &DataSourceStack{}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type DataSourceStackList struct {
|
||||
metav1.TypeMeta `json:",inline" yaml:",inline"`
|
||||
metav1.ListMeta `json:"metadata" yaml:"metadata"`
|
||||
Items []DataSourceStack `json:"items" yaml:"items"`
|
||||
}
|
||||
|
||||
func (o *DataSourceStackList) DeepCopyObject() runtime.Object {
|
||||
return o.Copy()
|
||||
}
|
||||
|
||||
func (o *DataSourceStackList) Copy() resource.ListObject {
|
||||
cpy := &DataSourceStackList{
|
||||
TypeMeta: o.TypeMeta,
|
||||
Items: make([]DataSourceStack, len(o.Items)),
|
||||
}
|
||||
o.ListMeta.DeepCopyInto(&cpy.ListMeta)
|
||||
for i := 0; i < len(o.Items); i++ {
|
||||
if item, ok := o.Items[i].Copy().(*DataSourceStack); ok {
|
||||
cpy.Items[i] = *item
|
||||
}
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *DataSourceStackList) GetItems() []resource.Object {
|
||||
items := make([]resource.Object, len(o.Items))
|
||||
for i := 0; i < len(o.Items); i++ {
|
||||
items[i] = &o.Items[i]
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func (o *DataSourceStackList) SetItems(items []resource.Object) {
|
||||
o.Items = make([]DataSourceStack, len(items))
|
||||
for i := 0; i < len(items); i++ {
|
||||
o.Items[i] = *items[i].(*DataSourceStack)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *DataSourceStackList) DeepCopy() *DataSourceStackList {
|
||||
cpy := &DataSourceStackList{}
|
||||
o.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func (o *DataSourceStackList) DeepCopyInto(dst *DataSourceStackList) {
|
||||
resource.CopyObjectInto(dst, o)
|
||||
}
|
||||
|
||||
// Interface compliance compile-time check
|
||||
var _ resource.ListObject = &DataSourceStackList{}
|
||||
|
||||
// Copy methods for all subresource types
|
||||
|
||||
// DeepCopy creates a full deep copy of Spec
|
||||
func (s *DataSourceStackSpec) DeepCopy() *DataSourceStackSpec {
|
||||
cpy := &DataSourceStackSpec{}
|
||||
s.DeepCopyInto(cpy)
|
||||
return cpy
|
||||
}
|
||||
|
||||
// DeepCopyInto deep copies Spec into another Spec object
|
||||
func (s *DataSourceStackSpec) DeepCopyInto(dst *DataSourceStackSpec) {
|
||||
resource.CopyObjectInto(dst, s)
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// Code generated by grafana-app-sdk. DO NOT EDIT.
|
||||
//
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
)
|
||||
|
||||
// schema is unexported to prevent accidental overwrites
|
||||
var (
|
||||
schemaDataSourceStack = resource.NewSimpleSchema("collections.grafana.app", "v1alpha1", &DataSourceStack{}, &DataSourceStackList{}, resource.WithKind("DataSourceStack"),
|
||||
resource.WithPlural("datasourcestacks"), resource.WithScope(resource.NamespacedScope))
|
||||
kindDataSourceStack = resource.Kind{
|
||||
Schema: schemaDataSourceStack,
|
||||
Codecs: map[resource.KindEncoding]resource.Codec{
|
||||
resource.KindEncodingJSON: &DataSourceStackJSONCodec{},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Kind returns a resource.Kind for this Schema with a JSON codec
|
||||
func DataSourceStackKind() resource.Kind {
|
||||
return kindDataSourceStack
|
||||
}
|
||||
|
||||
// Schema returns a resource.SimpleSchema representation of DataSourceStack
|
||||
func DataSourceStackSchema() *resource.SimpleSchema {
|
||||
return schemaDataSourceStack
|
||||
}
|
||||
|
||||
// Interface compliance checks
|
||||
var _ resource.Schema = kindDataSourceStack
|
||||
@@ -0,0 +1,58 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type DataSourceStackTemplateSpec map[string]DataSourceStackDataSourceStackTemplateItem
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type DataSourceStackDataSourceStackTemplateItem struct {
|
||||
// type
|
||||
Group string `json:"group"`
|
||||
// variable name / display name
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// NewDataSourceStackDataSourceStackTemplateItem creates a new DataSourceStackDataSourceStackTemplateItem object.
|
||||
func NewDataSourceStackDataSourceStackTemplateItem() *DataSourceStackDataSourceStackTemplateItem {
|
||||
return &DataSourceStackDataSourceStackTemplateItem{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type DataSourceStackModeSpec struct {
|
||||
Name string `json:"name"`
|
||||
Uid string `json:"uid"`
|
||||
Definition DataSourceStackMode `json:"definition"`
|
||||
}
|
||||
|
||||
// NewDataSourceStackModeSpec creates a new DataSourceStackModeSpec object.
|
||||
func NewDataSourceStackModeSpec() *DataSourceStackModeSpec {
|
||||
return &DataSourceStackModeSpec{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type DataSourceStackMode map[string]DataSourceStackModeItem
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type DataSourceStackModeItem struct {
|
||||
// grafana data source uid
|
||||
DataSourceRef string `json:"dataSourceRef"`
|
||||
}
|
||||
|
||||
// NewDataSourceStackModeItem creates a new DataSourceStackModeItem object.
|
||||
func NewDataSourceStackModeItem() *DataSourceStackModeItem {
|
||||
return &DataSourceStackModeItem{}
|
||||
}
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type DataSourceStackSpec struct {
|
||||
Template DataSourceStackTemplateSpec `json:"template"`
|
||||
Modes []DataSourceStackModeSpec `json:"modes"`
|
||||
}
|
||||
|
||||
// NewDataSourceStackSpec creates a new DataSourceStackSpec object.
|
||||
func NewDataSourceStackSpec() *DataSourceStackSpec {
|
||||
return &DataSourceStackSpec{
|
||||
Modes: []DataSourceStackModeSpec{},
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,19 @@ var StarsResourceInfo = utils.NewResourceInfo(APIGroup, APIVersion,
|
||||
},
|
||||
)
|
||||
|
||||
var DatasourceStacksResourceInfo = utils.NewResourceInfo(APIGroup, APIVersion,
|
||||
"datasourcestacks", "datasourcestack", "DataSourceStack",
|
||||
func() runtime.Object { return &DataSourceStack{} },
|
||||
func() runtime.Object { return &DataSourceStackList{} },
|
||||
utils.TableColumns{
|
||||
Definition: []metav1.TableColumnDefinition{
|
||||
{Name: "Name", Type: "string", Format: "name"},
|
||||
{Name: "Created At", Type: "date"},
|
||||
},
|
||||
// TODO: Reader?
|
||||
},
|
||||
)
|
||||
|
||||
var (
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
@@ -48,6 +61,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(schemeGroupVersion,
|
||||
&Stars{},
|
||||
&StarsList{},
|
||||
&DataSourceStack{},
|
||||
&DataSourceStackList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, schemeGroupVersion)
|
||||
return nil
|
||||
|
||||
@@ -14,10 +14,241 @@ import (
|
||||
|
||||
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||
return map[string]common.OpenAPIDefinition{
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.Stars": schema_pkg_apis_collections_v1alpha1_Stars(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.StarsList": schema_pkg_apis_collections_v1alpha1_StarsList(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.StarsResource": schema_pkg_apis_collections_v1alpha1_StarsResource(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.StarsSpec": schema_pkg_apis_collections_v1alpha1_StarsSpec(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStack": schema_pkg_apis_collections_v1alpha1_DataSourceStack(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackDataSourceStackTemplateItem": schema_pkg_apis_collections_v1alpha1_DataSourceStackDataSourceStackTemplateItem(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackList": schema_pkg_apis_collections_v1alpha1_DataSourceStackList(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackModeItem": schema_pkg_apis_collections_v1alpha1_DataSourceStackModeItem(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackModeSpec": schema_pkg_apis_collections_v1alpha1_DataSourceStackModeSpec(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackSpec": schema_pkg_apis_collections_v1alpha1_DataSourceStackSpec(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.Stars": schema_pkg_apis_collections_v1alpha1_Stars(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.StarsList": schema_pkg_apis_collections_v1alpha1_StarsList(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.StarsResource": schema_pkg_apis_collections_v1alpha1_StarsResource(ref),
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.StarsSpec": schema_pkg_apis_collections_v1alpha1_StarsSpec(ref),
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_collections_v1alpha1_DataSourceStack(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
|
||||
},
|
||||
},
|
||||
"spec": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Spec is the spec of the DataSourceStack",
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackSpec"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"metadata", "spec"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackSpec", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_collections_v1alpha1_DataSourceStackDataSourceStackTemplateItem(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"group": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "type",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"name": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "variable name / display name",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"group", "name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_collections_v1alpha1_DataSourceStackList(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"kind": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"apiVersion": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"metadata": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
|
||||
},
|
||||
},
|
||||
"items": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStack"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"metadata", "items"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStack", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_collections_v1alpha1_DataSourceStackModeItem(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"dataSourceRef": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "grafana data source uid",
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"dataSourceRef"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_collections_v1alpha1_DataSourceStackModeSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"name": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"uid": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: "",
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"definition": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackModeItem"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"name", "uid", "definition"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackModeItem"},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_pkg_apis_collections_v1alpha1_DataSourceStackSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"template": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"object"},
|
||||
AdditionalProperties: &spec.SchemaOrBool{
|
||||
Allows: true,
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackDataSourceStackTemplateItem"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"modes": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"array"},
|
||||
Items: &spec.SchemaOrArray{
|
||||
Schema: &spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref("github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackModeSpec"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"template", "modes"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackDataSourceStackTemplateItem", "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1.DataSourceStackModeSpec"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -1,2 +1,4 @@
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1,DataSourceStackSpec,Modes
|
||||
API rule violation: list_type_missing,github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1,StarsSpec,Resource
|
||||
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1,DataSourceStackList,ListMeta
|
||||
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1,StarsList,ListMeta
|
||||
|
||||
+19
-7
@@ -10,19 +10,22 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
"github.com/grafana/grafana-app-sdk/resource"
|
||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||
v1alpha1 "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||
)
|
||||
|
||||
var (
|
||||
rawSchemaStarsv1alpha1 = []byte(`{"Resource":{"additionalProperties":false,"properties":{"group":{"type":"string"},"kind":{"type":"string"},"names":{"description":"The set of resources\n+listType=set","items":{"type":"string"},"type":"array"}},"required":["group","kind","names"],"type":"object"},"Stars":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"resource":{"items":{"$ref":"#/components/schemas/Resource"},"type":"array"}},"required":["resource"],"type":"object"}}`)
|
||||
versionSchemaStarsv1alpha1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaStarsv1alpha1, &versionSchemaStarsv1alpha1)
|
||||
rawSchemaStarsv1alpha1 = []byte(`{"Resource":{"additionalProperties":false,"properties":{"group":{"type":"string"},"kind":{"type":"string"},"names":{"description":"The set of resources\n+listType=set","items":{"type":"string"},"type":"array"}},"required":["group","kind","names"],"type":"object"},"Stars":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"spec":{"additionalProperties":false,"properties":{"resource":{"items":{"$ref":"#/components/schemas/Resource"},"type":"array"}},"required":["resource"],"type":"object"}}`)
|
||||
versionSchemaStarsv1alpha1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaStarsv1alpha1, &versionSchemaStarsv1alpha1)
|
||||
rawSchemaDataSourceStackv1alpha1 = []byte(`{"DataSourceStack":{"properties":{"spec":{"$ref":"#/components/schemas/spec"}},"required":["spec"]},"DataSourceStackTemplateItem":{"additionalProperties":false,"properties":{"group":{"description":"type","type":"string"},"name":{"description":"variable name / display name","type":"string"}},"required":["group","name"],"type":"object"},"Mode":{"additionalProperties":{"$ref":"#/components/schemas/ModeItem"},"type":"object"},"ModeItem":{"additionalProperties":false,"properties":{"dataSourceRef":{"description":"grafana data source uid","type":"string"}},"required":["dataSourceRef"],"type":"object"},"ModeSpec":{"additionalProperties":false,"properties":{"definition":{"$ref":"#/components/schemas/Mode"},"name":{"type":"string"},"uid":{"type":"string"}},"required":["name","uid","definition"],"type":"object"},"TemplateSpec":{"additionalProperties":{"$ref":"#/components/schemas/DataSourceStackTemplateItem"},"type":"object"},"spec":{"additionalProperties":false,"properties":{"modes":{"items":{"$ref":"#/components/schemas/ModeSpec"},"type":"array"},"template":{"$ref":"#/components/schemas/TemplateSpec"}},"required":["template","modes"],"type":"object"}}`)
|
||||
versionSchemaDataSourceStackv1alpha1 app.VersionSchema
|
||||
_ = json.Unmarshal(rawSchemaDataSourceStackv1alpha1, &versionSchemaDataSourceStackv1alpha1)
|
||||
)
|
||||
|
||||
var appManifestData = app.ManifestData{
|
||||
@@ -49,6 +52,14 @@ var appManifestData = app.ManifestData{
|
||||
},
|
||||
Schema: &versionSchemaStarsv1alpha1,
|
||||
},
|
||||
|
||||
{
|
||||
Kind: "DataSourceStack",
|
||||
Plural: "DataSourceStacks",
|
||||
Scope: "Namespaced",
|
||||
Conversion: false,
|
||||
Schema: &versionSchemaDataSourceStackv1alpha1,
|
||||
},
|
||||
},
|
||||
Routes: app.ManifestVersionRoutes{
|
||||
Namespaced: map[string]spec3.PathProps{},
|
||||
@@ -68,7 +79,8 @@ func RemoteManifest() app.Manifest {
|
||||
}
|
||||
|
||||
var kindVersionToGoType = map[string]resource.Kind{
|
||||
"Stars/v1alpha1": v1alpha1.StarsKind(),
|
||||
"Stars/v1alpha1": v1alpha1.StarsKind(),
|
||||
"DataSourceStack/v1alpha1": v1alpha1.DataSourceStackKind(),
|
||||
}
|
||||
|
||||
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
|
||||
|
||||
Generated
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* This file was generated by grafana-app-sdk. DO NOT EDIT.
|
||||
*/
|
||||
import { Spec } from './types.spec.gen';
|
||||
|
||||
export interface Metadata {
|
||||
name: string;
|
||||
namespace: string;
|
||||
generateName?: string;
|
||||
selfLink?: string;
|
||||
uid?: string;
|
||||
resourceVersion?: string;
|
||||
generation?: number;
|
||||
creationTimestamp?: string;
|
||||
deletionTimestamp?: string;
|
||||
deletionGracePeriodSeconds?: number;
|
||||
labels?: Record<string, string>;
|
||||
annotations?: Record<string, string>;
|
||||
ownerReferences?: OwnerReference[];
|
||||
finalizers?: string[];
|
||||
managedFields?: ManagedFieldsEntry[];
|
||||
}
|
||||
|
||||
export interface OwnerReference {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
controller?: boolean;
|
||||
blockOwnerDeletion?: boolean;
|
||||
}
|
||||
|
||||
export interface ManagedFieldsEntry {
|
||||
manager?: string;
|
||||
operation?: string;
|
||||
apiVersion?: string;
|
||||
time?: string;
|
||||
fieldsType?: string;
|
||||
subresource?: string;
|
||||
}
|
||||
|
||||
export interface DataSourceStack {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
metadata: Metadata;
|
||||
spec: Spec;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
// metadata contains embedded CommonMetadata and can be extended with custom string fields
|
||||
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
|
||||
// without external reference as using the CommonMetadata reference breaks thema codegen.
|
||||
export interface Metadata {
|
||||
updateTimestamp: string;
|
||||
createdBy: string;
|
||||
uid: string;
|
||||
creationTimestamp: string;
|
||||
deletionTimestamp?: string;
|
||||
finalizers: string[];
|
||||
resourceVersion: string;
|
||||
generation: number;
|
||||
updatedBy: string;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
|
||||
export const defaultMetadata = (): Metadata => ({
|
||||
updateTimestamp: "",
|
||||
createdBy: "",
|
||||
uid: "",
|
||||
creationTimestamp: "",
|
||||
finalizers: [],
|
||||
resourceVersion: "",
|
||||
generation: 0,
|
||||
updatedBy: "",
|
||||
labels: {},
|
||||
});
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
export type TemplateSpec = Record<string, DataSourceStackTemplateItem>;
|
||||
|
||||
export const defaultTemplateSpec = (): TemplateSpec => ({});
|
||||
|
||||
export interface DataSourceStackTemplateItem {
|
||||
// type
|
||||
group: string;
|
||||
// variable name / display name
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const defaultDataSourceStackTemplateItem = (): DataSourceStackTemplateItem => ({
|
||||
group: "",
|
||||
name: "",
|
||||
});
|
||||
|
||||
export interface ModeSpec {
|
||||
name: string;
|
||||
uid: string;
|
||||
definition: Mode;
|
||||
}
|
||||
|
||||
export const defaultModeSpec = (): ModeSpec => ({
|
||||
name: "",
|
||||
uid: "",
|
||||
definition: defaultMode(),
|
||||
});
|
||||
|
||||
export type Mode = Record<string, ModeItem>;
|
||||
|
||||
export const defaultMode = (): Mode => ({});
|
||||
|
||||
export interface ModeItem {
|
||||
// grafana data source uid
|
||||
dataSourceRef: string;
|
||||
}
|
||||
|
||||
export const defaultModeItem = (): ModeItem => ({
|
||||
dataSourceRef: "",
|
||||
});
|
||||
|
||||
export interface Spec {
|
||||
template: TemplateSpec;
|
||||
modes: ModeSpec[];
|
||||
}
|
||||
|
||||
export const defaultSpec = (): Spec => ({
|
||||
template: defaultTemplateSpec(),
|
||||
modes: [],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* This file was generated by grafana-app-sdk. DO NOT EDIT.
|
||||
*/
|
||||
import { Spec } from './types.spec.gen';
|
||||
|
||||
export interface Metadata {
|
||||
name: string;
|
||||
namespace: string;
|
||||
generateName?: string;
|
||||
selfLink?: string;
|
||||
uid?: string;
|
||||
resourceVersion?: string;
|
||||
generation?: number;
|
||||
creationTimestamp?: string;
|
||||
deletionTimestamp?: string;
|
||||
deletionGracePeriodSeconds?: number;
|
||||
labels?: Record<string, string>;
|
||||
annotations?: Record<string, string>;
|
||||
ownerReferences?: OwnerReference[];
|
||||
finalizers?: string[];
|
||||
managedFields?: ManagedFieldsEntry[];
|
||||
}
|
||||
|
||||
export interface OwnerReference {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
uid: string;
|
||||
controller?: boolean;
|
||||
blockOwnerDeletion?: boolean;
|
||||
}
|
||||
|
||||
export interface ManagedFieldsEntry {
|
||||
manager?: string;
|
||||
operation?: string;
|
||||
apiVersion?: string;
|
||||
time?: string;
|
||||
fieldsType?: string;
|
||||
subresource?: string;
|
||||
}
|
||||
|
||||
export interface Stars {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
metadata: Metadata;
|
||||
spec: Spec;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
// metadata contains embedded CommonMetadata and can be extended with custom string fields
|
||||
// TODO: use CommonMetadata instead of redefining here; currently needs to be defined here
|
||||
// without external reference as using the CommonMetadata reference breaks thema codegen.
|
||||
export interface Metadata {
|
||||
updateTimestamp: string;
|
||||
createdBy: string;
|
||||
uid: string;
|
||||
creationTimestamp: string;
|
||||
deletionTimestamp?: string;
|
||||
finalizers: string[];
|
||||
resourceVersion: string;
|
||||
generation: number;
|
||||
updatedBy: string;
|
||||
labels: Record<string, string>;
|
||||
}
|
||||
|
||||
export const defaultMetadata = (): Metadata => ({
|
||||
updateTimestamp: "",
|
||||
createdBy: "",
|
||||
uid: "",
|
||||
creationTimestamp: "",
|
||||
finalizers: [],
|
||||
resourceVersion: "",
|
||||
generation: 0,
|
||||
updatedBy: "",
|
||||
labels: {},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
|
||||
export interface Resource {
|
||||
group: string;
|
||||
kind: string;
|
||||
// The set of resources
|
||||
// +listType=set
|
||||
names: string[];
|
||||
}
|
||||
|
||||
export const defaultResource = (): Resource => ({
|
||||
group: "",
|
||||
kind: "",
|
||||
names: [],
|
||||
});
|
||||
|
||||
export interface Spec {
|
||||
resource: Resource[];
|
||||
}
|
||||
|
||||
export const defaultSpec = (): Spec => ({
|
||||
resource: [],
|
||||
});
|
||||
|
||||
@@ -340,7 +340,12 @@ ValueMappingResult: {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -352,7 +357,7 @@ ValueMappingResult: {
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
FieldColorSeriesByMode: "min" | "max" | "last"
|
||||
@@ -377,7 +382,7 @@ FetchOptions: {
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -387,7 +392,7 @@ InfinityOptions: FetchOptions & {
|
||||
datasourceUid: string
|
||||
}
|
||||
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
|
||||
// Action variable type
|
||||
ActionVariableType: "string"
|
||||
|
||||
@@ -113,7 +113,7 @@ DashboardLink: {
|
||||
placement?: DashboardLinkPlacement
|
||||
}
|
||||
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
DashboardLinkPlacement: "inControlsMenu"
|
||||
|
||||
@@ -342,7 +342,12 @@ ValueMappingResult: {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -354,7 +359,7 @@ ValueMappingResult: {
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
FieldColorSeriesByMode: "min" | "max" | "last"
|
||||
@@ -379,7 +384,7 @@ FetchOptions: {
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -389,7 +394,7 @@ InfinityOptions: FetchOptions & {
|
||||
datasourceUid: string
|
||||
}
|
||||
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
|
||||
// Action variable type
|
||||
ActionVariableType: "string"
|
||||
@@ -906,6 +911,7 @@ CustomVariableSpec: {
|
||||
skipUrlSync: bool | *false
|
||||
description?: string
|
||||
allowCustomValue: bool | *true
|
||||
valuesFormat?: "csv" | "json"
|
||||
}
|
||||
|
||||
// Custom variable kind
|
||||
|
||||
@@ -301,8 +301,8 @@ lineage: schemas: [{
|
||||
// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
|
||||
#DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type")
|
||||
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
#DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type")
|
||||
|
||||
// Annotation Query placement. Defines where the annotation query should be displayed.
|
||||
@@ -318,7 +318,7 @@ lineage: schemas: [{
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining this way because we can't generate a go struct that
|
||||
// We are defining this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -330,7 +330,7 @@ lineage: schemas: [{
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -381,7 +381,12 @@ lineage: schemas: [{
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -393,7 +398,7 @@ lineage: schemas: [{
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousViridis|ContinuousMagma|ContinuousPlasma|ContinuousInferno|ContinuousCividis|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
#FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type")
|
||||
|
||||
@@ -301,8 +301,8 @@ lineage: schemas: [{
|
||||
// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
|
||||
#DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type")
|
||||
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
#DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type")
|
||||
|
||||
// Annotation Query placement. Defines where the annotation query should be displayed.
|
||||
@@ -318,7 +318,7 @@ lineage: schemas: [{
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining this way because we can't generate a go struct that
|
||||
// We are defining this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -330,7 +330,7 @@ lineage: schemas: [{
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -381,7 +381,12 @@ lineage: schemas: [{
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -393,7 +398,7 @@ lineage: schemas: [{
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousViridis|ContinuousMagma|ContinuousPlasma|ContinuousInferno|ContinuousCividis|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
#FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type")
|
||||
|
||||
@@ -344,7 +344,12 @@ ValueMappingResult: {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -356,7 +361,7 @@ ValueMappingResult: {
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
FieldColorSeriesByMode: "min" | "max" | "last"
|
||||
@@ -381,7 +386,7 @@ FetchOptions: {
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -391,7 +396,7 @@ InfinityOptions: FetchOptions & {
|
||||
datasourceUid: string
|
||||
}
|
||||
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
|
||||
// Action variable type
|
||||
ActionVariableType: "string"
|
||||
|
||||
@@ -583,7 +583,12 @@ func NewDashboardFieldColor() *DashboardFieldColor {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -602,6 +607,11 @@ const (
|
||||
DashboardFieldColorModeIdThresholds DashboardFieldColorModeId = "thresholds"
|
||||
DashboardFieldColorModeIdPaletteClassic DashboardFieldColorModeId = "palette-classic"
|
||||
DashboardFieldColorModeIdPaletteClassicByName DashboardFieldColorModeId = "palette-classic-by-name"
|
||||
DashboardFieldColorModeIdContinuousViridis DashboardFieldColorModeId = "continuous-viridis"
|
||||
DashboardFieldColorModeIdContinuousMagma DashboardFieldColorModeId = "continuous-magma"
|
||||
DashboardFieldColorModeIdContinuousPlasma DashboardFieldColorModeId = "continuous-plasma"
|
||||
DashboardFieldColorModeIdContinuousInferno DashboardFieldColorModeId = "continuous-inferno"
|
||||
DashboardFieldColorModeIdContinuousCividis DashboardFieldColorModeId = "continuous-cividis"
|
||||
DashboardFieldColorModeIdContinuousGrYlRd DashboardFieldColorModeId = "continuous-GrYlRd"
|
||||
DashboardFieldColorModeIdContinuousRdYlGr DashboardFieldColorModeId = "continuous-RdYlGr"
|
||||
DashboardFieldColorModeIdContinuousBlYlRd DashboardFieldColorModeId = "continuous-BlYlRd"
|
||||
|
||||
@@ -117,7 +117,7 @@ DashboardLink: {
|
||||
placement?: DashboardLinkPlacement
|
||||
}
|
||||
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
DashboardLinkPlacement: "inControlsMenu"
|
||||
|
||||
@@ -346,7 +346,12 @@ ValueMappingResult: {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -358,7 +363,7 @@ ValueMappingResult: {
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
FieldColorSeriesByMode: "min" | "max" | "last"
|
||||
@@ -383,7 +388,7 @@ FetchOptions: {
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -393,7 +398,7 @@ InfinityOptions: FetchOptions & {
|
||||
datasourceUid: string
|
||||
}
|
||||
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
HttpRequestMethod: "GET" | "PUT" | "POST" | "DELETE" | "PATCH"
|
||||
|
||||
// Action variable type
|
||||
ActionVariableType: "string"
|
||||
@@ -910,6 +915,7 @@ CustomVariableSpec: {
|
||||
skipUrlSync: bool | *false
|
||||
description?: string
|
||||
allowCustomValue: bool | *true
|
||||
valuesFormat?: "csv" | "json"
|
||||
}
|
||||
|
||||
// Custom variable kind
|
||||
|
||||
+32
-13
@@ -587,7 +587,12 @@ func NewDashboardFieldColor() *DashboardFieldColor {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -606,6 +611,11 @@ const (
|
||||
DashboardFieldColorModeIdThresholds DashboardFieldColorModeId = "thresholds"
|
||||
DashboardFieldColorModeIdPaletteClassic DashboardFieldColorModeId = "palette-classic"
|
||||
DashboardFieldColorModeIdPaletteClassicByName DashboardFieldColorModeId = "palette-classic-by-name"
|
||||
DashboardFieldColorModeIdContinuousViridis DashboardFieldColorModeId = "continuous-viridis"
|
||||
DashboardFieldColorModeIdContinuousMagma DashboardFieldColorModeId = "continuous-magma"
|
||||
DashboardFieldColorModeIdContinuousPlasma DashboardFieldColorModeId = "continuous-plasma"
|
||||
DashboardFieldColorModeIdContinuousInferno DashboardFieldColorModeId = "continuous-inferno"
|
||||
DashboardFieldColorModeIdContinuousCividis DashboardFieldColorModeId = "continuous-cividis"
|
||||
DashboardFieldColorModeIdContinuousGrYlRd DashboardFieldColorModeId = "continuous-GrYlRd"
|
||||
DashboardFieldColorModeIdContinuousRdYlGr DashboardFieldColorModeId = "continuous-RdYlGr"
|
||||
DashboardFieldColorModeIdContinuousBlYlRd DashboardFieldColorModeId = "continuous-BlYlRd"
|
||||
@@ -1665,18 +1675,19 @@ func NewDashboardCustomVariableKind() *DashboardCustomVariableKind {
|
||||
// Custom variable specification
|
||||
// +k8s:openapi-gen=true
|
||||
type DashboardCustomVariableSpec struct {
|
||||
Name string `json:"name"`
|
||||
Query string `json:"query"`
|
||||
Current DashboardVariableOption `json:"current"`
|
||||
Options []DashboardVariableOption `json:"options"`
|
||||
Multi bool `json:"multi"`
|
||||
IncludeAll bool `json:"includeAll"`
|
||||
AllValue *string `json:"allValue,omitempty"`
|
||||
Label *string `json:"label,omitempty"`
|
||||
Hide DashboardVariableHide `json:"hide"`
|
||||
SkipUrlSync bool `json:"skipUrlSync"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
AllowCustomValue bool `json:"allowCustomValue"`
|
||||
Name string `json:"name"`
|
||||
Query string `json:"query"`
|
||||
Current DashboardVariableOption `json:"current"`
|
||||
Options []DashboardVariableOption `json:"options"`
|
||||
Multi bool `json:"multi"`
|
||||
IncludeAll bool `json:"includeAll"`
|
||||
AllValue *string `json:"allValue,omitempty"`
|
||||
Label *string `json:"label,omitempty"`
|
||||
Hide DashboardVariableHide `json:"hide"`
|
||||
SkipUrlSync bool `json:"skipUrlSync"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
AllowCustomValue bool `json:"allowCustomValue"`
|
||||
ValuesFormat *DashboardCustomVariableSpecValuesFormat `json:"valuesFormat,omitempty"`
|
||||
}
|
||||
|
||||
// NewDashboardCustomVariableSpec creates a new DashboardCustomVariableSpec object.
|
||||
@@ -2091,6 +2102,14 @@ const (
|
||||
DashboardQueryVariableSpecStaticOptionsOrderSorted DashboardQueryVariableSpecStaticOptionsOrder = "sorted"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type DashboardCustomVariableSpecValuesFormat string
|
||||
|
||||
const (
|
||||
DashboardCustomVariableSpecValuesFormatCsv DashboardCustomVariableSpecValuesFormat = "csv"
|
||||
DashboardCustomVariableSpecValuesFormatJson DashboardCustomVariableSpecValuesFormat = "json"
|
||||
)
|
||||
|
||||
// +k8s:openapi-gen=true
|
||||
type DashboardPanelKindOrLibraryPanelKind struct {
|
||||
PanelKind *DashboardPanelKind `json:"PanelKind,omitempty"`
|
||||
|
||||
@@ -1510,6 +1510,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardCustomVariableSpec(ref common.Re
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
"valuesFormat": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Type: []string{"string"},
|
||||
Format: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"name", "query", "current", "options", "multi", "includeAll", "hide", "skipUrlSync", "allowCustomValue"},
|
||||
},
|
||||
|
||||
@@ -28,6 +28,7 @@ type fileWatcher struct {
|
||||
timers map[string]*time.Timer
|
||||
watcher *fsnotify.Watcher
|
||||
logger logging.Logger
|
||||
closed bool
|
||||
}
|
||||
|
||||
// File watcher that buffers events for 100ms before actually firing them
|
||||
@@ -77,22 +78,21 @@ func NewFileWatcher(path string, accept func(string) bool) (FileWatcher, error)
|
||||
|
||||
// Keep watching for changes until the context is done
|
||||
func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) {
|
||||
defer f.cleanup(events)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
close(events)
|
||||
return
|
||||
|
||||
case _, ok := <-f.watcher.Errors:
|
||||
if !ok { // Channel was closed (i.e. Watcher.Close() was called).
|
||||
close(events)
|
||||
return
|
||||
}
|
||||
|
||||
// Read from Events.
|
||||
case e, ok := <-f.watcher.Events:
|
||||
if !ok { // Channel was closed (i.e. Watcher.Close() was called).
|
||||
close(events)
|
||||
return
|
||||
}
|
||||
name := filepath.Base(e.Name)
|
||||
@@ -114,6 +114,11 @@ func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) {
|
||||
if !ok {
|
||||
nameCopy := e.Name
|
||||
t = time.AfterFunc(math.MaxInt64, func() {
|
||||
// before sending the event, check if the watcher has been closed
|
||||
if f.closed {
|
||||
return
|
||||
}
|
||||
|
||||
path, _ := strings.CutPrefix(nameCopy, f.prefix)
|
||||
events <- path
|
||||
|
||||
@@ -128,3 +133,17 @@ func (f *fileWatcher) Watch(ctx context.Context, events chan<- string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stop all pending timers and close the event channel
|
||||
func (f *fileWatcher) cleanup(events chan<- string) {
|
||||
f.timersMu.Lock()
|
||||
defer f.timersMu.Unlock()
|
||||
|
||||
for _, timer := range f.timers {
|
||||
timer.Stop()
|
||||
}
|
||||
f.timers = make(map[string]*time.Timer)
|
||||
|
||||
close(events)
|
||||
f.closed = true
|
||||
}
|
||||
|
||||
@@ -181,6 +181,8 @@ import (
|
||||
//go:generate mockery --name InterfaceName --structname MockImplementationName --inpackage --filename my_implementation_mock.go
|
||||
```
|
||||
|
||||
The current `go:generate` command format used in this repository is only compatible with mockery v2.
|
||||
|
||||
## Globals
|
||||
|
||||
As a general rule of thumb, avoid using global variables, since they make the code difficult to maintain and reason
|
||||
|
||||
@@ -43,7 +43,7 @@ require (
|
||||
github.com/benbjohnson/clock v1.3.5 // @grafana/alerting-backend
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect; @grafana/grafana-developer-enablement-squad
|
||||
github.com/blevesearch/bleve/v2 v2.5.0 // @grafana/grafana-search-and-storage
|
||||
github.com/blevesearch/bleve_index_api v1.2.11 // @grafana/grafana-search-and-storage
|
||||
github.com/blevesearch/bleve_index_api v1.2.7 // @grafana/grafana-search-and-storage
|
||||
github.com/blugelabs/bluge v0.2.2 // @grafana/grafana-backend-group
|
||||
github.com/blugelabs/bluge_segment_api v0.2.0 // @grafana/grafana-backend-group
|
||||
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // @grafana/grafana-backend-group
|
||||
|
||||
@@ -927,8 +927,8 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/blevesearch/bleve/v2 v2.5.0 h1:HzYqBy/5/M9Ul9ESEmXzN/3Jl7YpmWBdHM/+zzv/3k4=
|
||||
github.com/blevesearch/bleve/v2 v2.5.0/go.mod h1:PcJzTPnEynO15dCf9isxOga7YFRa/cMSsbnRwnszXUk=
|
||||
github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s=
|
||||
github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
|
||||
github.com/blevesearch/bleve_index_api v1.2.7 h1:c8r9vmbaYQroAMSGag7zq5gEVPiuXrUQDqfnj7uYZSY=
|
||||
github.com/blevesearch/bleve_index_api v1.2.7/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
|
||||
github.com/blevesearch/geo v0.1.20 h1:paaSpu2Ewh/tn5DKn/FB5SzvH0EWupxHEIwbCk/QPqM=
|
||||
github.com/blevesearch/geo v0.1.20/go.mod h1:DVG2QjwHNMFmjo+ZgzrIq2sfCh6rIHzy9d9d0B59I6w=
|
||||
github.com/blevesearch/go-faiss v1.0.25 h1:lel1rkOUGbT1CJ0YgzKwC7k+XH0XVBHnCVWahdCXk4U=
|
||||
|
||||
+11
@@ -870,6 +870,17 @@ github.com/grafana/grafana-app-sdk/logging v0.39.0/go.mod h1:WhDENSnaGHtyVVwZGVn
|
||||
github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.40.2/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
|
||||
github.com/grafana/gomemcache v0.0.0-20250228145437-da7b95fd2ac1/go.mod h1:j/s0jkda4UXTemDs7Pgw/vMT06alWc42CHisvYac0qw=
|
||||
github.com/grafana/grafana-app-sdk v0.40.1/go.mod h1:4P8h7VB6KcDjX9bAoBQc6IP8iNylxe6bSXLR9gA39gM=
|
||||
github.com/grafana/grafana-app-sdk v0.41.0 h1:SYHN3U7B1myRKY3UZZDkFsue9TDmAOap0UrQVTqtYBU=
|
||||
github.com/grafana/grafana-app-sdk v0.41.0/go.mod h1:Wg/3vEZfok1hhIWiHaaJm+FwkosfO98o8KbeLFEnZpY=
|
||||
github.com/grafana/grafana-app-sdk v0.46.0/go.mod h1:LCTrqR1SwBS13XGVYveBmM7giJDDjzuXK+M9VzPuPWc=
|
||||
github.com/grafana/grafana-app-sdk v0.47.0/go.mod h1:kywXmkppq0oReUMzkjTW8Fq2EBzyN7v914jttTWnWxA=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.38.0/go.mod h1:Y/bvbDhBiV/tkIle9RW49pgfSPIPSON8Q4qjx3pyqDk=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.39.0 h1:3GgN5+dUZYqq74Q+GT9/ET+yo+V54zWQk/Q2/JsJQB4=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.39.0/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.39.1/go.mod h1:WhDENSnaGHtyVVwZGVnAR7YLvh2xlLDYR3D7E6h7XVk=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.40.0/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.43.0/go.mod h1:0xrjKSGY5z+NLGuGsXQpxiCHR4Smu79i/CbAfdkaB1M=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.43.1/go.mod h1:0xrjKSGY5z+NLGuGsXQpxiCHR4Smu79i/CbAfdkaB1M=
|
||||
github.com/grafana/grafana-app-sdk/logging v0.43.2/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA=
|
||||
|
||||
@@ -297,8 +297,8 @@ lineage: schemas: [{
|
||||
// Dashboard Link type. Accepted values are dashboards (to refer to another dashboard) and link (to refer to an external resource)
|
||||
#DashboardLinkType: "link" | "dashboards" @cuetsy(kind="type")
|
||||
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
// Dashboard Link placement. Defines where the link should be displayed.
|
||||
// - "inControlsMenu" renders the link in bottom part of the dashboard controls dropdown menu
|
||||
#DashboardLinkPlacement: "inControlsMenu" @cuetsy(kind="type")
|
||||
|
||||
// Annotation Query placement. Defines where the annotation query should be displayed.
|
||||
@@ -314,7 +314,7 @@ lineage: schemas: [{
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining this way because we can't generate a go struct that
|
||||
// We are defining this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -326,7 +326,7 @@ lineage: schemas: [{
|
||||
url: string
|
||||
body?: string
|
||||
// These are 2D arrays of strings, each representing a key-value pair
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// We are defining them this way because we can't generate a go struct that
|
||||
// that would have exactly two strings in each sub-array
|
||||
queryParams?: [...[...string]]
|
||||
headers?: [...[...string]]
|
||||
@@ -377,7 +377,12 @@ lineage: schemas: [{
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -389,7 +394,7 @@ lineage: schemas: [{
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
|
||||
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades" @cuetsy(kind="enum",memberNames="Thresholds|PaletteClassic|PaletteClassicByName|ContinuousViridis|ContinuousMagma|ContinuousPlasma|ContinuousInferno|ContinuousCividis|ContinuousGrYlRd|ContinuousRdYlGr|ContinuousBlYlRd|ContinuousYlRd|ContinuousBlPu|ContinuousYlBl|ContinuousBlues|ContinuousReds|ContinuousGreens|ContinuousPurples|Fixed|Shades") @grafanamaturity(NeedsExpertReview)
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
#FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type")
|
||||
|
||||
+2
-2
@@ -296,8 +296,8 @@
|
||||
"@grafana/plugin-ui": "^0.11.1",
|
||||
"@grafana/prometheus": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "6.47.1",
|
||||
"@grafana/scenes-react": "6.47.1",
|
||||
"@grafana/scenes": "^6.48.0",
|
||||
"@grafana/scenes-react": "^6.48.0",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/sql": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"@types/string-hash": "1.1.3",
|
||||
"@types/systemjs": "6.15.3",
|
||||
"d3-interpolate": "3.0.1",
|
||||
"d3-scale-chromatic": "3.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"dompurify": "3.3.0",
|
||||
"eventemitter3": "5.0.1",
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { interpolateRgbBasis } from 'd3-interpolate';
|
||||
import {
|
||||
interpolateViridis,
|
||||
interpolateMagma,
|
||||
interpolatePlasma,
|
||||
interpolateInferno,
|
||||
interpolateCividis,
|
||||
} from 'd3-scale-chromatic';
|
||||
import stringHash from 'string-hash';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
@@ -75,6 +82,41 @@ export const fieldColorModeRegistry = new Registry<FieldColorMode>(() => {
|
||||
);
|
||||
},
|
||||
}),
|
||||
new FieldColorSchemeMode({
|
||||
id: FieldColorModeId.ContinuousViridis,
|
||||
name: 'Viridis',
|
||||
isContinuous: true,
|
||||
isByValue: true,
|
||||
interpolator: interpolateViridis,
|
||||
}),
|
||||
new FieldColorSchemeMode({
|
||||
id: FieldColorModeId.ContinuousMagma,
|
||||
name: 'Magma',
|
||||
isContinuous: true,
|
||||
isByValue: true,
|
||||
interpolator: interpolateMagma,
|
||||
}),
|
||||
new FieldColorSchemeMode({
|
||||
id: FieldColorModeId.ContinuousPlasma,
|
||||
name: 'Plasma',
|
||||
isContinuous: true,
|
||||
isByValue: true,
|
||||
interpolator: interpolatePlasma,
|
||||
}),
|
||||
new FieldColorSchemeMode({
|
||||
id: FieldColorModeId.ContinuousInferno,
|
||||
name: 'Inferno',
|
||||
isContinuous: true,
|
||||
isByValue: true,
|
||||
interpolator: interpolateInferno,
|
||||
}),
|
||||
new FieldColorSchemeMode({
|
||||
id: FieldColorModeId.ContinuousCividis,
|
||||
name: 'Cividis',
|
||||
isContinuous: true,
|
||||
isByValue: true,
|
||||
interpolator: interpolateCividis,
|
||||
}),
|
||||
new FieldColorSchemeMode({
|
||||
id: FieldColorModeId.ContinuousGrYlRd,
|
||||
name: 'Green-Yellow-Red',
|
||||
@@ -148,16 +190,27 @@ export const fieldColorModeRegistry = new Registry<FieldColorMode>(() => {
|
||||
];
|
||||
});
|
||||
|
||||
interface FieldColorSchemeModeOptions {
|
||||
interface BaseFieldColorSchemeModeOptions {
|
||||
id: FieldColorModeId;
|
||||
name: string;
|
||||
description?: string;
|
||||
getColors: (theme: GrafanaTheme2) => string[];
|
||||
isContinuous: boolean;
|
||||
isByValue: boolean;
|
||||
useSeriesName?: boolean;
|
||||
}
|
||||
|
||||
interface FieldColorSchemeModeInterpolator extends BaseFieldColorSchemeModeOptions {
|
||||
interpolator: (value: number) => string;
|
||||
getColors?: never;
|
||||
}
|
||||
|
||||
interface FieldColorSchemeModeGetColors extends BaseFieldColorSchemeModeOptions {
|
||||
getColors: (theme: GrafanaTheme2) => string[];
|
||||
interpolator?: never;
|
||||
}
|
||||
|
||||
type FieldColorSchemeModeOptions = FieldColorSchemeModeGetColors | FieldColorSchemeModeInterpolator;
|
||||
|
||||
export class FieldColorSchemeMode implements FieldColorMode {
|
||||
id: FieldColorModeId;
|
||||
name: string;
|
||||
@@ -178,11 +231,15 @@ export class FieldColorSchemeMode implements FieldColorMode {
|
||||
this.isContinuous = options.isContinuous;
|
||||
this.isByValue = options.isByValue;
|
||||
this.useSeriesName = options.useSeriesName;
|
||||
this.interpolator = options.interpolator;
|
||||
}
|
||||
|
||||
getColors(theme: GrafanaTheme2): string[] {
|
||||
if (!this.getNamedColors) {
|
||||
return [];
|
||||
if (!this.interpolator) {
|
||||
return [];
|
||||
}
|
||||
this.getNamedColors = () => new Array(9).fill(0).map((_, i) => this.getInterpolator()(i / 8));
|
||||
}
|
||||
|
||||
if (this.colorCache && this.colorCacheTheme === theme) {
|
||||
@@ -231,12 +288,15 @@ export class FieldColorSchemeMode implements FieldColorMode {
|
||||
|
||||
/** @beta */
|
||||
export function getFieldColorModeForField(field: Field): FieldColorMode {
|
||||
return fieldColorModeRegistry.get(field.config.color?.mode ?? FieldColorModeId.Thresholds);
|
||||
return (
|
||||
fieldColorModeRegistry.getIfExists(field.config.color?.mode) ??
|
||||
fieldColorModeRegistry.get(FieldColorModeId.Thresholds)
|
||||
);
|
||||
}
|
||||
|
||||
/** @beta */
|
||||
export function getFieldColorMode(mode?: FieldColorModeId | string): FieldColorMode {
|
||||
return fieldColorModeRegistry.get(mode ?? FieldColorModeId.Thresholds);
|
||||
return fieldColorModeRegistry.getIfExists(mode) ?? fieldColorModeRegistry.get(FieldColorModeId.Thresholds);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,11 @@ export enum FieldColorModeId {
|
||||
ContinuousReds = 'continuous-reds',
|
||||
ContinuousGreens = 'continuous-greens',
|
||||
ContinuousPurples = 'continuous-purples',
|
||||
ContinuousViridis = 'continuous-viridis',
|
||||
ContinuousMagma = 'continuous-magma',
|
||||
ContinuousPlasma = 'continuous-plasma',
|
||||
ContinuousInferno = 'continuous-inferno',
|
||||
ContinuousCividis = 'continuous-cividis',
|
||||
Fixed = 'fixed',
|
||||
Shades = 'shades',
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@ export interface IntervalVariableModel extends VariableWithOptions {
|
||||
|
||||
export interface CustomVariableModel extends VariableWithMultiSupport {
|
||||
type: 'custom';
|
||||
valuesFormat?: 'csv' | 'json';
|
||||
}
|
||||
|
||||
export interface DataSourceVariableModel extends VariableWithMultiSupport {
|
||||
|
||||
@@ -8,6 +8,7 @@ export const grafanaESModules = [
|
||||
'd3',
|
||||
'd3-color',
|
||||
'd3-interpolate',
|
||||
'd3-scale-chromatic',
|
||||
'delaunator',
|
||||
'get-user-locale',
|
||||
'internmap',
|
||||
|
||||
@@ -475,7 +475,12 @@ export type VariableType = ('query' | 'adhoc' | 'groupby' | 'constant' | 'dataso
|
||||
* `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
* `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
* `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
* `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
* `continuous-viridis`: Continuous Viridis palette mode
|
||||
* `continuous-magma`: Continuous Magma palette mode
|
||||
* `continuous-plasma`: Continuous Plasma palette mode
|
||||
* `continuous-inferno`: Continuous Inferno palette mode
|
||||
* `continuous-cividis`: Continuous Cividis palette mode
|
||||
* `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
* `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
* `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
* `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -492,11 +497,16 @@ export enum FieldColorModeId {
|
||||
ContinuousBlPu = 'continuous-BlPu',
|
||||
ContinuousBlYlRd = 'continuous-BlYlRd',
|
||||
ContinuousBlues = 'continuous-blues',
|
||||
ContinuousCividis = 'continuous-cividis',
|
||||
ContinuousGrYlRd = 'continuous-GrYlRd',
|
||||
ContinuousGreens = 'continuous-greens',
|
||||
ContinuousInferno = 'continuous-inferno',
|
||||
ContinuousMagma = 'continuous-magma',
|
||||
ContinuousPlasma = 'continuous-plasma',
|
||||
ContinuousPurples = 'continuous-purples',
|
||||
ContinuousRdYlGr = 'continuous-RdYlGr',
|
||||
ContinuousReds = 'continuous-reds',
|
||||
ContinuousViridis = 'continuous-viridis',
|
||||
ContinuousYlBl = 'continuous-YlBl',
|
||||
ContinuousYlRd = 'continuous-YlRd',
|
||||
Fixed = 'fixed',
|
||||
|
||||
@@ -316,6 +316,7 @@ export const handyTestingSchema: Spec = {
|
||||
query: 'option1, option2',
|
||||
skipUrlSync: false,
|
||||
allowCustomValue: true,
|
||||
valuesFormat: 'csv',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -334,7 +334,12 @@ ValueMappingResult: {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -346,7 +351,7 @@ ValueMappingResult: {
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
FieldColorModeId: "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades"
|
||||
|
||||
// Defines how to assign a series color from "by value" color schemes. For example for an aggregated data points like a timeseries, the color can be assigned by the min, max or last value.
|
||||
FieldColorSeriesByMode: "min" | "max" | "last"
|
||||
|
||||
@@ -327,7 +327,7 @@ export interface FieldConfig {
|
||||
description?: string;
|
||||
// An explicit path to the field in the datasource. When the frame meta includes a path,
|
||||
// This will default to `${frame.meta.path}/${field.name}
|
||||
//
|
||||
//
|
||||
// When defined, this value can be used as an identifier within the datasource scope, and
|
||||
// may be used to update the results
|
||||
path?: string;
|
||||
@@ -529,7 +529,12 @@ export const defaultFieldColor = (): FieldColor => ({
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -541,7 +546,7 @@ export const defaultFieldColor = (): FieldColor => ({
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades";
|
||||
export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades";
|
||||
|
||||
export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds");
|
||||
|
||||
|
||||
@@ -489,7 +489,12 @@ export const defaultFieldColor = (): FieldColor => ({
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -501,7 +506,7 @@ export const defaultFieldColor = (): FieldColor => ({
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades";
|
||||
export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades";
|
||||
|
||||
export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds");
|
||||
|
||||
|
||||
@@ -496,7 +496,12 @@ export const defaultFieldColor = (): FieldColor => ({
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -508,7 +513,7 @@ export const defaultFieldColor = (): FieldColor => ({
|
||||
// `continuous-purples`: Continuous Purple palette mode
|
||||
// `shades`: Shades of a single color. Specify a single color, useful in an override rule.
|
||||
// `fixed`: Fixed color mode. Specify a single color, useful in an override rule.
|
||||
export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades";
|
||||
export type FieldColorModeId = "thresholds" | "palette-classic" | "palette-classic-by-name" | "continuous-viridis" | "continuous-magma" | "continuous-plasma" | "continuous-inferno" | "continuous-cividis" | "continuous-GrYlRd" | "continuous-RdYlGr" | "continuous-BlYlRd" | "continuous-YlRd" | "continuous-BlPu" | "continuous-YlBl" | "continuous-blues" | "continuous-reds" | "continuous-greens" | "continuous-purples" | "fixed" | "shades";
|
||||
|
||||
export const defaultFieldColorModeId = (): FieldColorModeId => ("thresholds");
|
||||
|
||||
@@ -1330,6 +1335,7 @@ export interface CustomVariableSpec {
|
||||
skipUrlSync: boolean;
|
||||
description?: string;
|
||||
allowCustomValue: boolean;
|
||||
valuesFormat?: "csv" | "json";
|
||||
}
|
||||
|
||||
export const defaultCustomVariableSpec = (): CustomVariableSpec => ({
|
||||
|
||||
@@ -23,13 +23,32 @@ describe('Sidebar', () => {
|
||||
// Verify pane is closed
|
||||
expect(screen.queryByTestId('sidebar-pane-header-title')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Can persist docked state', async () => {
|
||||
const { unmount } = render(<TestSetup persistanceKey="test" />);
|
||||
|
||||
act(() => screen.getByLabelText('Settings').click());
|
||||
act(() => screen.getByLabelText('Dock').click());
|
||||
|
||||
unmount();
|
||||
|
||||
render(<TestSetup persistanceKey="test" />);
|
||||
|
||||
act(() => screen.getByLabelText('Settings').click());
|
||||
expect(screen.getByLabelText('Undock')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function TestSetup() {
|
||||
interface TestSetupProps {
|
||||
persistanceKey?: string;
|
||||
}
|
||||
|
||||
function TestSetup({ persistanceKey }: TestSetupProps) {
|
||||
const [openPane, setOpenPane] = React.useState('');
|
||||
const contextValue = useSidebar({
|
||||
position: 'right',
|
||||
hasOpenPane: openPane !== '',
|
||||
persistanceKey,
|
||||
onClosePane: () => setOpenPane(''),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { clamp } from 'lodash';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { store } from '@grafana/data';
|
||||
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
|
||||
export type SidebarPosition = 'left' | 'right';
|
||||
@@ -30,7 +32,10 @@ export interface UseSideBarOptions {
|
||||
hasOpenPane?: boolean;
|
||||
position?: SidebarPosition;
|
||||
tabsMode?: boolean;
|
||||
compactDefault?: boolean;
|
||||
/** Initial state for compact mode */
|
||||
defaultToCompact?: boolean;
|
||||
/** Initial state for docked mode */
|
||||
defaultToDocked?: boolean;
|
||||
/** defaults to 2 grid units (16px) */
|
||||
bottomMargin?: number;
|
||||
/** defaults to 2 grid units (16px) */
|
||||
@@ -39,6 +44,11 @@ export interface UseSideBarOptions {
|
||||
contentMargin?: number;
|
||||
/** Called when pane is closed or clicked outside of (in undocked mode) */
|
||||
onClosePane?: () => void;
|
||||
/**
|
||||
* Optional key to use for persisting sidebar state (docked / compact / size)
|
||||
* Can only be app name as the final local storag key will be `grafana.ui.sidebar.{persistanceKey}.{docked|compact|size}`
|
||||
*/
|
||||
persistanceKey?: string;
|
||||
}
|
||||
|
||||
export const SIDE_BAR_WIDTH_ICON_ONLY = 5;
|
||||
@@ -48,21 +58,30 @@ export function useSidebar({
|
||||
hasOpenPane,
|
||||
position = 'right',
|
||||
tabsMode,
|
||||
compactDefault = true,
|
||||
defaultToCompact = true,
|
||||
defaultToDocked = false,
|
||||
bottomMargin = 2,
|
||||
edgeMargin = 2,
|
||||
contentMargin = 2,
|
||||
persistanceKey,
|
||||
onClosePane,
|
||||
}: UseSideBarOptions): SidebarContextValue {
|
||||
const theme = useTheme2();
|
||||
const [isDocked, setIsDocked] = React.useState(false);
|
||||
const [paneWidth, setPaneWidth] = React.useState(280);
|
||||
const [compact, setCompact] = React.useState(compactDefault);
|
||||
|
||||
const [isDocked, setIsDocked] = useSidebarSavedState(persistanceKey, 'docked', defaultToDocked);
|
||||
const [compact, setCompact] = useSidebarSavedState(persistanceKey, 'compact', defaultToCompact);
|
||||
const [paneWidth, setPaneWidth] = useSidebarSavedState(persistanceKey, 'size', 280);
|
||||
|
||||
// Used to accumulate drag distance to know when to change compact mode
|
||||
const [_, setCompactDrag] = React.useState(0);
|
||||
|
||||
const onToggleDock = useCallback(() => setIsDocked((prev) => !prev), []);
|
||||
const onToggleDock = useCallback(() => {
|
||||
setIsDocked((prev) => {
|
||||
return !prev;
|
||||
});
|
||||
}, [setIsDocked]);
|
||||
|
||||
// Calculate how much space the outer wrapper needs to reserve for the sidebar toolbar + pane (if docked)
|
||||
const prop = position === 'right' ? 'paddingRight' : 'paddingLeft';
|
||||
const toolbarWidth =
|
||||
((compact ? SIDE_BAR_WIDTH_ICON_ONLY : SIDE_BAR_WIDTH_WITH_TEXT) + edgeMargin + contentMargin) *
|
||||
@@ -82,10 +101,10 @@ export function useSidebar({
|
||||
setCompactDrag((prevDrag) => {
|
||||
const newDrag = prevDrag + diff;
|
||||
if (newDrag < -20 && !compact) {
|
||||
setCompact(true);
|
||||
setCompact(() => true);
|
||||
return 0;
|
||||
} else if (newDrag > 20 && compact) {
|
||||
setCompact(false);
|
||||
setCompact(() => false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -98,7 +117,7 @@ export function useSidebar({
|
||||
return clamp(prevWidth + diff, 100, 500);
|
||||
});
|
||||
},
|
||||
[hasOpenPane, compact]
|
||||
[hasOpenPane, setCompact, setPaneWidth, compact]
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -117,3 +136,53 @@ export function useSidebar({
|
||||
onClosePane,
|
||||
};
|
||||
}
|
||||
|
||||
function useSidebarSavedState<T = number | boolean>(
|
||||
persistanceKey: string | undefined,
|
||||
subKey: string,
|
||||
defaultValue: T
|
||||
) {
|
||||
const [state, setState] = React.useState<T>(() => {
|
||||
if (!persistanceKey) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'boolean') {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return store.getBool(`grafana.ui.sidebar.${persistanceKey}.${subKey}`, defaultValue) as T;
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'number') {
|
||||
const value = Number.parseInt(store.get(`grafana.ui.sidebar.${persistanceKey}.${subKey}`), 10);
|
||||
if (Number.isNaN(value)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
return value as T;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
});
|
||||
|
||||
const setPersisted = useCallback(
|
||||
(cb: (prevState: T) => T) => {
|
||||
setState((prevState) => {
|
||||
const newState = cb(prevState);
|
||||
|
||||
if (!persistanceKey) {
|
||||
return newState;
|
||||
}
|
||||
|
||||
if (persistanceKey) {
|
||||
store.set(`grafana.ui.sidebar.${persistanceKey}.${subKey}`, String(newState));
|
||||
}
|
||||
|
||||
return newState;
|
||||
});
|
||||
},
|
||||
[persistanceKey, subKey]
|
||||
);
|
||||
|
||||
return [state, setPersisted] as const;
|
||||
}
|
||||
|
||||
@@ -150,6 +150,10 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/connections/datasources/edit/*", authorize(datasources.EditPageAccess), hs.Index)
|
||||
r.Get("/connections", authorize(datasources.ConfigurationPageAccess), hs.Index)
|
||||
r.Get("/connections/add-new-connection", authorize(datasources.ConfigurationPageAccess), hs.Index)
|
||||
r.Get("/connections/stacks", authorize(datasources.ConfigurationPageAccess), hs.Index)
|
||||
r.Get("/connections/stacks/new", authorize(datasources.ConfigurationPageAccess), hs.Index)
|
||||
r.Get("/connections/stacks/edit/*", authorize(datasources.ConfigurationPageAccess), hs.Index)
|
||||
|
||||
// Plugin details pages
|
||||
r.Get("/connections/datasources/:id", middleware.CanAdminPlugins(hs.Cfg, hs.AccessControl), hs.Index)
|
||||
r.Get("/connections/datasources/:id/page/:page", middleware.CanAdminPlugins(hs.Cfg, hs.AccessControl), hs.Index)
|
||||
|
||||
+2
-4
@@ -30,10 +30,8 @@ func (hs *HTTPServer) registerSwaggerUI(r routing.RouteRegister) {
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"Nonce": c.RequestNonce,
|
||||
"Assets": assets,
|
||||
"FavIcon": "public/img/fav32.png",
|
||||
"AppleTouchIcon": "public/img/apple-touch-icon.png",
|
||||
"Nonce": c.RequestNonce,
|
||||
"Assets": assets,
|
||||
}
|
||||
if hs.Cfg.CSPEnabled {
|
||||
data["CSPEnabled"] = true
|
||||
|
||||
+11
-1
@@ -619,7 +619,12 @@ func NewFieldColor() *FieldColor {
|
||||
// `thresholds`: From thresholds. Informs Grafana to take the color from the matching threshold
|
||||
// `palette-classic`: Classic palette. Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `palette-classic-by-name`: Classic palette (by name). Grafana will assign color by looking up a color in a palette by series name. Useful for Graphs and pie charts and other categorical data visualizations
|
||||
// `continuous-GrYlRd`: ontinuous Green-Yellow-Red palette mode
|
||||
// `continuous-viridis`: Continuous Viridis palette mode
|
||||
// `continuous-magma`: Continuous Magma palette mode
|
||||
// `continuous-plasma`: Continuous Plasma palette mode
|
||||
// `continuous-inferno`: Continuous Inferno palette mode
|
||||
// `continuous-cividis`: Continuous Cividis palette mode
|
||||
// `continuous-GrYlRd`: Continuous Green-Yellow-Red palette mode
|
||||
// `continuous-RdYlGr`: Continuous Red-Yellow-Green palette mode
|
||||
// `continuous-BlYlRd`: Continuous Blue-Yellow-Red palette mode
|
||||
// `continuous-YlRd`: Continuous Yellow-Red palette mode
|
||||
@@ -637,6 +642,11 @@ const (
|
||||
FieldColorModeIdThresholds FieldColorModeId = "thresholds"
|
||||
FieldColorModeIdPaletteClassic FieldColorModeId = "palette-classic"
|
||||
FieldColorModeIdPaletteClassicByName FieldColorModeId = "palette-classic-by-name"
|
||||
FieldColorModeIdContinuousViridis FieldColorModeId = "continuous-viridis"
|
||||
FieldColorModeIdContinuousMagma FieldColorModeId = "continuous-magma"
|
||||
FieldColorModeIdContinuousPlasma FieldColorModeId = "continuous-plasma"
|
||||
FieldColorModeIdContinuousInferno FieldColorModeId = "continuous-inferno"
|
||||
FieldColorModeIdContinuousCividis FieldColorModeId = "continuous-cividis"
|
||||
FieldColorModeIdContinuousGrYlRd FieldColorModeId = "continuous-GrYlRd"
|
||||
FieldColorModeIdContinuousRdYlGr FieldColorModeId = "continuous-RdYlGr"
|
||||
FieldColorModeIdContinuousBlYlRd FieldColorModeId = "continuous-BlYlRd"
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/datasources/service/client"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
var _ builder.APIGroupValidation = (*DatasourceStacksValidator)(nil)
|
||||
|
||||
type DatasourceStacksValidator struct {
|
||||
dsClient client.DataSourceConnectionClient
|
||||
}
|
||||
|
||||
func GetDatasourceStacksValidator(dsClient client.DataSourceConnectionClient) builder.APIGroupValidation {
|
||||
return &DatasourceStacksValidator{dsClient: dsClient}
|
||||
}
|
||||
|
||||
func (v *DatasourceStacksValidator) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
|
||||
obj := a.GetObject()
|
||||
operation := a.GetOperation()
|
||||
|
||||
if operation == admission.Connect {
|
||||
return fmt.Errorf("Connect operation is not allowed (%s %s)", a.GetName(), a.GetKind().GroupVersion().String())
|
||||
}
|
||||
|
||||
if operation != admission.Create && operation != admission.Update {
|
||||
return nil
|
||||
}
|
||||
|
||||
cast, ok := obj.(*collections.DataSourceStack)
|
||||
if !ok {
|
||||
return fmt.Errorf("object is not of type *collections.DataSourceStack (%s %s)", a.GetName(), a.GetKind().GroupVersion().String())
|
||||
}
|
||||
|
||||
// get the keys from the template
|
||||
template := cast.Spec.Template
|
||||
|
||||
templateNames := map[string]bool{}
|
||||
for _, item := range template {
|
||||
// template items cannot be empty
|
||||
if item.Group == "" || item.Name == "" {
|
||||
return fmt.Errorf("template items cannot be empty (%s %s)", a.GetName(), a.GetKind().GroupVersion().String())
|
||||
}
|
||||
// template names must be unique
|
||||
if _, exists := templateNames[item.Name]; exists {
|
||||
return fmt.Errorf("template item names must be unique. name '%s' already exists (%s %s)", item.Name, a.GetName(), a.GetKind().GroupVersion().String())
|
||||
}
|
||||
templateNames[item.Name] = true
|
||||
}
|
||||
|
||||
// for each mode, check that the keys are in the template
|
||||
modes := cast.Spec.Modes
|
||||
|
||||
for _, mode := range modes {
|
||||
for key, item := range mode.Definition {
|
||||
// if a key is not in the template, return an error
|
||||
if _, ok := template[key]; !ok {
|
||||
return fmt.Errorf("key '%s' is not in the DataSourceStack template (%s %s)", key, a.GetName(), a.GetKind().GroupVersion().String())
|
||||
}
|
||||
|
||||
exists, err := v.checkDatasourceExists(ctx, item.DataSourceRef)
|
||||
if err != nil || !exists {
|
||||
return fmt.Errorf("datasource '%s' in group '%s' does not exist (%s %s): %w", item.DataSourceRef, template[key].Group, a.GetName(), a.GetKind().GroupVersion().String(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *DatasourceStacksValidator) checkDatasourceExists(ctx context.Context, name string) (bool, error) {
|
||||
dsConn, err := v.dsClient.GetByUID(ctx, name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if dsConn == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package collections_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
collectionsv1alpha1 "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||
queryv0alpha1 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/collections"
|
||||
datasourcesclient "github.com/grafana/grafana/pkg/services/datasources/service/client"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
func TestDataSourceValidator_Validate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
operation admission.Operation
|
||||
object runtime.Object
|
||||
needMockDSClient bool // only set to true if you expect to make a call to the datasource client
|
||||
dsClientReturnValue *queryv0alpha1.DataSourceConnection
|
||||
dsClientReturnError error
|
||||
expectError bool
|
||||
errorMsg string
|
||||
}{
|
||||
{
|
||||
name: "should return no error for invalid kind",
|
||||
operation: admission.Delete,
|
||||
object: &collectionsv1alpha1.Stars{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "should return error for Connect operation",
|
||||
operation: admission.Connect,
|
||||
object: &collectionsv1alpha1.DataSourceStack{},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "template items cannot be empty",
|
||||
operation: admission.Create,
|
||||
object: &collectionsv1alpha1.DataSourceStack{
|
||||
Spec: collectionsv1alpha1.DataSourceStackSpec{
|
||||
Template: collectionsv1alpha1.DataSourceStackTemplateSpec{
|
||||
"key1": collectionsv1alpha1.DataSourceStackDataSourceStackTemplateItem{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "template items cannot be empty (test-datasourcestack collections.grafana.app/v1alpha1)",
|
||||
},
|
||||
{
|
||||
name: "template item name keys must be unique",
|
||||
operation: admission.Create,
|
||||
object: &collectionsv1alpha1.DataSourceStack{
|
||||
Spec: collectionsv1alpha1.DataSourceStackSpec{
|
||||
Template: collectionsv1alpha1.DataSourceStackTemplateSpec{
|
||||
"key1": collectionsv1alpha1.DataSourceStackDataSourceStackTemplateItem{
|
||||
Name: "foo",
|
||||
Group: "foo.grafana",
|
||||
},
|
||||
"key2": collectionsv1alpha1.DataSourceStackDataSourceStackTemplateItem{
|
||||
Name: "foo",
|
||||
Group: "foo.grafana",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "template item names must be unique. name 'foo' already exists (test-datasourcestack collections.grafana.app/v1alpha1)",
|
||||
},
|
||||
{
|
||||
name: "mode keys must exist in the template",
|
||||
operation: admission.Create,
|
||||
object: &collectionsv1alpha1.DataSourceStack{
|
||||
Spec: collectionsv1alpha1.DataSourceStackSpec{
|
||||
Template: collectionsv1alpha1.DataSourceStackTemplateSpec{
|
||||
"key1": collectionsv1alpha1.DataSourceStackDataSourceStackTemplateItem{
|
||||
Name: "foo",
|
||||
Group: "foo.grafana",
|
||||
},
|
||||
},
|
||||
Modes: []collectionsv1alpha1.DataSourceStackModeSpec{
|
||||
{
|
||||
Name: "prod",
|
||||
Definition: collectionsv1alpha1.DataSourceStackMode{
|
||||
"notintemplate": collectionsv1alpha1.DataSourceStackModeItem{
|
||||
DataSourceRef: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
errorMsg: "key 'notintemplate' is not in the DataSourceStack template (test-datasourcestack collections.grafana.app/v1alpha1)",
|
||||
},
|
||||
{
|
||||
name: "error if data source does not exist",
|
||||
operation: admission.Create,
|
||||
object: &collectionsv1alpha1.DataSourceStack{
|
||||
Spec: collectionsv1alpha1.DataSourceStackSpec{
|
||||
Template: collectionsv1alpha1.DataSourceStackTemplateSpec{
|
||||
"key1": collectionsv1alpha1.DataSourceStackDataSourceStackTemplateItem{
|
||||
Name: "foo",
|
||||
Group: "foo.grafana",
|
||||
},
|
||||
},
|
||||
Modes: []collectionsv1alpha1.DataSourceStackModeSpec{
|
||||
{
|
||||
Name: "prod",
|
||||
Definition: collectionsv1alpha1.DataSourceStackMode{
|
||||
"key1": collectionsv1alpha1.DataSourceStackModeItem{
|
||||
DataSourceRef: "ref",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
needMockDSClient: true,
|
||||
dsClientReturnValue: nil, // no result - this is the default anyway
|
||||
expectError: true,
|
||||
errorMsg: "datasource 'ref' in group 'foo.grafana' does not exist (test-datasourcestack collections.grafana.app/v1alpha1)",
|
||||
},
|
||||
{
|
||||
name: "valid request",
|
||||
operation: admission.Create,
|
||||
object: &collectionsv1alpha1.DataSourceStack{
|
||||
Spec: collectionsv1alpha1.DataSourceStackSpec{
|
||||
Template: collectionsv1alpha1.DataSourceStackTemplateSpec{
|
||||
"key1": collectionsv1alpha1.DataSourceStackDataSourceStackTemplateItem{
|
||||
Name: "foo",
|
||||
Group: "foo.grafana",
|
||||
},
|
||||
},
|
||||
Modes: []collectionsv1alpha1.DataSourceStackModeSpec{
|
||||
{
|
||||
Name: "prod",
|
||||
Definition: collectionsv1alpha1.DataSourceStackMode{
|
||||
"key1": collectionsv1alpha1.DataSourceStackModeItem{
|
||||
DataSourceRef: "ref",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
needMockDSClient: true,
|
||||
dsClientReturnValue: &queryv0alpha1.DataSourceConnection{}, // returning any non-nil value will pass validation
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
attrs := &FakeAdmissionAttributes{
|
||||
Operation: tt.operation,
|
||||
Object: tt.object,
|
||||
Name: "test-datasourcestack",
|
||||
Kind: schema.GroupVersionKind{Group: "collections.grafana.app", Version: "v1alpha1", Kind: "DataSourceStack"},
|
||||
}
|
||||
|
||||
var client *datasourcesclient.MockDataSourceConnectionClient
|
||||
if tt.needMockDSClient {
|
||||
client = datasourcesclient.NewMockDataSourceConnectionClient(t)
|
||||
client.On("GetByUID", mock.Anything, mock.Anything).Return(tt.dsClientReturnValue, tt.dsClientReturnError)
|
||||
}
|
||||
|
||||
validator := collections.GetDatasourceStacksValidator(client)
|
||||
err := validator.Validate(ctx, attrs, nil)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
if tt.errorMsg != "" {
|
||||
assert.Contains(t, err.Error(), tt.errorMsg)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type FakeAdmissionAttributes struct {
|
||||
admission.Attributes
|
||||
Operation admission.Operation
|
||||
Object runtime.Object
|
||||
Name string
|
||||
Kind schema.GroupVersionKind
|
||||
}
|
||||
|
||||
func (m *FakeAdmissionAttributes) GetOperation() admission.Operation {
|
||||
return m.Operation
|
||||
}
|
||||
|
||||
func (m *FakeAdmissionAttributes) GetObject() runtime.Object {
|
||||
return m.Object
|
||||
}
|
||||
|
||||
func (m *FakeAdmissionAttributes) GetName() string {
|
||||
return m.Name
|
||||
}
|
||||
|
||||
func (m *FakeAdmissionAttributes) GetKind() schema.GroupVersionKind {
|
||||
return m.Kind
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package collections
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
@@ -14,13 +16,16 @@ import (
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
|
||||
collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/collections/legacy"
|
||||
"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/builder"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
datasourcesClient "github.com/grafana/grafana/pkg/services/datasources/service/client"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/star"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -29,13 +34,15 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
_ builder.APIGroupBuilder = (*APIBuilder)(nil)
|
||||
_ builder.APIGroupMutation = (*APIBuilder)(nil)
|
||||
_ builder.APIGroupBuilder = (*APIBuilder)(nil)
|
||||
_ builder.APIGroupMutation = (*APIBuilder)(nil)
|
||||
_ builder.APIGroupValidation = (*APIBuilder)(nil)
|
||||
)
|
||||
|
||||
type APIBuilder struct {
|
||||
authorizer authorizer.Authorizer
|
||||
legacyStars *legacy.DashboardStarsStorage
|
||||
authorizer authorizer.Authorizer
|
||||
legacyStars *legacy.DashboardStarsStorage
|
||||
datasourceStacksValidator builder.APIGroupValidation
|
||||
}
|
||||
|
||||
func RegisterAPIService(
|
||||
@@ -45,6 +52,8 @@ func RegisterAPIService(
|
||||
stars star.Service,
|
||||
users user.Service,
|
||||
apiregistration builder.APIRegistrar,
|
||||
dsConnClientFactory datasourcesClient.DataSourceConnectionClientFactory,
|
||||
restConfigProvider apiserver.RestConfigProvider,
|
||||
) *APIBuilder {
|
||||
// Requires development settings and clearly experimental
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
@@ -52,11 +61,15 @@ func RegisterAPIService(
|
||||
return nil
|
||||
}
|
||||
|
||||
dsConnClient := dsConnClientFactory(restConfigProvider)
|
||||
|
||||
sql := legacy.NewLegacySQL(legacysql.NewDatabaseProvider(db))
|
||||
builder := &APIBuilder{
|
||||
datasourceStacksValidator: GetDatasourceStacksValidator(dsConnClient),
|
||||
authorizer: &utils.AuthorizeFromName{
|
||||
Resource: map[string][]utils.ResourceOwner{
|
||||
"stars": {utils.UserResourceOwner},
|
||||
"stars": {utils.UserResourceOwner},
|
||||
"datasources": {utils.UserResourceOwner},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -94,28 +107,60 @@ func (b *APIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupI
|
||||
storage := map[string]rest.Storage{}
|
||||
|
||||
// Configure Stars Dual writer
|
||||
resource := collections.StarsResourceInfo
|
||||
starsResource := collections.StarsResourceInfo
|
||||
var stars grafanarest.Storage
|
||||
stars, err := grafanaregistry.NewRegistryStore(opts.Scheme, resource, opts.OptsGetter)
|
||||
stars, err := grafanaregistry.NewRegistryStore(opts.Scheme, starsResource, opts.OptsGetter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stars = &starStorage{Storage: stars} // wrap List so we only return one value
|
||||
if b.legacyStars != nil && opts.DualWriteBuilder != nil {
|
||||
stars, err = opts.DualWriteBuilder(resource.GroupResource(), b.legacyStars, stars)
|
||||
stars, err = opts.DualWriteBuilder(starsResource.GroupResource(), b.legacyStars, stars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
storage[resource.StoragePath()] = stars
|
||||
storage[resource.StoragePath("update")] = &starsREST{store: stars}
|
||||
storage[starsResource.StoragePath()] = stars
|
||||
storage[starsResource.StoragePath("update")] = &starsREST{store: stars}
|
||||
|
||||
// no need for dual writer for a kind that does not exist in the legacy database
|
||||
resourceInfo := collections.DatasourceStacksResourceInfo
|
||||
datasourcesStorage, err := grafanaregistry.NewRegistryStore(opts.Scheme, resourceInfo, opts.OptsGetter)
|
||||
storage[resourceInfo.StoragePath()] = datasourcesStorage
|
||||
|
||||
apiGroupInfo.VersionedResourcesStorageMap[collections.APIVersion] = storage
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *APIBuilder) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) {
|
||||
if a.GetKind().Group == collections.DatasourceStacksResourceInfo.GroupResource().Group {
|
||||
return b.datasourceStacksValidator.Validate(ctx, a, o)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *APIBuilder) GetAuthorizer() authorizer.Authorizer {
|
||||
return b.authorizer
|
||||
|
||||
return authorizer.AuthorizerFunc(
|
||||
func(ctx context.Context, attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
if attr.GetResource() == "stars" {
|
||||
return b.authorizer.Authorize(ctx, attr)
|
||||
}
|
||||
|
||||
// datasources auth branch starts
|
||||
if !attr.IsResourceRequest() {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
// require a user
|
||||
_, err = identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "valid user is required", err
|
||||
}
|
||||
|
||||
// TODO make the auth more restrictive
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func (b *APIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
|
||||
|
||||
@@ -85,7 +85,7 @@ func RegisterAPIService(
|
||||
accessControl,
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes),
|
||||
false,
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -28,11 +29,11 @@ var (
|
||||
// Get all datasource connections -- this will be backed by search or duplicated resource in unified storage
|
||||
type DataSourceConnectionProvider interface {
|
||||
// Get gets a specific datasource (that the user in context can see)
|
||||
// The name is {group}:{name}, see /pkg/apis/query/v0alpha1/connection.go#L34
|
||||
// The name is the legacy datasource UID.
|
||||
GetConnection(ctx context.Context, namespace string, name string) (*queryV0.DataSourceConnection, error)
|
||||
|
||||
// List lists all data sources the user in context can see
|
||||
ListConnections(ctx context.Context, namespace string) (*queryV0.DataSourceConnectionList, error)
|
||||
// List lists all data sources the user in context can see. Optional field selectors can filter the results.
|
||||
ListConnections(ctx context.Context, namespace string, fieldSelector fields.Selector) (*queryV0.DataSourceConnectionList, error)
|
||||
}
|
||||
|
||||
type connectionAccess struct {
|
||||
@@ -74,7 +75,11 @@ func (s *connectionAccess) Get(ctx context.Context, name string, options *metav1
|
||||
}
|
||||
|
||||
func (s *connectionAccess) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
return s.connections.ListConnections(ctx, request.NamespaceValue(ctx))
|
||||
var fs fields.Selector
|
||||
if options != nil && options.FieldSelector != nil {
|
||||
fs = options.FieldSelector
|
||||
}
|
||||
return s.connections.ListConnections(ctx, request.NamespaceValue(ctx), fs)
|
||||
}
|
||||
|
||||
type connectionsProvider struct {
|
||||
@@ -103,19 +108,47 @@ func (q *connectionsProvider) GetConnection(ctx context.Context, namespace strin
|
||||
return q.asConnection(ds, namespace)
|
||||
}
|
||||
|
||||
func (q *connectionsProvider) ListConnections(ctx context.Context, namespace string) (*queryV0.DataSourceConnectionList, error) {
|
||||
func (q *connectionsProvider) ListConnections(ctx context.Context, namespace string, fieldSelector fields.Selector) (*queryV0.DataSourceConnectionList, error) {
|
||||
ns, err := authlib.ParseNamespace(namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dss, err := q.dsService.GetDataSources(ctx, &datasources.GetDataSourcesQuery{
|
||||
OrgID: ns.OrgID,
|
||||
DataSourceLimit: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var dss []*datasources.DataSource
|
||||
// if fieldSelector is not nil, find any uids in the metadata.name field and
|
||||
// use them in the query
|
||||
if fieldSelector != nil && !fieldSelector.Empty() {
|
||||
uids := []string{}
|
||||
for _, req := range fieldSelector.Requirements() {
|
||||
if req.Field == "metadata.name" {
|
||||
uids = append(uids, req.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// We don't have a way to fetch a subset of datasources by UID in the legacy
|
||||
// datasource service, so fetch them one by one.
|
||||
if len(uids) > 0 {
|
||||
for _, uid := range uids {
|
||||
ds, err := q.dsService.GetDataSource(ctx, &datasources.GetDataSourceQuery{
|
||||
UID: uid,
|
||||
OrgID: ns.OrgID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dss = append(dss, ds)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dss, err = q.dsService.GetDataSources(ctx, &datasources.GetDataSourcesQuery{
|
||||
OrgID: ns.OrgID,
|
||||
DataSourceLimit: 10000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
result := &queryV0.DataSourceConnectionList{
|
||||
Items: []queryV0.DataSourceConnection{},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package annotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1"
|
||||
)
|
||||
|
||||
type memoryStore struct {
|
||||
mu sync.RWMutex
|
||||
data map[string]*annotationV0.Annotation
|
||||
}
|
||||
|
||||
func NewMemoryStore() Store {
|
||||
return &memoryStore{
|
||||
data: make(map[string]*annotationV0.Annotation),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *memoryStore) Get(ctx context.Context, namespace, name string) (*annotationV0.Annotation, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
key := namespace + "/" + name
|
||||
anno, ok := m.data[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("annotation not found")
|
||||
}
|
||||
|
||||
return anno.DeepCopy(), nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) List(ctx context.Context, namespace string, opts ListOptions) (*AnnotationList, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
//nolint:prealloc
|
||||
var result []annotationV0.Annotation // no, we can't pre-alloc it, we don't know the size yet
|
||||
|
||||
for _, anno := range m.data {
|
||||
if anno.Namespace != namespace {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.DashboardUID != "" && (anno.Spec.DashboardUID == nil || *anno.Spec.DashboardUID != opts.DashboardUID) {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.PanelID != 0 && (anno.Spec.PanelID == nil || *anno.Spec.PanelID != opts.PanelID) {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.From > 0 && anno.Spec.Time < opts.From {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.To > 0 && anno.Spec.Time > opts.To {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, *anno.DeepCopy())
|
||||
|
||||
if opts.Limit > 0 && int64(len(result)) >= opts.Limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &AnnotationList{Items: result}, nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) Create(ctx context.Context, anno *annotationV0.Annotation) (*annotationV0.Annotation, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if anno.Name == "" {
|
||||
anno.Name = uuid.New().String()
|
||||
}
|
||||
|
||||
key := anno.Namespace + "/" + anno.Name
|
||||
|
||||
if _, exists := m.data[key]; exists {
|
||||
return nil, fmt.Errorf("annotation already exists")
|
||||
}
|
||||
|
||||
created := anno.DeepCopy()
|
||||
if created.CreationTimestamp.IsZero() {
|
||||
created.CreationTimestamp = metav1.Now()
|
||||
}
|
||||
|
||||
m.data[key] = created
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) Update(ctx context.Context, anno *annotationV0.Annotation) (*annotationV0.Annotation, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := anno.Namespace + "/" + anno.Name
|
||||
|
||||
if _, exists := m.data[key]; !exists {
|
||||
return nil, fmt.Errorf("annotation not found")
|
||||
}
|
||||
|
||||
updated := anno.DeepCopy()
|
||||
m.data[key] = updated
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) Delete(ctx context.Context, namespace, name string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := namespace + "/" + name
|
||||
|
||||
if _, exists := m.data[key]; !exists {
|
||||
return fmt.Errorf("annotation not found")
|
||||
}
|
||||
|
||||
delete(m.data, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryStore) ListTags(ctx context.Context, namespace string, opts TagListOptions) ([]Tag, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
tagCounts := make(map[string]int64)
|
||||
|
||||
for _, anno := range m.data {
|
||||
if anno.Namespace != namespace {
|
||||
continue
|
||||
}
|
||||
for _, tag := range anno.Spec.Tags {
|
||||
if opts.Prefix == "" || strings.HasPrefix(tag, opts.Prefix) {
|
||||
tagCounts[tag]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags := make([]Tag, 0, len(tagCounts))
|
||||
for name, count := range tagCounts {
|
||||
tags = append(tags, Tag{Name: name, Count: count})
|
||||
}
|
||||
|
||||
if opts.Limit > 0 && len(tags) > opts.Limit {
|
||||
tags = tags[:opts.Limit]
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
@@ -21,12 +22,11 @@ import (
|
||||
"github.com/grafana/grafana/apps/annotation/pkg/apis"
|
||||
annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1"
|
||||
annotationapp "github.com/grafana/grafana/apps/annotation/pkg/app"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
apiserverrest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
grafrequest "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@@ -46,15 +46,32 @@ func RegisterAppInstaller(
|
||||
cfg *setting.Cfg,
|
||||
features featuremgmt.FeatureToggles,
|
||||
service annotations.Repository,
|
||||
cleaner annotations.Cleaner,
|
||||
) (*AnnotationAppInstaller, error) {
|
||||
installer := &AnnotationAppInstaller{
|
||||
cfg: cfg,
|
||||
}
|
||||
|
||||
var tagHandler func(context.Context, app.CustomRouteResponseWriter, *app.CustomRouteRequest) error
|
||||
if service != nil {
|
||||
mapper := grafrequest.GetNamespaceMapper(cfg)
|
||||
sqlAdapter := NewSQLAdapter(service, cleaner, mapper, cfg)
|
||||
installer.legacy = &legacyStorage{
|
||||
store: sqlAdapter,
|
||||
mapper: mapper,
|
||||
}
|
||||
// Create the tags handler using the sqlAdapter as TagProvider
|
||||
tagHandler = newTagsHandler(sqlAdapter)
|
||||
}
|
||||
|
||||
provider := simple.NewAppProvider(apis.LocalManifest(), nil, annotationapp.New)
|
||||
|
||||
appConfig := app.Config{
|
||||
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
|
||||
KubeConfig: restclient.Config{},
|
||||
ManifestData: *apis.LocalManifest().ManifestData,
|
||||
SpecificConfig: &annotationapp.AnnotationConfig{
|
||||
TagHandler: tagHandler,
|
||||
},
|
||||
}
|
||||
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, apis.NewGoTypeAssociator())
|
||||
if err != nil {
|
||||
@@ -62,13 +79,6 @@ func RegisterAppInstaller(
|
||||
}
|
||||
installer.AppInstaller = i
|
||||
|
||||
if service != nil {
|
||||
installer.legacy = &legacyStorage{
|
||||
service: service,
|
||||
namespacer: request.GetNamespaceMapper(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
return installer, nil
|
||||
}
|
||||
|
||||
@@ -79,9 +89,11 @@ func (a *AnnotationAppInstaller) GetLegacyStorage(requested schema.GroupVersionR
|
||||
Version: kind.Version(),
|
||||
Resource: kind.Plural(),
|
||||
}
|
||||
|
||||
if requested.String() != gvr.String() {
|
||||
return nil
|
||||
}
|
||||
|
||||
a.legacy.tableConverter = utils.NewTableConverter(
|
||||
gvr.GroupResource(),
|
||||
utils.TableColumns{
|
||||
@@ -114,8 +126,8 @@ var (
|
||||
)
|
||||
|
||||
type legacyStorage struct {
|
||||
service annotations.Repository
|
||||
namespacer request.NamespaceMapper
|
||||
store Store
|
||||
mapper grafrequest.NamespaceMapper
|
||||
tableConverter rest.TableConvertor
|
||||
}
|
||||
|
||||
@@ -142,21 +154,15 @@ func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Objec
|
||||
}
|
||||
|
||||
func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
|
||||
orgID, err := request.OrgIDForList(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := &annotations.ItemQuery{OrgID: orgID, SignedInUser: user, AlertID: -1}
|
||||
namespace := request.NamespaceValue(ctx)
|
||||
|
||||
opts := ListOptions{}
|
||||
if options.FieldSelector != nil {
|
||||
for _, r := range options.FieldSelector.Requirements() {
|
||||
switch r.Field {
|
||||
case "spec.dashboardUID":
|
||||
if r.Operator == selection.Equals || r.Operator == selection.DoubleEquals {
|
||||
query.DashboardUID = r.Value
|
||||
opts.DashboardUID = r.Value
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported operator %s for spec.dashboardUID (only = supported)", r.Operator)
|
||||
}
|
||||
@@ -167,7 +173,7 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid panelID value %q: %w", r.Value, err)
|
||||
}
|
||||
query.PanelID = panelID
|
||||
opts.PanelID = panelID
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported operator %s for spec.panelID (only = supported)", r.Operator)
|
||||
}
|
||||
@@ -178,13 +184,13 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid time value %q: %w", r.Value, err)
|
||||
}
|
||||
query.From = from
|
||||
opts.From = from
|
||||
case selection.LessThan:
|
||||
to, err := strconv.ParseInt(r.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid time value %q: %w", r.Value, err)
|
||||
}
|
||||
query.To = to
|
||||
opts.To = to
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported operator %s for spec.time (only >, < supported for ranges)", r.Operator)
|
||||
}
|
||||
@@ -196,13 +202,13 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid timeEnd value %q: %w", r.Value, err)
|
||||
}
|
||||
query.From = from
|
||||
opts.From = from
|
||||
case selection.LessThan:
|
||||
to, err := strconv.ParseInt(r.Value, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid timeEnd value %q: %w", r.Value, err)
|
||||
}
|
||||
query.To = to
|
||||
opts.To = to
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported operator %s for spec.timeEnd (only >, < supported for ranges)", r.Operator)
|
||||
}
|
||||
@@ -213,31 +219,22 @@ func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListO
|
||||
}
|
||||
}
|
||||
|
||||
query.Limit = 100
|
||||
opts.Limit = 100
|
||||
if options.Limit > 0 {
|
||||
query.Limit = options.Limit
|
||||
opts.Limit = options.Limit
|
||||
}
|
||||
items, err := s.service.Find(ctx, query)
|
||||
|
||||
result, err := s.store.List(ctx, namespace, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := &annotationV0.AnnotationList{
|
||||
Items: make([]annotationV0.Annotation, len(items)),
|
||||
}
|
||||
for i, item := range items {
|
||||
c, err := toK8sResource(orgID, item, s.namespacer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list.Items[i] = *c
|
||||
}
|
||||
|
||||
// TODO: pagination?
|
||||
return list, nil
|
||||
return &annotationV0.AnnotationList{Items: result.Items}, nil
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
return nil, errors.New("fetching single annotations not supported by legacy storage")
|
||||
namespace := request.NamespaceValue(ctx)
|
||||
return s.store.Get(ctx, namespace, name)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Create(ctx context.Context,
|
||||
@@ -245,22 +242,11 @@ func (s *legacyStorage) Create(ctx context.Context,
|
||||
createValidation rest.ValidateObjectFunc,
|
||||
options *metav1.CreateOptions,
|
||||
) (runtime.Object, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
// resource, ok := obj.(*correlationsV0.Correlation)
|
||||
// if !ok {
|
||||
// return nil, fmt.Errorf("expected correlation")
|
||||
// }
|
||||
//
|
||||
// cmd, err := correlations.ToCreateCorrelationCommand(resource)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// out, err := s.service.CreateCorrelation(ctx, *cmd)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return s.Get(ctx, out.UID, &metav1.GetOptions{})
|
||||
resource, ok := obj.(*annotationV0.Annotation)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected annotation")
|
||||
}
|
||||
return s.store.Create(ctx, resource)
|
||||
}
|
||||
|
||||
func (s *legacyStorage) Update(ctx context.Context,
|
||||
@@ -272,73 +258,14 @@ func (s *legacyStorage) Update(ctx context.Context,
|
||||
options *metav1.UpdateOptions,
|
||||
) (runtime.Object, bool, error) {
|
||||
return nil, false, errors.New("not implemented")
|
||||
// before, err := s.Get(ctx, name, &metav1.GetOptions{})
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
// obj, err := objInfo.UpdatedObject(ctx, before)
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
//
|
||||
// resource, ok := obj.(*correlationsV0.Correlation)
|
||||
// if !ok {
|
||||
// return nil, false, fmt.Errorf("expected correlation")
|
||||
// }
|
||||
//
|
||||
// cmd, err := correlations.ToUpdateCorrelationCommand(resource)
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
//
|
||||
// out, err := s.service.UpdateCorrelation(ctx, *cmd)
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
// obj, err = s.Get(ctx, out.UID, &metav1.GetOptions{})
|
||||
// return obj, false, err
|
||||
}
|
||||
|
||||
// GracefulDeleter
|
||||
func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||
return nil, false, errors.New("not implemented")
|
||||
// orgID, err := request.OrgIDForList(ctx)
|
||||
// if err != nil {
|
||||
// return nil, false, err
|
||||
// }
|
||||
// err = s.service.DeleteCorrelation(ctx, correlations.DeleteCorrelationCommand{
|
||||
// OrgId: orgID,
|
||||
// UID: name,
|
||||
// })
|
||||
// return nil, (err == nil), err
|
||||
namespace := request.NamespaceValue(ctx)
|
||||
err := s.store.Delete(ctx, namespace, name)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// CollectionDeleter
|
||||
func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
|
||||
return nil, fmt.Errorf("DeleteCollection for annotation not implemented")
|
||||
}
|
||||
|
||||
func toK8sResource(orgID int64, item *annotations.ItemDTO, namespacer request.NamespaceMapper) (*annotationV0.Annotation, error) {
|
||||
annotation := &annotationV0.Annotation{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("a-%d", item.ID), // FIXME
|
||||
Namespace: namespacer(orgID),
|
||||
},
|
||||
Spec: annotationV0.AnnotationSpec{
|
||||
Text: item.Text,
|
||||
Time: item.Time,
|
||||
Tags: item.Tags,
|
||||
},
|
||||
}
|
||||
|
||||
if item.DashboardUID != nil && *item.DashboardUID != "" {
|
||||
annotation.Spec.DashboardUID = item.DashboardUID
|
||||
}
|
||||
if item.PanelID != 0 {
|
||||
annotation.Spec.PanelID = &item.PanelID
|
||||
}
|
||||
if item.TimeEnd != 0 {
|
||||
annotation.Spec.TimeEnd = &item.TimeEnd
|
||||
}
|
||||
return annotation, nil
|
||||
return nil, fmt.Errorf("DeleteCollection for annotation is not available")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
package annotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
claims "github.com/grafana/authlib/types"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type sqlAdapter struct {
|
||||
repo annotations.Repository
|
||||
cleaner annotations.Cleaner
|
||||
nsMapper request.NamespaceMapper
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func NewSQLAdapter(repo annotations.Repository, cleaner annotations.Cleaner, nsMapper request.NamespaceMapper, cfg *setting.Cfg) *sqlAdapter {
|
||||
return &sqlAdapter{
|
||||
repo: repo,
|
||||
cleaner: cleaner,
|
||||
nsMapper: nsMapper,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Get(ctx context.Context, namespace, name string) (*annotationV0.Annotation, error) {
|
||||
id, err := parseAnnotationID(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgID, err := namespaceToOrgID(ctx, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := &annotations.ItemQuery{
|
||||
SignedInUser: user,
|
||||
OrgID: orgID,
|
||||
Limit: 1000,
|
||||
AlertID: -1,
|
||||
}
|
||||
|
||||
items, err := a.repo.Find(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if item.ID == id {
|
||||
return a.toK8sResource(item, namespace), nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("annotation not found")
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) List(ctx context.Context, namespace string, opts ListOptions) (*AnnotationList, error) {
|
||||
orgID, err := namespaceToOrgID(ctx, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := &annotations.ItemQuery{
|
||||
SignedInUser: user,
|
||||
OrgID: orgID,
|
||||
DashboardUID: opts.DashboardUID,
|
||||
PanelID: opts.PanelID,
|
||||
From: opts.From,
|
||||
To: opts.To,
|
||||
Limit: opts.Limit,
|
||||
AlertID: -1,
|
||||
}
|
||||
|
||||
items, err := a.repo.Find(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]annotationV0.Annotation, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, *a.toK8sResource(item, namespace))
|
||||
}
|
||||
|
||||
return &AnnotationList{
|
||||
Items: result,
|
||||
Continue: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Create(ctx context.Context, anno *annotationV0.Annotation) (*annotationV0.Annotation, error) {
|
||||
orgID, err := namespaceToOrgID(ctx, anno.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item := a.fromK8sResource(anno)
|
||||
item.OrgID = orgID
|
||||
|
||||
if err := a.repo.Save(ctx, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
created := anno.DeepCopy()
|
||||
created.Name = fmt.Sprintf("a-%d", item.ID)
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Update(ctx context.Context, anno *annotationV0.Annotation) (*annotationV0.Annotation, error) {
|
||||
orgID, err := namespaceToOrgID(ctx, anno.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item := a.fromK8sResource(anno)
|
||||
item.OrgID = orgID
|
||||
|
||||
if err := a.repo.Update(ctx, item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return anno, nil
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Delete(ctx context.Context, namespace, name string) error {
|
||||
id, err := parseAnnotationID(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
orgID, err := namespaceToOrgID(ctx, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return a.repo.Delete(ctx, &annotations.DeleteParams{
|
||||
ID: id,
|
||||
OrgID: orgID,
|
||||
})
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) Cleanup(ctx context.Context) (int64, error) {
|
||||
if a.cleaner == nil {
|
||||
return 0, nil
|
||||
}
|
||||
deleted, _, err := a.cleaner.Run(ctx, a.cfg)
|
||||
return deleted, err
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) ListTags(ctx context.Context, namespace string, opts TagListOptions) ([]Tag, error) {
|
||||
orgID, err := namespaceToOrgID(ctx, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
query := &annotations.TagsQuery{
|
||||
OrgID: orgID,
|
||||
Limit: int64(opts.Limit),
|
||||
Tag: opts.Prefix,
|
||||
}
|
||||
|
||||
result, err := a.repo.FindTags(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tags := make([]Tag, len(result.Tags))
|
||||
for i, t := range result.Tags {
|
||||
tags[i] = Tag{Name: t.Tag, Count: t.Count}
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) toK8sResource(item *annotations.ItemDTO, namespace string) *annotationV0.Annotation {
|
||||
anno := &annotationV0.Annotation{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("a-%d", item.ID),
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: annotationV0.AnnotationSpec{
|
||||
Text: item.Text,
|
||||
Time: item.Time,
|
||||
Tags: item.Tags,
|
||||
},
|
||||
}
|
||||
|
||||
if item.DashboardUID != nil && *item.DashboardUID != "" {
|
||||
anno.Spec.DashboardUID = item.DashboardUID
|
||||
}
|
||||
if item.PanelID != 0 {
|
||||
anno.Spec.PanelID = &item.PanelID
|
||||
}
|
||||
if item.TimeEnd != 0 {
|
||||
anno.Spec.TimeEnd = &item.TimeEnd
|
||||
}
|
||||
|
||||
return anno
|
||||
}
|
||||
|
||||
func (a *sqlAdapter) fromK8sResource(anno *annotationV0.Annotation) *annotations.Item {
|
||||
item := &annotations.Item{
|
||||
Text: anno.Spec.Text,
|
||||
Epoch: anno.Spec.Time,
|
||||
Tags: anno.Spec.Tags,
|
||||
}
|
||||
|
||||
if anno.Name != "" {
|
||||
if id, err := parseAnnotationID(anno.Name); err == nil {
|
||||
item.ID = id
|
||||
}
|
||||
}
|
||||
|
||||
if anno.Spec.DashboardUID != nil {
|
||||
item.DashboardUID = *anno.Spec.DashboardUID
|
||||
}
|
||||
if anno.Spec.PanelID != nil {
|
||||
item.PanelID = *anno.Spec.PanelID
|
||||
}
|
||||
if anno.Spec.TimeEnd != nil {
|
||||
item.EpochEnd = *anno.Spec.TimeEnd
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
|
||||
func parseAnnotationID(name string) (int64, error) {
|
||||
if len(name) < 3 || name[:2] != "a-" {
|
||||
return 0, fmt.Errorf("invalid annotation name format: %s", name)
|
||||
}
|
||||
return strconv.ParseInt(name[2:], 10, 64)
|
||||
}
|
||||
|
||||
func namespaceToOrgID(ctx context.Context, namespace string) (int64, error) {
|
||||
info, err := claims.ParseNamespace(namespace)
|
||||
return info.OrgID, err
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package annotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
annotationV0 "github.com/grafana/grafana/apps/annotation/pkg/apis/annotation/v0alpha1"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
Get(ctx context.Context, namespace, name string) (*annotationV0.Annotation, error)
|
||||
List(ctx context.Context, namespace string, opts ListOptions) (*AnnotationList, error)
|
||||
Create(ctx context.Context, annotation *annotationV0.Annotation) (*annotationV0.Annotation, error)
|
||||
Update(ctx context.Context, annotation *annotationV0.Annotation) (*annotationV0.Annotation, error)
|
||||
Delete(ctx context.Context, namespace, name string) error
|
||||
}
|
||||
|
||||
type ListOptions struct {
|
||||
DashboardUID string
|
||||
PanelID int64
|
||||
From int64
|
||||
To int64
|
||||
Limit int64
|
||||
Continue string
|
||||
}
|
||||
|
||||
type AnnotationList struct {
|
||||
Items []annotationV0.Annotation
|
||||
Continue string
|
||||
}
|
||||
|
||||
type LifecycleManager interface {
|
||||
Cleanup(ctx context.Context) (int64, error)
|
||||
}
|
||||
|
||||
type TagProvider interface {
|
||||
ListTags(ctx context.Context, namespace string, opts TagListOptions) ([]Tag, error)
|
||||
}
|
||||
|
||||
type TagListOptions struct {
|
||||
Prefix string
|
||||
Limit int
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Name string
|
||||
Count int64
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package annotation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
)
|
||||
|
||||
type tagResponse struct {
|
||||
Tags []tagItem `json:"tags"`
|
||||
}
|
||||
|
||||
type tagItem struct {
|
||||
Tag string `json:"tag"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func newTagsHandler(tagProvider TagProvider) func(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error {
|
||||
return func(ctx context.Context, writer app.CustomRouteResponseWriter, request *app.CustomRouteRequest) error {
|
||||
fmt.Println("Handling /tags request")
|
||||
namespace := request.ResourceIdentifier.Namespace
|
||||
if namespace == "" {
|
||||
namespace = "default"
|
||||
}
|
||||
tags, err := tagProvider.ListTags(ctx, namespace, TagListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := make([]tagItem, len(tags))
|
||||
for i, tag := range tags {
|
||||
items[i] = tagItem{
|
||||
Tag: tag.Name,
|
||||
Count: tag.Count,
|
||||
}
|
||||
}
|
||||
|
||||
response := tagResponse{
|
||||
Tags: items,
|
||||
}
|
||||
|
||||
return json.NewEncoder(writer).Encode(response)
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
||||
datasourcesclient "github.com/grafana/grafana/pkg/services/datasources/service/client"
|
||||
"github.com/grafana/grafana/pkg/services/dsquerierclient"
|
||||
"github.com/grafana/grafana/pkg/services/encryption"
|
||||
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
|
||||
@@ -476,6 +477,7 @@ var wireBasicSet = wire.NewSet(
|
||||
appregistry.WireSet,
|
||||
// Dashboard Kubernetes helpers
|
||||
dashboardclient.ProvideK8sClientWithFallback,
|
||||
datasourcesclient.ProvideDataSourceConnectionClientFactory,
|
||||
)
|
||||
|
||||
var wireSet = wire.NewSet(
|
||||
|
||||
Generated
+8
-5
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
@@ -15,6 +16,8 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest
|
||||
defer span.End()
|
||||
|
||||
if err := authorize(ctx, r.GetNamespace(), s.cfg); err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -24,11 +27,15 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest
|
||||
|
||||
store, err := s.getStoreInfo(ctx, r.GetNamespace())
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contextuals, err := s.getContextuals(r.GetSubject())
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -37,6 +44,8 @@ func (s *Server) BatchCheck(ctx context.Context, r *authzextv1.BatchCheckRequest
|
||||
for _, item := range r.GetItems() {
|
||||
res, err := s.batchCheckItem(ctx, r, item, contextuals, store, groupResourceAccess)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
@@ -25,6 +26,8 @@ func (s *Server) Check(ctx context.Context, r *authzv1.CheckRequest) (*authzv1.C
|
||||
|
||||
res, err := s.check(ctx, r)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform check request", "error", err, "namespace", r.GetNamespace())
|
||||
return nil, errors.New("failed to perform check request")
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
authzv1 "github.com/grafana/authlib/authz/proto/v1"
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
)
|
||||
@@ -28,6 +29,8 @@ func (s *Server) List(ctx context.Context, r *authzv1.ListRequest) (*authzv1.Lis
|
||||
|
||||
res, err := s.list(ctx, r)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform list request", "error", err, "namespace", r.GetNamespace())
|
||||
return nil, errors.New("failed to perform list request")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
)
|
||||
|
||||
type OperationGroup string
|
||||
@@ -30,6 +31,8 @@ func (s *Server) Mutate(ctx context.Context, req *authzextv1.MutateRequest) (*au
|
||||
|
||||
res, err := s.mutate(ctx, req)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform mutate request", "error", err, "namespace", req.GetNamespace())
|
||||
return nil, errors.New("failed to perform mutate request")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
)
|
||||
|
||||
func (s *Server) Query(ctx context.Context, req *authzextv1.QueryRequest) (*authzextv1.QueryResponse, error) {
|
||||
@@ -20,6 +21,8 @@ func (s *Server) Query(ctx context.Context, req *authzextv1.QueryRequest) (*auth
|
||||
|
||||
res, err := s.query(ctx, req)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform query request", "error", err, "namespace", req.GetNamespace())
|
||||
return nil, errors.New("failed to perform query request")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
@@ -22,6 +23,8 @@ func (s *Server) Read(ctx context.Context, req *authzextv1.ReadRequest) (*authze
|
||||
|
||||
res, err := s.read(ctx, req)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform read request", "error", err, "namespace", req.GetNamespace())
|
||||
return nil, errors.New("failed to perform read request")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
openfgav1 "github.com/openfga/api/proto/openfga/v1"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
|
||||
authzextv1 "github.com/grafana/grafana/pkg/services/authz/proto/v1"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana/common"
|
||||
@@ -22,6 +23,8 @@ func (s *Server) Write(ctx context.Context, req *authzextv1.WriteRequest) (*auth
|
||||
|
||||
res, err := s.write(ctx, req)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
s.logger.Error("failed to perform write request", "error", err, "namespace", req.GetNamespace())
|
||||
return nil, errors.New("failed to perform write request")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
datasourcev0alpha1 "github.com/grafana/grafana/pkg/apis/datasource/v0alpha1"
|
||||
queryv0alpha1 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
// DataSourceConnectionClient can get information about data source connections.
|
||||
//
|
||||
//go:generate mockery --name DataSourceConnectionClient --structname MockDataSourceConnectionClient --inpackage --filename=client_mock.go --with-expecter
|
||||
type DataSourceConnectionClient interface {
|
||||
GetByUID(ctx context.Context, uid string) (*queryv0alpha1.DataSourceConnection, error)
|
||||
}
|
||||
|
||||
func ProvideDataSourceConnectionClientFactory(
|
||||
restConfigProvider apiserver.RestConfigProvider,
|
||||
) DataSourceConnectionClientFactory {
|
||||
return func(configProvider apiserver.RestConfigProvider) DataSourceConnectionClient {
|
||||
return &dataSourceConnectionClient{
|
||||
configProvider: configProvider,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type DataSourceConnectionClientFactory func(configProvider apiserver.RestConfigProvider) DataSourceConnectionClient
|
||||
|
||||
type dataSourceConnectionClient struct {
|
||||
configProvider apiserver.RestConfigProvider
|
||||
}
|
||||
|
||||
func (dc *dataSourceConnectionClient) Get(ctx context.Context, group, version, name string) (*queryv0alpha1.DataSourceConnection, error) {
|
||||
cfg, err := dc.configProvider.GetRestConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if version == "" {
|
||||
version = "v0alpha1"
|
||||
}
|
||||
|
||||
result := client.RESTClient().Get().
|
||||
Prefix("apis", group, version).
|
||||
Namespace("default"). // TODO do something about namespace
|
||||
Resource("datasources").
|
||||
Name(name).
|
||||
Do(ctx)
|
||||
|
||||
if err = result.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var statusCode int
|
||||
|
||||
result = result.StatusCode(&statusCode)
|
||||
if statusCode == http.StatusNotFound {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
|
||||
fullDS := datasourcev0alpha1.DataSource{}
|
||||
err = result.Into(&fullDS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dsConnection := &queryv0alpha1.DataSourceConnection{
|
||||
Title: fullDS.Spec.Title(),
|
||||
Datasource: queryv0alpha1.DataSourceConnectionRef{
|
||||
Group: fullDS.GroupVersionKind().Group,
|
||||
Name: fullDS.ObjectMeta.Name,
|
||||
Version: fullDS.GroupVersionKind().Version,
|
||||
},
|
||||
}
|
||||
|
||||
return dsConnection, nil
|
||||
}
|
||||
|
||||
func (dc *dataSourceConnectionClient) GetByUID(ctx context.Context, uid string) (*queryv0alpha1.DataSourceConnection, error) {
|
||||
cfg, err := dc.configProvider.GetRestConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// use the list endpoint with a fieldSelector so that can get multiple results
|
||||
// in the case of a non-unique "uid". This should not be possible when we are
|
||||
// backed by the legacy database, but wont be guaranteed when we are using
|
||||
// uniStore as the names will not be guaranteed unique across apiGroups. We
|
||||
// error below if more than one result is returned.
|
||||
result := client.RESTClient().Get().
|
||||
Prefix("apis", "query.grafana.app", "v0alpha1").
|
||||
Namespace("default"). // TODO do something about namespace
|
||||
Resource("connections").
|
||||
Param("fieldSelector", "metadata.name="+uid).
|
||||
Do(ctx)
|
||||
|
||||
if err = result.Error(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var statusCode int
|
||||
|
||||
result = result.StatusCode(&statusCode)
|
||||
if statusCode == http.StatusNotFound {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
|
||||
dsList := datasourcev0alpha1.DataSourceList{}
|
||||
err = result.Into(&dsList)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(dsList.Items) == 0 {
|
||||
return nil, errors.New("not found")
|
||||
}
|
||||
|
||||
if len(dsList.Items) > 1 {
|
||||
return nil, errors.New("multiple connections found")
|
||||
}
|
||||
|
||||
fullDS := dsList.Items[0]
|
||||
dsConnection := &queryv0alpha1.DataSourceConnection{
|
||||
Title: fullDS.Spec.Title(),
|
||||
Datasource: queryv0alpha1.DataSourceConnectionRef{
|
||||
Group: fullDS.GroupVersionKind().Group,
|
||||
Name: fullDS.ObjectMeta.Name,
|
||||
Version: fullDS.GroupVersionKind().Version,
|
||||
},
|
||||
}
|
||||
|
||||
return dsConnection, nil
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Code generated by mockery v2.53.3. DO NOT EDIT.
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
v0alpha1 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockDataSourceConnectionClient is an autogenerated mock type for the DataSourceConnectionClient type
|
||||
type MockDataSourceConnectionClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockDataSourceConnectionClient_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockDataSourceConnectionClient) EXPECT() *MockDataSourceConnectionClient_Expecter {
|
||||
return &MockDataSourceConnectionClient_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// GetByUID provides a mock function with given fields: ctx, uid
|
||||
func (_m *MockDataSourceConnectionClient) GetByUID(ctx context.Context, uid string) (*v0alpha1.DataSourceConnection, error) {
|
||||
ret := _m.Called(ctx, uid)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetByUID")
|
||||
}
|
||||
|
||||
var r0 *v0alpha1.DataSourceConnection
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) (*v0alpha1.DataSourceConnection, error)); ok {
|
||||
return rf(ctx, uid)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, string) *v0alpha1.DataSourceConnection); ok {
|
||||
r0 = rf(ctx, uid)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v0alpha1.DataSourceConnection)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
|
||||
r1 = rf(ctx, uid)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockDataSourceConnectionClient_GetByUID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetByUID'
|
||||
type MockDataSourceConnectionClient_GetByUID_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetByUID is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - uid string
|
||||
func (_e *MockDataSourceConnectionClient_Expecter) GetByUID(ctx interface{}, uid interface{}) *MockDataSourceConnectionClient_GetByUID_Call {
|
||||
return &MockDataSourceConnectionClient_GetByUID_Call{Call: _e.mock.On("GetByUID", ctx, uid)}
|
||||
}
|
||||
|
||||
func (_c *MockDataSourceConnectionClient_GetByUID_Call) Run(run func(ctx context.Context, uid string)) *MockDataSourceConnectionClient_GetByUID_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockDataSourceConnectionClient_GetByUID_Call) Return(_a0 *v0alpha1.DataSourceConnection, _a1 error) *MockDataSourceConnectionClient_GetByUID_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockDataSourceConnectionClient_GetByUID_Call) RunAndReturn(run func(context.Context, string) (*v0alpha1.DataSourceConnection, error)) *MockDataSourceConnectionClient_GetByUID_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockDataSourceConnectionClient creates a new instance of MockDataSourceConnectionClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockDataSourceConnectionClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockDataSourceConnectionClient {
|
||||
mock := &MockDataSourceConnectionClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -574,6 +574,15 @@ func (s *ServiceImpl) buildDataConnectionsNavLink(c *contextmodel.ReqContext) *n
|
||||
Url: baseUrl + "/datasources",
|
||||
Children: []*navtree.NavLink{},
|
||||
})
|
||||
|
||||
// Stacks
|
||||
children = append(children, &navtree.NavLink{
|
||||
Id: "connections-stacks",
|
||||
Text: "Stacks",
|
||||
SubTitle: "Manage data source stacks for different environments",
|
||||
Url: baseUrl + "/stacks",
|
||||
Children: []*navtree.NavLink{},
|
||||
})
|
||||
}
|
||||
|
||||
if len(children) > 0 {
|
||||
|
||||
@@ -107,6 +107,9 @@ func StartGrafanaEnvWithDB(t *testing.T, grafDir, cfgPath string) (string, *serv
|
||||
dbCfg.Key("user").SetValue(testDB.User)
|
||||
dbCfg.Key("password").SetValue(testDB.Password)
|
||||
dbCfg.Key("name").SetValue(testDB.Database)
|
||||
if testDB.Path != "" {
|
||||
dbCfg.Key("path").SetValue(testDB.Path)
|
||||
}
|
||||
|
||||
t.Log("Using test database", "type", testDB.DriverName, "host", testDB.Host, "port", testDB.Port, "user", testDB.User, "name", testDB.Database, "path", testDB.Path)
|
||||
|
||||
|
||||
@@ -103,10 +103,17 @@ const combineFolderResponses = (
|
||||
|
||||
export async function getFolderByUidFacade(uid: string): Promise<FolderDTO> {
|
||||
const isVirtualFolder = uid && [GENERAL_FOLDER_UID, config.sharedWithMeFolderUID].includes(uid);
|
||||
// We need the legacy API call regardless, for now
|
||||
const legacyApiCall = dispatch(browseDashboardsAPI.endpoints.getFolder.initiate(uid));
|
||||
|
||||
const shouldUseAppPlatformAPI = Boolean(config.featureToggles.foldersAppPlatformAPI);
|
||||
|
||||
// We need the legacy API call regardless, for now
|
||||
const legacyApiCall = dispatch(
|
||||
browseDashboardsAPI.endpoints.getFolder.initiate({
|
||||
folderUID: uid,
|
||||
accesscontrol: true,
|
||||
isLegacyCall: shouldUseAppPlatformAPI,
|
||||
})
|
||||
);
|
||||
|
||||
if (shouldUseAppPlatformAPI) {
|
||||
let virtualFolderResponse;
|
||||
if (isVirtualFolder) {
|
||||
@@ -165,7 +172,9 @@ export function useGetFolderQueryFacade(uid?: string) {
|
||||
// This may look weird that we call the legacy folder anyway all the time, but the issue is we don't have good API
|
||||
// for the access control metadata yet, and so we still take it from the old api.
|
||||
// see https://github.com/grafana/identity-access-team/issues/1103
|
||||
const legacyFolderResult = useGetFolderQueryLegacy(uid || skipToken);
|
||||
const legacyFolderResult = useGetFolderQueryLegacy(
|
||||
uid ? { folderUID: uid, accesscontrol: true, isLegacyCall: true } : skipToken
|
||||
);
|
||||
let resultFolder = useGetFolderQuery(shouldUseAppPlatformAPI && !isVirtualFolder ? params : skipToken);
|
||||
// We get parents and folders for virtual folders too. Parents should just return empty array but it's easier to
|
||||
// stitch the responses this way and access can actually return different response based on the grafana setup.
|
||||
|
||||
@@ -182,6 +182,8 @@ export function getNavTitle(navId: string | undefined) {
|
||||
return t('nav.connections.title', 'Connections');
|
||||
case 'connections-add-new-connection':
|
||||
return t('nav.add-new-connections.title', 'Add new connection');
|
||||
case 'connections-stacks':
|
||||
return t('nav.stacks.title', 'Stacks');
|
||||
case 'standalone-plugin-page-/connections/collector':
|
||||
return t('nav.collector.title', 'Collector');
|
||||
case 'connections-datasources':
|
||||
|
||||
@@ -94,9 +94,17 @@ export const browseDashboardsAPI = createApi({
|
||||
}),
|
||||
|
||||
// get folder info (e.g. title, parents) but *not* children
|
||||
getFolder: builder.query<FolderDTO, string>({
|
||||
providesTags: (_result, _error, folderUID) => [{ type: 'getFolder', id: folderUID }],
|
||||
query: (folderUID) => ({ url: `/folders/${folderUID}`, params: { accesscontrol: true } }),
|
||||
getFolder: builder.query<FolderDTO, { folderUID: string; accesscontrol: boolean; isLegacyCall: boolean }>({
|
||||
providesTags: (_result, _error, { folderUID }) => [{ type: 'getFolder', id: folderUID }],
|
||||
query: ({ folderUID, accesscontrol, isLegacyCall }) => ({
|
||||
url: `/folders/${folderUID}`,
|
||||
params: {
|
||||
accesscontrol,
|
||||
// Add additional query param so we can tell when
|
||||
// this was called for app platform compatibility purposes vs. actually needing to use the legacy API
|
||||
isLegacyCall,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
|
||||
// create a new folder
|
||||
|
||||
@@ -10,10 +10,13 @@ import { CacheFeatureHighlightPage } from './pages/CacheFeatureHighlightPage';
|
||||
import ConnectionsHomePage from './pages/ConnectionsHomePage';
|
||||
import { DataSourceDashboardsPage } from './pages/DataSourceDashboardsPage';
|
||||
import { DataSourceDetailsPage } from './pages/DataSourceDetailsPage';
|
||||
import { DataSourceStacksPage } from './pages/DataSourceStacksPage';
|
||||
import { DataSourcesListPage } from './pages/DataSourcesListPage';
|
||||
import { EditDataSourcePage } from './pages/EditDataSourcePage';
|
||||
import { EditStackPage } from './pages/EditStackPage';
|
||||
import { InsightsFeatureHighlightPage } from './pages/InsightsFeatureHighlightPage';
|
||||
import { NewDataSourcePage } from './pages/NewDataSourcePage';
|
||||
import { NewStackPage } from './pages/NewStackPage';
|
||||
import { PermissionsFeatureHighlightPage } from './pages/PermissionsFeatureHighlightPage';
|
||||
|
||||
function RedirectToAddNewConnection() {
|
||||
@@ -41,6 +44,9 @@ export default function Connections() {
|
||||
{/* The route paths need to be relative to the parent path (ROUTES.Base), so we need to remove that part */}
|
||||
<Route caseSensitive path={ROUTES.DataSources.replace(ROUTES.Base, '')} element={<DataSourcesListPage />} />
|
||||
<Route caseSensitive path={ROUTES.DataSourcesNew.replace(ROUTES.Base, '')} element={<NewDataSourcePage />} />
|
||||
<Route caseSensitive path={ROUTES.Stacks.replace(ROUTES.Base, '')} element={<DataSourceStacksPage />} />
|
||||
<Route caseSensitive path={ROUTES.StacksNew.replace(ROUTES.Base, '')} element={<NewStackPage />} />
|
||||
<Route caseSensitive path={ROUTES.StacksEdit.replace(ROUTES.Base, '')} element={<EditStackPage />} />
|
||||
<Route
|
||||
caseSensitive
|
||||
path={ROUTES.DataSourcesDetails.replace(ROUTES.Base, '')}
|
||||
|
||||
@@ -75,5 +75,11 @@ export function getOssCardData(): CardData[] {
|
||||
url: '/connections/datasources',
|
||||
icon: 'database',
|
||||
},
|
||||
{
|
||||
text: 'Stacks',
|
||||
subTitle: 'Manage your data source stacks',
|
||||
url: '/connections/stacks',
|
||||
icon: 'layers',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ export const ROUTES = {
|
||||
DataSourcesNew: `/${ROUTE_BASE_ID}/datasources/new`,
|
||||
DataSourcesEdit: `/${ROUTE_BASE_ID}/datasources/edit/:uid`,
|
||||
DataSourcesDashboards: `/${ROUTE_BASE_ID}/datasources/edit/:uid/dashboards`,
|
||||
// Stacks
|
||||
Stacks: `/${ROUTE_BASE_ID}/stacks`,
|
||||
StacksNew: `/${ROUTE_BASE_ID}/stacks/new`,
|
||||
StacksEdit: `/${ROUTE_BASE_ID}/stacks/edit/:uid`,
|
||||
|
||||
// Add new connection
|
||||
AddNewConnection: `/${ROUTE_BASE_ID}/add-new-connection`,
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import {
|
||||
Card,
|
||||
EmptyState,
|
||||
FilterInput,
|
||||
IconButton,
|
||||
LinkButton,
|
||||
Spinner,
|
||||
Stack,
|
||||
TagList,
|
||||
useStyles2,
|
||||
} from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { ScopedResourceClient } from 'app/features/apiserver/client';
|
||||
import { Resource, ResourceList, GroupVersionResource } from 'app/features/apiserver/types';
|
||||
|
||||
// Define the DataSourceStack spec type based on the backend Go types
|
||||
export interface DataSourceStackTemplateItem {
|
||||
group: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DataSourceStackModeItem {
|
||||
dataSourceRef: string;
|
||||
}
|
||||
|
||||
export interface DataSourceStackModeSpec {
|
||||
name: string;
|
||||
uid: string;
|
||||
definition: Record<string, DataSourceStackModeItem>;
|
||||
}
|
||||
|
||||
export interface DataSourceStackSpec {
|
||||
template: Record<string, DataSourceStackTemplateItem>;
|
||||
modes: DataSourceStackModeSpec[];
|
||||
}
|
||||
|
||||
// GroupVersionResource for datasourcestacks
|
||||
const datasourceStacksGVR: GroupVersionResource = {
|
||||
group: 'collections.grafana.app',
|
||||
version: 'v1alpha1',
|
||||
resource: 'datasourcestacks',
|
||||
};
|
||||
|
||||
const datasourceStacksClient = new ScopedResourceClient<DataSourceStackSpec>(datasourceStacksGVR);
|
||||
|
||||
export function DataSourceStacksPage() {
|
||||
const [stacks, setStacks] = useState<Array<Resource<DataSourceStackSpec>>>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const fetchStacks = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response: ResourceList<DataSourceStackSpec> = await datasourceStacksClient.list();
|
||||
setStacks(response.items);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch datasource stacks:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch datasource stacks');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchStacks();
|
||||
}, [fetchStacks]);
|
||||
|
||||
const onDeleteStack = (stackName: string) => async () => {
|
||||
await datasourceStacksClient.delete(stackName, false);
|
||||
fetchStacks();
|
||||
};
|
||||
|
||||
// Filter stacks based on search query
|
||||
const filteredStacks = useMemo(() => {
|
||||
if (!searchQuery) {
|
||||
return stacks;
|
||||
}
|
||||
const query = searchQuery.toLowerCase();
|
||||
return stacks.filter((stack) => {
|
||||
const nameMatch = stack.metadata.name?.toLowerCase().includes(query);
|
||||
const templateMatch = Object.values(stack.spec.template).some(
|
||||
(template) => template.name.toLowerCase().includes(query) || template.group.toLowerCase().includes(query)
|
||||
);
|
||||
return nameMatch || templateMatch;
|
||||
});
|
||||
}, [stacks, searchQuery]);
|
||||
|
||||
const actions =
|
||||
stacks.length > 0 ? (
|
||||
<LinkButton variant="primary" icon="plus" href="/connections/stacks/new">
|
||||
<Trans i18nKey="connections.stacks-list-view.add-stack">Add stack</Trans>
|
||||
</LinkButton>
|
||||
) : undefined;
|
||||
|
||||
const pageNav = {
|
||||
text: t('connections.stacks-list-view.title', 'Data source stacks'),
|
||||
subTitle: t(
|
||||
'connections.stacks-list-view.subtitle',
|
||||
'Manage your data source stacks to group environments like dev, staging, and production'
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<Page navId="connections-stacks" pageNav={pageNav} actions={actions}>
|
||||
<Page.Contents>
|
||||
<DataSourceStacksListContent
|
||||
stacks={filteredStacks}
|
||||
loading={loading}
|
||||
error={error}
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
onDeleteStack={onDeleteStack}
|
||||
styles={styles}
|
||||
/>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
interface DataSourceStacksListContentProps {
|
||||
stacks: Array<Resource<DataSourceStackSpec>>;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
searchQuery: string;
|
||||
setSearchQuery: (query: string) => void;
|
||||
styles: ReturnType<typeof getStyles>;
|
||||
onDeleteStack: (stackName: string) => () => Promise<void>;
|
||||
}
|
||||
|
||||
function DataSourceStacksListContent({
|
||||
stacks,
|
||||
loading,
|
||||
error,
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
styles,
|
||||
onDeleteStack,
|
||||
}: DataSourceStacksListContentProps) {
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyState
|
||||
variant="not-found"
|
||||
message={t('connections.stacks-list-view.error', 'Failed to load data source stacks')}
|
||||
>
|
||||
<div>{error}</div>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
if (stacks.length === 0 && !searchQuery) {
|
||||
return (
|
||||
<EmptyState
|
||||
message={t(
|
||||
'connections.stacks-list-view.empty.no-rules-created',
|
||||
"You haven't created any data source stacks yet"
|
||||
)}
|
||||
variant="call-to-action"
|
||||
>
|
||||
<div>
|
||||
<Trans i18nKey="connections.stacks-list-view.empty.description">
|
||||
Use data source stacks to group environments like dev, stg, and prod. Reference the stack in your query, and
|
||||
Grafana automatically selects the right data source for that environment.
|
||||
</Trans>
|
||||
</div>
|
||||
|
||||
<LinkButton variant="primary" icon="plus" size="lg" href="/connections/stacks/new">
|
||||
<Trans i18nKey="connections.stacks-list-view.empty.new-stack">New stack</Trans>
|
||||
</LinkButton>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={2}>
|
||||
<div className={styles.searchContainer}>
|
||||
<FilterInput
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder={t('connections.stacks-list-view.search-placeholder', 'Search by name or type')}
|
||||
/>
|
||||
</div>
|
||||
{stacks.length === 0 && searchQuery ? (
|
||||
<EmptyState
|
||||
variant="not-found"
|
||||
message={t('connections.stacks-list-view.no-results', 'No data source stacks found')}
|
||||
/>
|
||||
) : (
|
||||
<ul className={styles.list}>
|
||||
{stacks.map((stack) => (
|
||||
<li key={stack.metadata.name}>
|
||||
<Card noMargin href={`/connections/stacks/edit/${stack.metadata.name}`}>
|
||||
<Card.Heading>{stack.metadata.name}</Card.Heading>
|
||||
<Card.Tags>
|
||||
<Stack direction="row" gap={2} alignItems="center">
|
||||
<TagList tags={getDatasourceList(stack.spec)} />
|
||||
<IconButton
|
||||
name="trash-alt"
|
||||
variant="destructive"
|
||||
aria-label={t('connections.stacks-list-view.delete-stack', 'Delete stack')}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onDeleteStack(stack.metadata.name)();
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</Card.Tags>
|
||||
</Card>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
searchContainer: css({
|
||||
marginBottom: theme.spacing(2),
|
||||
maxWidth: '500px',
|
||||
}),
|
||||
list: css({
|
||||
listStyle: 'none',
|
||||
display: 'grid',
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
});
|
||||
|
||||
const getDatasourceList = (stack: DataSourceStackSpec): string[] => {
|
||||
return Array.from(
|
||||
// remove duplicates
|
||||
new Set(
|
||||
Object.values(stack.template).map((template) => {
|
||||
const match = template.group.match(/^grafana-(.+)-datasource$/);
|
||||
if (match && match[1]) {
|
||||
return match[1].charAt(0).toUpperCase() + match[1].slice(1);
|
||||
}
|
||||
return template.name.charAt(0).toUpperCase() + template.name.slice(1);
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { EmptyState, Spinner } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { ScopedResourceClient } from 'app/features/apiserver/client';
|
||||
import { Resource, GroupVersionResource } from 'app/features/apiserver/types';
|
||||
import {
|
||||
StackForm,
|
||||
transformStackSpecToFormValues,
|
||||
} from 'app/features/datasources/components/new-stack-form/StackForm';
|
||||
import { StackFormValues } from 'app/features/datasources/components/new-stack-form/types';
|
||||
|
||||
import { DataSourceStackSpec } from './DataSourceStacksPage';
|
||||
|
||||
const datasourceStacksGVR: GroupVersionResource = {
|
||||
group: 'collections.grafana.app',
|
||||
version: 'v1alpha1',
|
||||
resource: 'datasourcestacks',
|
||||
};
|
||||
|
||||
const datasourceStacksClient = new ScopedResourceClient<DataSourceStackSpec>(datasourceStacksGVR);
|
||||
|
||||
export function EditStackPage() {
|
||||
const { uid } = useParams<{ uid: string }>();
|
||||
const [stack, setStack] = useState<Resource<DataSourceStackSpec> | null>(null);
|
||||
const [formValues, setFormValues] = useState<StackFormValues | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStack = async () => {
|
||||
if (!uid) {
|
||||
setError('No stack UID provided');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await datasourceStacksClient.get(uid);
|
||||
setStack(response);
|
||||
|
||||
const values = transformStackSpecToFormValues(response.metadata.name || '', response.spec);
|
||||
setFormValues(values);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch datasource stack:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch datasource stack');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStack();
|
||||
}, [uid]);
|
||||
|
||||
const pageNav = {
|
||||
text: stack?.metadata.name
|
||||
? t('connections.edit-stack-page.title-with-name', 'Edit {{name}}', { name: stack.metadata.name })
|
||||
: t('connections.edit-stack-page.title', 'Edit Data Source Stack'),
|
||||
subTitle: t('connections.edit-stack-page.subtitle', 'Modify your data source stack configuration'),
|
||||
};
|
||||
|
||||
return (
|
||||
<Page navId="connections-stacks" pageNav={pageNav}>
|
||||
<Page.Contents>
|
||||
<EditStackContent loading={loading} error={error} formValues={formValues} />
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
interface EditStackContentProps {
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
formValues: StackFormValues | null;
|
||||
}
|
||||
|
||||
function EditStackContent({ loading, error, formValues }: EditStackContentProps) {
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyState
|
||||
variant="not-found"
|
||||
message={t('connections.edit-stack-page.error', 'Failed to load data source stack')}
|
||||
>
|
||||
<div>{error}</div>
|
||||
</EmptyState>
|
||||
);
|
||||
}
|
||||
|
||||
if (!formValues) {
|
||||
return (
|
||||
<EmptyState
|
||||
variant="not-found"
|
||||
message={t('connections.edit-stack-page.not-found', 'Data source stack not found')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <StackForm existing={formValues} />;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { StackForm } from 'app/features/datasources/components/new-stack-form/StackForm';
|
||||
|
||||
export function NewStackPage() {
|
||||
return (
|
||||
<Page
|
||||
navId="connections-stacks"
|
||||
pageNav={{
|
||||
text: 'New Data Source Stack',
|
||||
subTitle: 'Add a new data source stack',
|
||||
active: true,
|
||||
}}
|
||||
>
|
||||
<Page.Contents>
|
||||
<StackForm />
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -69,6 +69,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
||||
hasOpenPane: Boolean(openPane),
|
||||
contentMargin: 1,
|
||||
position: 'right',
|
||||
persistanceKey: 'dashboard',
|
||||
onClosePane: () => editPane.closePane(),
|
||||
});
|
||||
|
||||
|
||||
@@ -849,6 +849,7 @@ describe('DashboardSceneSerializer', () => {
|
||||
query: 'app1',
|
||||
skipUrlSync: false,
|
||||
allowCustomValue: true,
|
||||
valuesFormat: 'csv',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
+5
@@ -294,6 +294,7 @@ exports[`Given a scene with custom quick ranges should save quick ranges to save
|
||||
"options": [],
|
||||
"query": "a, b, c",
|
||||
"type": "custom",
|
||||
"valuesFormat": "csv",
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
@@ -679,6 +680,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
|
||||
"options": [],
|
||||
"query": "A,B,C,D,E,F,E,G,H,I,J,K,L",
|
||||
"type": "custom",
|
||||
"valuesFormat": "csv",
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
@@ -697,6 +699,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
|
||||
"options": [],
|
||||
"query": "Bob : 1, Rob : 2,Sod : 3, Hod : 4, Cod : 5",
|
||||
"type": "custom",
|
||||
"valuesFormat": "csv",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -1019,6 +1022,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
|
||||
"options": [],
|
||||
"query": "a, b, c",
|
||||
"type": "custom",
|
||||
"valuesFormat": "csv",
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
@@ -1378,6 +1382,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
|
||||
"options": [],
|
||||
"query": "a, b, c",
|
||||
"type": "custom",
|
||||
"valuesFormat": "csv",
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
|
||||
+1
@@ -197,6 +197,7 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model
|
||||
"options": [],
|
||||
"query": "option1, option2",
|
||||
"skipUrlSync": false,
|
||||
"valuesFormat": "csv",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -374,6 +374,7 @@ describe('sceneVariablesSetToVariables', () => {
|
||||
"options": [],
|
||||
"query": "test,test1,test2",
|
||||
"type": "custom",
|
||||
"valuesFormat": "csv",
|
||||
}
|
||||
`);
|
||||
});
|
||||
@@ -1148,6 +1149,7 @@ describe('sceneVariablesSetToVariables', () => {
|
||||
"options": [],
|
||||
"query": "test,test1,test2",
|
||||
"skipUrlSync": false,
|
||||
"valuesFormat": "csv",
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -110,6 +110,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
|
||||
allValue: variable.state.allValue,
|
||||
includeAll: variable.state.includeAll,
|
||||
allowCustomValue: variable.state.allowCustomValue,
|
||||
valuesFormat: variable.state.valuesFormat,
|
||||
});
|
||||
} else if (sceneUtils.isDataSourceVariable(variable)) {
|
||||
variables.push({
|
||||
@@ -392,6 +393,7 @@ export function sceneVariablesSetToSchemaV2Variables(
|
||||
allValue: variable.state.allValue,
|
||||
includeAll: variable.state.includeAll ?? false,
|
||||
allowCustomValue: variable.state.allowCustomValue ?? true,
|
||||
valuesFormat: variable.state.valuesFormat,
|
||||
},
|
||||
};
|
||||
variables.push(customVariable);
|
||||
|
||||
+2
-1
@@ -335,12 +335,12 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (variable.kind === defaultCustomVariableKind().kind) {
|
||||
return new CustomVariable({
|
||||
...commonProperties,
|
||||
value: variable.spec.current?.value ?? '',
|
||||
text: variable.spec.current?.text ?? '',
|
||||
|
||||
query: variable.spec.query,
|
||||
isMulti: variable.spec.multi,
|
||||
allValue: variable.spec.allValue || undefined,
|
||||
@@ -348,6 +348,7 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
|
||||
defaultToAll: Boolean(variable.spec.includeAll),
|
||||
skipUrlSync: variable.spec.skipUrlSync,
|
||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||
valuesFormat: variable.spec.valuesFormat || 'csv',
|
||||
});
|
||||
} else if (variable.kind === defaultQueryVariableKind().kind) {
|
||||
return new QueryVariable({
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { SceneVariable } from '@grafana/scenes';
|
||||
import { VariableHide, defaultVariableModel } from '@grafana/schema';
|
||||
import { Button, LoadingPlaceholder, ConfirmModal, ModalsController, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { Button, ConfirmModal, LoadingPlaceholder, ModalsController, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { VariableHideSelect } from 'app/features/dashboard-scene/settings/variables/components/VariableHideSelect';
|
||||
import { VariableLegend } from 'app/features/dashboard-scene/settings/variables/components/VariableLegend';
|
||||
import { VariableTextAreaField } from 'app/features/dashboard-scene/settings/variables/components/VariableTextAreaField';
|
||||
@@ -68,6 +68,9 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
|
||||
const onHideChange = (hide: VariableHide) => variable.setState({ hide });
|
||||
|
||||
const isHasVariableOptions = hasVariableOptions(variable);
|
||||
const optionsForSelect = isHasVariableOptions ? variable.getOptionsForSelect(false) : [];
|
||||
const hasJsonValuesFormat = 'valuesFormat' in variable.state && variable.state.valuesFormat === 'json';
|
||||
const hasMultiProps = hasJsonValuesFormat || optionsForSelect.every((o) => Boolean(o.properties));
|
||||
|
||||
const onDeleteVariable = (hideModal: () => void) => () => {
|
||||
reportInteraction('Delete variable');
|
||||
@@ -123,7 +126,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
|
||||
|
||||
{EditorToRender && <EditorToRender variable={variable} onRunQuery={onRunQuery} />}
|
||||
|
||||
{isHasVariableOptions && <VariableValuesPreview options={variable.getOptionsForSelect(false)} />}
|
||||
{isHasVariableOptions && <VariableValuesPreview options={optionsForSelect} hasMultiProps={hasMultiProps} />}
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<Stack gap={2}>
|
||||
|
||||
+69
-1
@@ -1,4 +1,5 @@
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
@@ -130,4 +131,71 @@ describe('CustomVariableForm', () => {
|
||||
expect(onMultiChange).not.toHaveBeenCalled();
|
||||
expect(onIncludeAllChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('JSON values format', () => {
|
||||
test('should render the form fields correctly', async () => {
|
||||
const { getByTestId, queryByTestId } = render(
|
||||
<CustomVariableForm
|
||||
query="query"
|
||||
valuesFormat="json"
|
||||
multi={true}
|
||||
allowCustomValue={true}
|
||||
includeAll={true}
|
||||
allValue="custom value"
|
||||
onQueryChange={onQueryChange}
|
||||
onMultiChange={onMultiChange}
|
||||
onIncludeAllChange={onIncludeAllChange}
|
||||
onAllValueChange={onAllValueChange}
|
||||
onAllowCustomValueChange={onAllowCustomValueChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Object values in a JSON array'));
|
||||
|
||||
const multiCheckbox = getByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
|
||||
);
|
||||
const allowCustomValueCheckbox = queryByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
|
||||
);
|
||||
const includeAllCheckbox = getByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
|
||||
);
|
||||
const allValueInput = queryByTestId(
|
||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
|
||||
);
|
||||
|
||||
expect(multiCheckbox).toBeInTheDocument();
|
||||
expect(multiCheckbox).toBeChecked();
|
||||
expect(includeAllCheckbox).toBeInTheDocument();
|
||||
expect(includeAllCheckbox).toBeChecked();
|
||||
|
||||
expect(allowCustomValueCheckbox).not.toBeInTheDocument();
|
||||
expect(allValueInput).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should display validation error', async () => {
|
||||
const validationError = new Error('Ooops! Validation error.');
|
||||
|
||||
const { findByText } = render(
|
||||
<CustomVariableForm
|
||||
query="query"
|
||||
valuesFormat="json"
|
||||
queryValidationError={validationError}
|
||||
multi={false}
|
||||
includeAll={false}
|
||||
onQueryChange={onQueryChange}
|
||||
onMultiChange={onMultiChange}
|
||||
onIncludeAllChange={onIncludeAllChange}
|
||||
onAllValueChange={onAllValueChange}
|
||||
onAllowCustomValueChange={onAllowCustomValueChange}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByText('Object values in a JSON array'));
|
||||
|
||||
const errorEl = await findByText(validationError.message);
|
||||
expect(errorEl).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+69
-3
@@ -1,7 +1,9 @@
|
||||
import { FormEvent } from 'react';
|
||||
|
||||
import { CustomVariableModel } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { FieldValidationMessage, Icon, RadioButtonGroup, Stack, TextLink, Tooltip } from '@grafana/ui';
|
||||
|
||||
import { SelectionOptionsForm } from './SelectionOptionsForm';
|
||||
import { VariableLegend } from './VariableLegend';
|
||||
@@ -9,10 +11,12 @@ import { VariableTextAreaField } from './VariableTextAreaField';
|
||||
|
||||
interface CustomVariableFormProps {
|
||||
query: string;
|
||||
valuesFormat?: CustomVariableModel['valuesFormat'];
|
||||
multi: boolean;
|
||||
allValue?: string | null;
|
||||
includeAll: boolean;
|
||||
allowCustomValue?: boolean;
|
||||
queryValidationError?: Error;
|
||||
onQueryChange: (event: FormEvent<HTMLTextAreaElement>) => void;
|
||||
onMultiChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||
@@ -20,19 +24,23 @@ interface CustomVariableFormProps {
|
||||
onQueryBlur?: (event: FormEvent<HTMLTextAreaElement>) => void;
|
||||
onAllValueBlur?: (event: FormEvent<HTMLInputElement>) => void;
|
||||
onAllowCustomValueChange?: (event: FormEvent<HTMLInputElement>) => void;
|
||||
onValuesFormatChange?: (format: CustomVariableModel['valuesFormat']) => void;
|
||||
}
|
||||
|
||||
export function CustomVariableForm({
|
||||
query,
|
||||
valuesFormat,
|
||||
multi,
|
||||
allValue,
|
||||
includeAll,
|
||||
allowCustomValue,
|
||||
queryValidationError,
|
||||
onQueryChange,
|
||||
onMultiChange,
|
||||
onIncludeAllChange,
|
||||
onAllValueChange,
|
||||
onAllowCustomValueChange,
|
||||
onValuesFormatChange,
|
||||
}: CustomVariableFormProps) {
|
||||
return (
|
||||
<>
|
||||
@@ -40,16 +48,27 @@ export function CustomVariableForm({
|
||||
<Trans i18nKey="dashboard-scene.custom-variable-form.custom-options">Custom options</Trans>
|
||||
</VariableLegend>
|
||||
|
||||
<ValuesFormatSelector valuesFormat={valuesFormat} onValuesFormatChange={onValuesFormatChange} />
|
||||
|
||||
<VariableTextAreaField
|
||||
name={t('dashboard-scene.custom-variable-form.name-values-separated-comma', 'Values separated by comma')}
|
||||
// we don't use a controlled component so we make sure the textarea content is cleared when changing format by providing a key
|
||||
key={valuesFormat}
|
||||
name=""
|
||||
placeholder={
|
||||
valuesFormat === 'json'
|
||||
? // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
'[{ "text":"text1", "value":"val1", "propA":"a1", "propB":"b1" },\n{ "text":"text2", "value":"val2", "propA":"a2", "propB":"b2" }]'
|
||||
: // eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
'1, 10, mykey : myvalue, myvalue, escaped\,value'
|
||||
}
|
||||
defaultValue={query}
|
||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
placeholder="1, 10, mykey : myvalue, myvalue, escaped\,value"
|
||||
onBlur={onQueryChange}
|
||||
required
|
||||
width={52}
|
||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput}
|
||||
/>
|
||||
{queryValidationError && <FieldValidationMessage>{queryValidationError.message}</FieldValidationMessage>}
|
||||
|
||||
<VariableLegend>
|
||||
<Trans i18nKey="dashboard-scene.custom-variable-form.selection-options">Selection options</Trans>
|
||||
</VariableLegend>
|
||||
@@ -58,6 +77,8 @@ export function CustomVariableForm({
|
||||
includeAll={includeAll}
|
||||
allValue={allValue}
|
||||
allowCustomValue={allowCustomValue}
|
||||
disableAllowCustomValue={valuesFormat === 'json'}
|
||||
disableCustomAllValue={valuesFormat === 'json'}
|
||||
onMultiChange={onMultiChange}
|
||||
onIncludeAllChange={onIncludeAllChange}
|
||||
onAllValueChange={onAllValueChange}
|
||||
@@ -66,3 +87,48 @@ export function CustomVariableForm({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface ValuesFormatSelectorProps {
|
||||
valuesFormat?: CustomVariableModel['valuesFormat'];
|
||||
onValuesFormatChange?: (format: CustomVariableModel['valuesFormat']) => void;
|
||||
}
|
||||
|
||||
export function ValuesFormatSelector({ valuesFormat, onValuesFormatChange }: ValuesFormatSelectorProps) {
|
||||
return (
|
||||
<Stack direction="row" gap={1}>
|
||||
<RadioButtonGroup
|
||||
value={valuesFormat}
|
||||
onChange={onValuesFormatChange}
|
||||
options={[
|
||||
{
|
||||
value: 'csv',
|
||||
label: t('dashboard-scene.custom-variable-form.name-values-separated-comma', 'Values separated by comma'),
|
||||
},
|
||||
{
|
||||
value: 'json',
|
||||
label: t('dashboard-scene.custom-variable-form.name-json-values', 'Object values in a JSON array'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{valuesFormat === 'json' && (
|
||||
<Tooltip
|
||||
content={
|
||||
<Trans i18nKey="dashboard-scene.custom-variable-form.json-values-tooltip">
|
||||
Provide a JSON representing an array of objects, where each object can have any number of properties.
|
||||
<br />
|
||||
Check{' '}
|
||||
<TextLink href="https://grafana.com/docs/grafana/latest/variables/xxx" external>
|
||||
our docs
|
||||
</TextLink>{' '}
|
||||
for more information.
|
||||
</Trans>
|
||||
}
|
||||
placement="top"
|
||||
interactive
|
||||
>
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
+53
-38
@@ -7,7 +7,7 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { QueryVariable } from '@grafana/scenes';
|
||||
import { DataSourceRef, VariableRefresh, VariableSort } from '@grafana/schema';
|
||||
import { Field, TextLink } from '@grafana/ui';
|
||||
import { Box, Field, TextLink } from '@grafana/ui';
|
||||
import { QueryEditor } from 'app/features/dashboard-scene/settings/variables/components/QueryEditor';
|
||||
import { SelectionOptionsForm } from 'app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm';
|
||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||
@@ -15,9 +15,9 @@ import { getVariableQueryEditor } from 'app/features/variables/editor/getVariabl
|
||||
import { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect';
|
||||
import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect';
|
||||
import {
|
||||
QueryVariableStaticOptions,
|
||||
StaticOptionsOrderType,
|
||||
StaticOptionsType,
|
||||
QueryVariableStaticOptions,
|
||||
} from 'app/features/variables/query/QueryVariableStaticOptions';
|
||||
|
||||
import { VariableLegend } from './VariableLegend';
|
||||
@@ -34,6 +34,7 @@ interface QueryVariableEditorFormProps {
|
||||
timeRange: TimeRange;
|
||||
regex: string | null;
|
||||
onRegExChange: (event: FormEvent<HTMLTextAreaElement>) => void;
|
||||
disableRegexEdition?: boolean;
|
||||
sort: VariableSort;
|
||||
onSortChange: (option: SelectableValue<VariableSort>) => void;
|
||||
refresh: VariableRefresh;
|
||||
@@ -42,14 +43,17 @@ interface QueryVariableEditorFormProps {
|
||||
onMultiChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||
allowCustomValue?: boolean;
|
||||
onAllowCustomValueChange?: (event: FormEvent<HTMLInputElement>) => void;
|
||||
disableAllowCustomValue?: boolean;
|
||||
includeAll: boolean;
|
||||
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||
allValue: string;
|
||||
onAllValueChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||
disableCustomAllValue?: boolean;
|
||||
staticOptions?: StaticOptionsType;
|
||||
staticOptionsOrder?: StaticOptionsOrderType;
|
||||
onStaticOptionsChange?: (staticOptions: StaticOptionsType) => void;
|
||||
onStaticOptionsOrderChange?: (staticOptionsOrder: StaticOptionsOrderType) => void;
|
||||
disableStaticOptions?: boolean;
|
||||
}
|
||||
|
||||
export function QueryVariableEditorForm({
|
||||
@@ -61,6 +65,7 @@ export function QueryVariableEditorForm({
|
||||
timeRange,
|
||||
regex,
|
||||
onRegExChange,
|
||||
disableRegexEdition,
|
||||
sort,
|
||||
onSortChange,
|
||||
refresh,
|
||||
@@ -69,14 +74,17 @@ export function QueryVariableEditorForm({
|
||||
onMultiChange,
|
||||
allowCustomValue,
|
||||
onAllowCustomValueChange,
|
||||
disableAllowCustomValue,
|
||||
includeAll,
|
||||
onIncludeAllChange,
|
||||
allValue,
|
||||
onAllValueChange,
|
||||
disableCustomAllValue,
|
||||
staticOptions,
|
||||
staticOptionsOrder,
|
||||
onStaticOptionsChange,
|
||||
onStaticOptionsOrderChange,
|
||||
disableStaticOptions,
|
||||
}: QueryVariableEditorFormProps) {
|
||||
const { value: dsConfig } = useAsync(async () => {
|
||||
const datasource = await getDataSourceSrv().get(datasourceRef ?? '');
|
||||
@@ -116,48 +124,53 @@ export function QueryVariableEditorForm({
|
||||
<Field
|
||||
label={t('dashboard-scene.query-variable-editor-form.label-data-source', 'Data source')}
|
||||
htmlFor="data-source-picker"
|
||||
noMargin
|
||||
>
|
||||
<DataSourcePicker current={datasourceRef} onChange={datasourceChangeHandler} variables={true} width={30} />
|
||||
</Field>
|
||||
|
||||
{datasource && VariableQueryEditor && (
|
||||
<QueryEditor
|
||||
onQueryChange={onQueryChange}
|
||||
onLegacyQueryChange={onLegacyQueryChange}
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
VariableQueryEditor={VariableQueryEditor}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
<Box marginBottom={2}>
|
||||
<QueryEditor
|
||||
onQueryChange={onQueryChange}
|
||||
onLegacyQueryChange={onLegacyQueryChange}
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
VariableQueryEditor={VariableQueryEditor}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<VariableTextAreaField
|
||||
defaultValue={regex ?? ''}
|
||||
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
|
||||
description={
|
||||
<div>
|
||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
|
||||
Optional, if you want to extract part of a series name or metric node segment.
|
||||
</Trans>
|
||||
<br />
|
||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
|
||||
Named capture groups can be used to separate the display text and value (
|
||||
<TextLink
|
||||
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
|
||||
external
|
||||
>
|
||||
see examples
|
||||
</TextLink>
|
||||
).
|
||||
</Trans>
|
||||
</div>
|
||||
}
|
||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
|
||||
onBlur={onRegExChange}
|
||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
|
||||
width={52}
|
||||
/>
|
||||
{!disableRegexEdition && (
|
||||
<VariableTextAreaField
|
||||
defaultValue={regex ?? ''}
|
||||
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
|
||||
description={
|
||||
<div>
|
||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
|
||||
Optional, if you want to extract part of a series name or metric node segment.
|
||||
</Trans>
|
||||
<br />
|
||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
|
||||
Named capture groups can be used to separate the display text and value (
|
||||
<TextLink
|
||||
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
|
||||
external
|
||||
>
|
||||
see examples
|
||||
</TextLink>
|
||||
).
|
||||
</Trans>
|
||||
</div>
|
||||
}
|
||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
|
||||
onBlur={onRegExChange}
|
||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
|
||||
width={52}
|
||||
/>
|
||||
)}
|
||||
|
||||
<QueryVariableSortSelect
|
||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2}
|
||||
@@ -171,7 +184,7 @@ export function QueryVariableEditorForm({
|
||||
refresh={refresh}
|
||||
/>
|
||||
|
||||
{onStaticOptionsChange && onStaticOptionsOrderChange && (
|
||||
{!disableStaticOptions && onStaticOptionsChange && onStaticOptionsOrderChange && (
|
||||
<QueryVariableStaticOptions
|
||||
staticOptions={staticOptions}
|
||||
staticOptionsOrder={staticOptionsOrder}
|
||||
@@ -187,6 +200,8 @@ export function QueryVariableEditorForm({
|
||||
multi={!!isMulti}
|
||||
includeAll={!!includeAll}
|
||||
allowCustomValue={allowCustomValue}
|
||||
disableAllowCustomValue={disableAllowCustomValue}
|
||||
disableCustomAllValue={disableCustomAllValue}
|
||||
allValue={allValue}
|
||||
onMultiChange={onMultiChange}
|
||||
onIncludeAllChange={onIncludeAllChange}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user