mirror of
https://github.com/grafana/grafana.git
synced 2025-12-21 03:54:29 +08:00
Compare commits
36 Commits
docs/updat
...
hackathon-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
637ec53f6b | ||
|
|
6b6a434383 | ||
|
|
6d6112b627 | ||
|
|
41f9162472 | ||
|
|
7e991886e0 | ||
|
|
85925d0765 | ||
|
|
7790698aaa | ||
|
|
5499ad8023 | ||
|
|
90c4ab9d96 | ||
|
|
fd31f087ee | ||
|
|
3ee834922b | ||
|
|
2e2ce8fddd | ||
|
|
8214dbc758 | ||
|
|
98d454401c | ||
|
|
fcf1a47222 | ||
|
|
8a5b6804dd | ||
|
|
f0028f692b | ||
|
|
d71474246c | ||
|
|
9447015e54 | ||
|
|
abe10b2bb6 | ||
|
|
009716a408 | ||
|
|
e0c28cfa4c | ||
|
|
18c4f5b875 | ||
|
|
400f3a91d0 | ||
|
|
d6b04d28b6 | ||
|
|
0400d536c7 | ||
|
|
694e88b95b | ||
|
|
ad73303328 | ||
|
|
3dcd809aaf | ||
|
|
6b7fac65b1 | ||
|
|
2d17de2395 | ||
|
|
5b685373aa | ||
|
|
4d29e5bf6a | ||
|
|
7a0e64196b | ||
|
|
f1e24f528e | ||
|
|
198f4dbf93 |
@@ -7,4 +7,4 @@ generate: install-app-sdk update-app-sdk
|
|||||||
--gogenpath=./pkg/apis \
|
--gogenpath=./pkg/apis \
|
||||||
--grouping=group \
|
--grouping=group \
|
||||||
--genoperatorstate=false \
|
--genoperatorstate=false \
|
||||||
--defencoding=none
|
--defencoding=none
|
||||||
|
|||||||
35
apps/collections/kinds/datasourcestacks.cue
Normal file
35
apps/collections/kinds/datasourcestacks.cue
Normal file
@@ -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: {
|
versions: {
|
||||||
"v1alpha1": {
|
"v1alpha1": {
|
||||||
codegen: {
|
codegen: {
|
||||||
ts: {enabled: false}
|
ts: {enabled: true}
|
||||||
go: {enabled: true}
|
go: {enabled: true}
|
||||||
}
|
}
|
||||||
kinds: [
|
kinds: [
|
||||||
starsV1alpha1,
|
starsV1alpha1,
|
||||||
|
datasourcestacksV1alpha1
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
80
apps/collections/pkg/apis/collections/v1alpha1/datasourcestack_client_gen.go
generated
Normal file
80
apps/collections/pkg/apis/collections/v1alpha1/datasourcestack_client_gen.go
generated
Normal file
@@ -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)
|
||||||
|
}
|
||||||
28
apps/collections/pkg/apis/collections/v1alpha1/datasourcestack_codec_gen.go
generated
Normal file
28
apps/collections/pkg/apis/collections/v1alpha1/datasourcestack_codec_gen.go
generated
Normal file
@@ -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
apps/collections/pkg/apis/collections/v1alpha1/datasourcestack_metadata_gen.go
generated
Normal file
31
apps/collections/pkg/apis/collections/v1alpha1/datasourcestack_metadata_gen.go
generated
Normal file
@@ -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
apps/collections/pkg/apis/collections/v1alpha1/datasourcestack_object_gen.go
generated
Normal file
293
apps/collections/pkg/apis/collections/v1alpha1/datasourcestack_object_gen.go
generated
Normal file
@@ -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
apps/collections/pkg/apis/collections/v1alpha1/datasourcestack_schema_gen.go
generated
Normal file
34
apps/collections/pkg/apis/collections/v1alpha1/datasourcestack_schema_gen.go
generated
Normal file
@@ -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
|
||||||
58
apps/collections/pkg/apis/collections/v1alpha1/datasourcestack_spec_gen.go
generated
Normal file
58
apps/collections/pkg/apis/collections/v1alpha1/datasourcestack_spec_gen.go
generated
Normal file
@@ -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 (
|
var (
|
||||||
SchemeBuilder runtime.SchemeBuilder
|
SchemeBuilder runtime.SchemeBuilder
|
||||||
localSchemeBuilder = &SchemeBuilder
|
localSchemeBuilder = &SchemeBuilder
|
||||||
@@ -48,6 +61,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
|||||||
scheme.AddKnownTypes(schemeGroupVersion,
|
scheme.AddKnownTypes(schemeGroupVersion,
|
||||||
&Stars{},
|
&Stars{},
|
||||||
&StarsList{},
|
&StarsList{},
|
||||||
|
&DataSourceStack{},
|
||||||
|
&DataSourceStackList{},
|
||||||
)
|
)
|
||||||
metav1.AddToGroupVersion(scheme, schemeGroupVersion)
|
metav1.AddToGroupVersion(scheme, schemeGroupVersion)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -14,10 +14,241 @@ import (
|
|||||||
|
|
||||||
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
|
||||||
return 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.DataSourceStack": schema_pkg_apis_collections_v1alpha1_DataSourceStack(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.DataSourceStackDataSourceStackTemplateItem": schema_pkg_apis_collections_v1alpha1_DataSourceStackDataSourceStackTemplateItem(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.DataSourceStackList": schema_pkg_apis_collections_v1alpha1_DataSourceStackList(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.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"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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: 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
|
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1,StarsList,ListMeta
|
||||||
|
|||||||
26
apps/collections/pkg/apis/collections_manifest.go
generated
26
apps/collections/pkg/apis/collections_manifest.go
generated
@@ -10,19 +10,22 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-app-sdk/app"
|
||||||
|
"github.com/grafana/grafana-app-sdk/resource"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/kube-openapi/pkg/spec3"
|
"k8s.io/kube-openapi/pkg/spec3"
|
||||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
|
|
||||||
"github.com/grafana/grafana-app-sdk/app"
|
v1alpha1 "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
||||||
"github.com/grafana/grafana-app-sdk/resource"
|
|
||||||
"github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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"}}`)
|
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
|
versionSchemaStarsv1alpha1 app.VersionSchema
|
||||||
_ = json.Unmarshal(rawSchemaStarsv1alpha1, &versionSchemaStarsv1alpha1)
|
_ = 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{
|
var appManifestData = app.ManifestData{
|
||||||
@@ -49,6 +52,14 @@ var appManifestData = app.ManifestData{
|
|||||||
},
|
},
|
||||||
Schema: &versionSchemaStarsv1alpha1,
|
Schema: &versionSchemaStarsv1alpha1,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Kind: "DataSourceStack",
|
||||||
|
Plural: "DataSourceStacks",
|
||||||
|
Scope: "Namespaced",
|
||||||
|
Conversion: false,
|
||||||
|
Schema: &versionSchemaDataSourceStackv1alpha1,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Routes: app.ManifestVersionRoutes{
|
Routes: app.ManifestVersionRoutes{
|
||||||
Namespaced: map[string]spec3.PathProps{},
|
Namespaced: map[string]spec3.PathProps{},
|
||||||
@@ -68,7 +79,8 @@ func RemoteManifest() app.Manifest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var kindVersionToGoType = map[string]resource.Kind{
|
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.
|
// ManifestGoTypeAssociator returns the associated resource.Kind instance for a given Kind and Version, if one exists.
|
||||||
|
|||||||
47
apps/collections/plugin/src/generated/datasourcestack/v1alpha1/datasourcestack_object_gen.ts
generated
Normal file
47
apps/collections/plugin/src/generated/datasourcestack/v1alpha1/datasourcestack_object_gen.ts
generated
Normal file
@@ -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
apps/collections/plugin/src/generated/datasourcestack/v1alpha1/types.metadata.gen.ts
generated
Normal file
30
apps/collections/plugin/src/generated/datasourcestack/v1alpha1/types.metadata.gen.ts
generated
Normal file
@@ -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
apps/collections/plugin/src/generated/datasourcestack/v1alpha1/types.spec.gen.ts
generated
Normal file
53
apps/collections/plugin/src/generated/datasourcestack/v1alpha1/types.spec.gen.ts
generated
Normal file
@@ -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: [],
|
||||||
|
});
|
||||||
|
|
||||||
47
apps/collections/plugin/src/generated/stars/v1alpha1/stars_object_gen.ts
generated
Normal file
47
apps/collections/plugin/src/generated/stars/v1alpha1/stars_object_gen.ts
generated
Normal file
@@ -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;
|
||||||
|
}
|
||||||
30
apps/collections/plugin/src/generated/stars/v1alpha1/types.metadata.gen.ts
generated
Normal file
30
apps/collections/plugin/src/generated/stars/v1alpha1/types.metadata.gen.ts
generated
Normal file
@@ -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: {},
|
||||||
|
});
|
||||||
|
|
||||||
24
apps/collections/plugin/src/generated/stars/v1alpha1/types.spec.gen.ts
generated
Normal file
24
apps/collections/plugin/src/generated/stars/v1alpha1/types.spec.gen.ts
generated
Normal file
@@ -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: [],
|
||||||
|
});
|
||||||
|
|
||||||
@@ -725,9 +725,9 @@ VariableCustomFormatterFn: {
|
|||||||
// `custom`: Define the variable options manually using a comma-separated list.
|
// `custom`: Define the variable options manually using a comma-separated list.
|
||||||
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
||||||
VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
||||||
"system" | "snapshot" | "switch"
|
"system" | "snapshot" | "switch" | "stack"
|
||||||
|
|
||||||
VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind
|
VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind | StackVariableKind
|
||||||
|
|
||||||
// Sort variable options
|
// Sort variable options
|
||||||
// Accepted values are:
|
// Accepted values are:
|
||||||
@@ -911,6 +911,7 @@ CustomVariableSpec: {
|
|||||||
skipUrlSync: bool | *false
|
skipUrlSync: bool | *false
|
||||||
description?: string
|
description?: string
|
||||||
allowCustomValue: bool | *true
|
allowCustomValue: bool | *true
|
||||||
|
valuesFormat?: "csv" | "json"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom variable kind
|
// Custom variable kind
|
||||||
@@ -935,6 +936,24 @@ SwitchVariableKind: {
|
|||||||
spec: SwitchVariableSpec
|
spec: SwitchVariableSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StackVariableSpec: {
|
||||||
|
name: string | *""
|
||||||
|
defaultValue?: VariableOption
|
||||||
|
current: VariableOption | *{
|
||||||
|
text: ""
|
||||||
|
value: ""
|
||||||
|
}
|
||||||
|
options: [...VariableOption] | *[]
|
||||||
|
label?: string
|
||||||
|
description?: string
|
||||||
|
properties: [string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
StackVariableKind: {
|
||||||
|
kind: "StackVariable"
|
||||||
|
spec: StackVariableSpec
|
||||||
|
}
|
||||||
|
|
||||||
// GroupBy variable specification
|
// GroupBy variable specification
|
||||||
GroupByVariableSpec: {
|
GroupByVariableSpec: {
|
||||||
name: string | *""
|
name: string | *""
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ lineage: schemas: [{
|
|||||||
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
||||||
// `switch`: Boolean variables rendered as a switch
|
// `switch`: Boolean variables rendered as a switch
|
||||||
#VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
#VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
||||||
"system" | "snapshot" | "switch" @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview)
|
"system" | "snapshot" | "switch" | "stack" @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview)
|
||||||
|
|
||||||
// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
|
// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
|
||||||
// Continuous color interpolates a color using the percentage of a value relative to min and max.
|
// Continuous color interpolates a color using the percentage of a value relative to min and max.
|
||||||
|
|||||||
@@ -294,6 +294,8 @@ var _ resource.ListObject = &DashboardList{}
|
|||||||
|
|
||||||
// Copy methods for all subresource types
|
// Copy methods for all subresource types
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// DeepCopy creates a full deep copy of DashboardStatus
|
// DeepCopy creates a full deep copy of DashboardStatus
|
||||||
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||||
cpy := &DashboardStatus{}
|
cpy := &DashboardStatus{}
|
||||||
|
|||||||
@@ -373,7 +373,7 @@ lineage: schemas: [{
|
|||||||
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
||||||
// `switch`: Boolean variables rendered as a switch
|
// `switch`: Boolean variables rendered as a switch
|
||||||
#VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
#VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
||||||
"system" | "snapshot" | "switch" @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview)
|
"system" | "snapshot" | "switch" | "stack" @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview)
|
||||||
|
|
||||||
// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
|
// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
|
||||||
// Continuous color interpolates a color using the percentage of a value relative to min and max.
|
// Continuous color interpolates a color using the percentage of a value relative to min and max.
|
||||||
|
|||||||
@@ -294,6 +294,8 @@ var _ resource.ListObject = &DashboardList{}
|
|||||||
|
|
||||||
// Copy methods for all subresource types
|
// Copy methods for all subresource types
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// DeepCopy creates a full deep copy of DashboardStatus
|
// DeepCopy creates a full deep copy of DashboardStatus
|
||||||
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
func (s *DashboardStatus) DeepCopy() *DashboardStatus {
|
||||||
cpy := &DashboardStatus{}
|
cpy := &DashboardStatus{}
|
||||||
|
|||||||
@@ -729,9 +729,9 @@ VariableCustomFormatterFn: {
|
|||||||
// `custom`: Define the variable options manually using a comma-separated list.
|
// `custom`: Define the variable options manually using a comma-separated list.
|
||||||
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
||||||
VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
||||||
"system" | "snapshot" | "switch"
|
"system" | "snapshot" | "switch" | "stack"
|
||||||
|
|
||||||
VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind
|
VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind | StackVariableKind
|
||||||
|
|
||||||
// Sort variable options
|
// Sort variable options
|
||||||
// Accepted values are:
|
// Accepted values are:
|
||||||
@@ -915,6 +915,7 @@ CustomVariableSpec: {
|
|||||||
skipUrlSync: bool | *false
|
skipUrlSync: bool | *false
|
||||||
description?: string
|
description?: string
|
||||||
allowCustomValue: bool | *true
|
allowCustomValue: bool | *true
|
||||||
|
valuesFormat?: "csv" | "json"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom variable kind
|
// Custom variable kind
|
||||||
@@ -939,6 +940,24 @@ SwitchVariableKind: {
|
|||||||
spec: SwitchVariableSpec
|
spec: SwitchVariableSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StackVariableSpec: {
|
||||||
|
name: string | *""
|
||||||
|
defaultValue?: VariableOption
|
||||||
|
current: VariableOption | *{
|
||||||
|
text: ""
|
||||||
|
value: ""
|
||||||
|
}
|
||||||
|
options: [...VariableOption] | *[]
|
||||||
|
label?: string
|
||||||
|
description?: string
|
||||||
|
properties: [string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
StackVariableKind: {
|
||||||
|
kind: "StackVariable"
|
||||||
|
spec: StackVariableSpec
|
||||||
|
}
|
||||||
|
|
||||||
// GroupBy variable specification
|
// GroupBy variable specification
|
||||||
GroupByVariableSpec: {
|
GroupByVariableSpec: {
|
||||||
name: string | *""
|
name: string | *""
|
||||||
|
|||||||
@@ -1313,11 +1313,11 @@ func NewDashboardTimeRangeOption() *DashboardTimeRangeOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// +k8s:openapi-gen=true
|
// +k8s:openapi-gen=true
|
||||||
type DashboardVariableKind = DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind
|
type DashboardVariableKind = DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKindOrStackVariableKind
|
||||||
|
|
||||||
// NewDashboardVariableKind creates a new DashboardVariableKind object.
|
// NewDashboardVariableKind creates a new DashboardVariableKind object.
|
||||||
func NewDashboardVariableKind() *DashboardVariableKind {
|
func NewDashboardVariableKind() *DashboardVariableKind {
|
||||||
return NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind()
|
return NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKindOrStackVariableKind()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query variable kind
|
// Query variable kind
|
||||||
@@ -1675,18 +1675,19 @@ func NewDashboardCustomVariableKind() *DashboardCustomVariableKind {
|
|||||||
// Custom variable specification
|
// Custom variable specification
|
||||||
// +k8s:openapi-gen=true
|
// +k8s:openapi-gen=true
|
||||||
type DashboardCustomVariableSpec struct {
|
type DashboardCustomVariableSpec struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
Current DashboardVariableOption `json:"current"`
|
Current DashboardVariableOption `json:"current"`
|
||||||
Options []DashboardVariableOption `json:"options"`
|
Options []DashboardVariableOption `json:"options"`
|
||||||
Multi bool `json:"multi"`
|
Multi bool `json:"multi"`
|
||||||
IncludeAll bool `json:"includeAll"`
|
IncludeAll bool `json:"includeAll"`
|
||||||
AllValue *string `json:"allValue,omitempty"`
|
AllValue *string `json:"allValue,omitempty"`
|
||||||
Label *string `json:"label,omitempty"`
|
Label *string `json:"label,omitempty"`
|
||||||
Hide DashboardVariableHide `json:"hide"`
|
Hide DashboardVariableHide `json:"hide"`
|
||||||
SkipUrlSync bool `json:"skipUrlSync"`
|
SkipUrlSync bool `json:"skipUrlSync"`
|
||||||
Description *string `json:"description,omitempty"`
|
Description *string `json:"description,omitempty"`
|
||||||
AllowCustomValue bool `json:"allowCustomValue"`
|
AllowCustomValue bool `json:"allowCustomValue"`
|
||||||
|
ValuesFormat *DashboardCustomVariableSpecValuesFormat `json:"valuesFormat,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDashboardCustomVariableSpec creates a new DashboardCustomVariableSpec object.
|
// NewDashboardCustomVariableSpec creates a new DashboardCustomVariableSpec object.
|
||||||
@@ -1876,6 +1877,48 @@ func NewDashboardSwitchVariableSpec() *DashboardSwitchVariableSpec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// +k8s:openapi-gen=true
|
||||||
|
type DashboardStackVariableKind struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
Spec DashboardStackVariableSpec `json:"spec"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDashboardStackVariableKind creates a new DashboardStackVariableKind object.
|
||||||
|
func NewDashboardStackVariableKind() *DashboardStackVariableKind {
|
||||||
|
return &DashboardStackVariableKind{
|
||||||
|
Kind: "StackVariable",
|
||||||
|
Spec: *NewDashboardStackVariableSpec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// +k8s:openapi-gen=true
|
||||||
|
type DashboardStackVariableSpec struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
DefaultValue *DashboardVariableOption `json:"defaultValue,omitempty"`
|
||||||
|
Current DashboardVariableOption `json:"current"`
|
||||||
|
Options []DashboardVariableOption `json:"options"`
|
||||||
|
Label *string `json:"label,omitempty"`
|
||||||
|
Description *string `json:"description,omitempty"`
|
||||||
|
Properties map[string]string `json:"properties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDashboardStackVariableSpec creates a new DashboardStackVariableSpec object.
|
||||||
|
func NewDashboardStackVariableSpec() *DashboardStackVariableSpec {
|
||||||
|
return &DashboardStackVariableSpec{
|
||||||
|
Name: "",
|
||||||
|
Current: DashboardVariableOption{
|
||||||
|
Text: DashboardStringOrArrayOfString{
|
||||||
|
String: (func(input string) *string { return &input })(""),
|
||||||
|
},
|
||||||
|
Value: DashboardStringOrArrayOfString{
|
||||||
|
String: (func(input string) *string { return &input })(""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Options: []DashboardVariableOption{},
|
||||||
|
Properties: map[string]string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// +k8s:openapi-gen=true
|
// +k8s:openapi-gen=true
|
||||||
type DashboardSpec struct {
|
type DashboardSpec struct {
|
||||||
Annotations []DashboardAnnotationQueryKind `json:"annotations"`
|
Annotations []DashboardAnnotationQueryKind `json:"annotations"`
|
||||||
@@ -2101,6 +2144,14 @@ const (
|
|||||||
DashboardQueryVariableSpecStaticOptionsOrderSorted DashboardQueryVariableSpecStaticOptionsOrder = "sorted"
|
DashboardQueryVariableSpecStaticOptionsOrderSorted DashboardQueryVariableSpecStaticOptionsOrder = "sorted"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// +k8s:openapi-gen=true
|
||||||
|
type DashboardCustomVariableSpecValuesFormat string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DashboardCustomVariableSpecValuesFormatCsv DashboardCustomVariableSpecValuesFormat = "csv"
|
||||||
|
DashboardCustomVariableSpecValuesFormatJson DashboardCustomVariableSpecValuesFormat = "json"
|
||||||
|
)
|
||||||
|
|
||||||
// +k8s:openapi-gen=true
|
// +k8s:openapi-gen=true
|
||||||
type DashboardPanelKindOrLibraryPanelKind struct {
|
type DashboardPanelKindOrLibraryPanelKind struct {
|
||||||
PanelKind *DashboardPanelKind `json:"PanelKind,omitempty"`
|
PanelKind *DashboardPanelKind `json:"PanelKind,omitempty"`
|
||||||
@@ -2496,7 +2547,7 @@ func (resource *DashboardGridLayoutKindOrRowsLayoutKindOrAutoGridLayoutKindOrTab
|
|||||||
}
|
}
|
||||||
|
|
||||||
// +k8s:openapi-gen=true
|
// +k8s:openapi-gen=true
|
||||||
type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind struct {
|
type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKindOrStackVariableKind struct {
|
||||||
QueryVariableKind *DashboardQueryVariableKind `json:"QueryVariableKind,omitempty"`
|
QueryVariableKind *DashboardQueryVariableKind `json:"QueryVariableKind,omitempty"`
|
||||||
TextVariableKind *DashboardTextVariableKind `json:"TextVariableKind,omitempty"`
|
TextVariableKind *DashboardTextVariableKind `json:"TextVariableKind,omitempty"`
|
||||||
ConstantVariableKind *DashboardConstantVariableKind `json:"ConstantVariableKind,omitempty"`
|
ConstantVariableKind *DashboardConstantVariableKind `json:"ConstantVariableKind,omitempty"`
|
||||||
@@ -2506,15 +2557,16 @@ type DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasou
|
|||||||
GroupByVariableKind *DashboardGroupByVariableKind `json:"GroupByVariableKind,omitempty"`
|
GroupByVariableKind *DashboardGroupByVariableKind `json:"GroupByVariableKind,omitempty"`
|
||||||
AdhocVariableKind *DashboardAdhocVariableKind `json:"AdhocVariableKind,omitempty"`
|
AdhocVariableKind *DashboardAdhocVariableKind `json:"AdhocVariableKind,omitempty"`
|
||||||
SwitchVariableKind *DashboardSwitchVariableKind `json:"SwitchVariableKind,omitempty"`
|
SwitchVariableKind *DashboardSwitchVariableKind `json:"SwitchVariableKind,omitempty"`
|
||||||
|
StackVariableKind *DashboardStackVariableKind `json:"StackVariableKind,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind creates a new DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind object.
|
// NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKindOrStackVariableKind creates a new DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKindOrStackVariableKind object.
|
||||||
func NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind() *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind {
|
func NewDashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKindOrStackVariableKind() *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKindOrStackVariableKind {
|
||||||
return &DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind{}
|
return &DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKindOrStackVariableKind{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind` as JSON.
|
// MarshalJSON implements a custom JSON marshalling logic to encode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKindOrStackVariableKind` as JSON.
|
||||||
func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind) MarshalJSON() ([]byte, error) {
|
func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKindOrStackVariableKind) MarshalJSON() ([]byte, error) {
|
||||||
if resource.QueryVariableKind != nil {
|
if resource.QueryVariableKind != nil {
|
||||||
return json.Marshal(resource.QueryVariableKind)
|
return json.Marshal(resource.QueryVariableKind)
|
||||||
}
|
}
|
||||||
@@ -2542,12 +2594,15 @@ func (resource DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKin
|
|||||||
if resource.SwitchVariableKind != nil {
|
if resource.SwitchVariableKind != nil {
|
||||||
return json.Marshal(resource.SwitchVariableKind)
|
return json.Marshal(resource.SwitchVariableKind)
|
||||||
}
|
}
|
||||||
|
if resource.StackVariableKind != nil {
|
||||||
|
return json.Marshal(resource.StackVariableKind)
|
||||||
|
}
|
||||||
|
|
||||||
return []byte("null"), nil
|
return []byte("null"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind` from JSON.
|
// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKindOrStackVariableKind` from JSON.
|
||||||
func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKind) UnmarshalJSON(raw []byte) error {
|
func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKindOrDatasourceVariableKindOrIntervalVariableKindOrCustomVariableKindOrGroupByVariableKindOrAdhocVariableKindOrSwitchVariableKindOrStackVariableKind) UnmarshalJSON(raw []byte) error {
|
||||||
if raw == nil {
|
if raw == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -2620,6 +2675,14 @@ func (resource *DashboardQueryVariableKindOrTextVariableKindOrConstantVariableKi
|
|||||||
|
|
||||||
resource.QueryVariableKind = &dashboardQueryVariableKind
|
resource.QueryVariableKind = &dashboardQueryVariableKind
|
||||||
return nil
|
return nil
|
||||||
|
case "StackVariable":
|
||||||
|
var dashboardStackVariableKind DashboardStackVariableKind
|
||||||
|
if err := json.Unmarshal(raw, &dashboardStackVariableKind); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resource.StackVariableKind = &dashboardStackVariableKind
|
||||||
|
return nil
|
||||||
case "SwitchVariable":
|
case "SwitchVariable":
|
||||||
var dashboardSwitchVariableKind DashboardSwitchVariableKind
|
var dashboardSwitchVariableKind DashboardSwitchVariableKind
|
||||||
if err := json.Unmarshal(raw, &dashboardSwitchVariableKind); err != nil {
|
if err := json.Unmarshal(raw, &dashboardSwitchVariableKind); err != nil {
|
||||||
|
|||||||
@@ -1510,6 +1510,12 @@ func schema_pkg_apis_dashboard_v2beta1_DashboardCustomVariableSpec(ref common.Re
|
|||||||
Format: "",
|
Format: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"valuesFormat": {
|
||||||
|
SchemaProps: spec.SchemaProps{
|
||||||
|
Type: []string{"string"},
|
||||||
|
Format: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Required: []string{"name", "query", "current", "options", "multi", "includeAll", "hide", "skipUrlSync", "allowCustomValue"},
|
Required: []string{"name", "query", "current", "options", "multi", "includeAll", "hide", "skipUrlSync", "allowCustomValue"},
|
||||||
},
|
},
|
||||||
|
|||||||
2
apps/dashboard/pkg/apis/dashboard_manifest.go
generated
2
apps/dashboard/pkg/apis/dashboard_manifest.go
generated
@@ -21,6 +21,8 @@ import (
|
|||||||
v2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
|
v2beta1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2beta1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ()
|
||||||
|
|
||||||
var appManifestData = app.ManifestData{
|
var appManifestData = app.ManifestData{
|
||||||
AppName: "dashboard",
|
AppName: "dashboard",
|
||||||
Group: "dashboard.grafana.app",
|
Group: "dashboard.grafana.app",
|
||||||
|
|||||||
@@ -181,6 +181,8 @@ import (
|
|||||||
//go:generate mockery --name InterfaceName --structname MockImplementationName --inpackage --filename my_implementation_mock.go
|
//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
|
## Globals
|
||||||
|
|
||||||
As a general rule of thumb, avoid using global variables, since they make the code difficult to maintain and reason
|
As a general rule of thumb, avoid using global variables, since they make the code difficult to maintain and reason
|
||||||
|
|||||||
@@ -369,7 +369,7 @@ lineage: schemas: [{
|
|||||||
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
||||||
// `switch`: Boolean variables rendered as a switch
|
// `switch`: Boolean variables rendered as a switch
|
||||||
#VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
#VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
||||||
"system" | "snapshot" | "switch" @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview)
|
"system" | "snapshot" | "switch" | "stack" @cuetsy(kind="type") @grafanamaturity(NeedsExpertReview)
|
||||||
|
|
||||||
// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
|
// Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
|
||||||
// Continuous color interpolates a color using the percentage of a value relative to min and max.
|
// Continuous color interpolates a color using the percentage of a value relative to min and max.
|
||||||
|
|||||||
@@ -296,8 +296,8 @@
|
|||||||
"@grafana/plugin-ui": "^0.11.1",
|
"@grafana/plugin-ui": "^0.11.1",
|
||||||
"@grafana/prometheus": "workspace:*",
|
"@grafana/prometheus": "workspace:*",
|
||||||
"@grafana/runtime": "workspace:*",
|
"@grafana/runtime": "workspace:*",
|
||||||
"@grafana/scenes": "6.47.1",
|
"@grafana/scenes": "^6.48.0",
|
||||||
"@grafana/scenes-react": "6.47.1",
|
"@grafana/scenes-react": "^6.48.0",
|
||||||
"@grafana/schema": "workspace:*",
|
"@grafana/schema": "workspace:*",
|
||||||
"@grafana/sql": "workspace:*",
|
"@grafana/sql": "workspace:*",
|
||||||
"@grafana/ui": "workspace:*",
|
"@grafana/ui": "workspace:*",
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export type TypedVariableModel =
|
|||||||
| OrgVariableModel
|
| OrgVariableModel
|
||||||
| DashboardVariableModel
|
| DashboardVariableModel
|
||||||
| SnapshotVariableModel
|
| SnapshotVariableModel
|
||||||
| SwitchVariableModel;
|
| SwitchVariableModel
|
||||||
|
| StackVariableModel;
|
||||||
|
|
||||||
export enum VariableRefresh {
|
export enum VariableRefresh {
|
||||||
never, // removed from the UI
|
never, // removed from the UI
|
||||||
@@ -101,8 +102,16 @@ export interface IntervalVariableModel extends VariableWithOptions {
|
|||||||
|
|
||||||
export interface CustomVariableModel extends VariableWithMultiSupport {
|
export interface CustomVariableModel extends VariableWithMultiSupport {
|
||||||
type: 'custom';
|
type: 'custom';
|
||||||
|
valuesFormat?: 'csv' | 'json';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StackVariableModel extends VariableWithMultiSupport {
|
||||||
|
type: 'stack';
|
||||||
|
properties: StackVariableValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StackVariableValue = Record<string,string>;
|
||||||
|
|
||||||
export interface DataSourceVariableModel extends VariableWithMultiSupport {
|
export interface DataSourceVariableModel extends VariableWithMultiSupport {
|
||||||
type: 'datasource';
|
type: 'datasource';
|
||||||
regex: string;
|
regex: string;
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ export const defaultAction: Partial<Action> = {
|
|||||||
* `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
* `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
||||||
* `switch`: Boolean variables rendered as a switch
|
* `switch`: Boolean variables rendered as a switch
|
||||||
*/
|
*/
|
||||||
export type VariableType = ('query' | 'adhoc' | 'groupby' | 'constant' | 'datasource' | 'interval' | 'textbox' | 'custom' | 'system' | 'snapshot' | 'switch');
|
export type VariableType = ('query' | 'adhoc' | 'groupby' | 'constant' | 'datasource' | 'interval' | 'textbox' | 'custom' | 'system' | 'snapshot' | 'switch' | 'stack');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
|
* Color mode for a field. You can specify a single color, or select a continuous (gradient) color schemes, based on a value.
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ export const handyTestingSchema: Spec = {
|
|||||||
query: 'option1, option2',
|
query: 'option1, option2',
|
||||||
skipUrlSync: false,
|
skipUrlSync: false,
|
||||||
allowCustomValue: true,
|
allowCustomValue: true,
|
||||||
|
valuesFormat: 'csv',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -672,9 +672,9 @@ VariableCustomFormatterFn: {
|
|||||||
// `custom`: Define the variable options manually using a comma-separated list.
|
// `custom`: Define the variable options manually using a comma-separated list.
|
||||||
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
// `system`: Variables defined by Grafana. See: https://grafana.com/docs/grafana/latest/dashboards/variables/add-template-variables/#global-variables
|
||||||
VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
VariableType: "query" | "adhoc" | "groupby" | "constant" | "datasource" | "interval" | "textbox" | "custom" |
|
||||||
"system" | "snapshot"
|
"system" | "snapshot" | "stack"
|
||||||
|
|
||||||
VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind
|
VariableKind: QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | StackVariableKind
|
||||||
|
|
||||||
// Sort variable options
|
// Sort variable options
|
||||||
// Accepted values are:
|
// Accepted values are:
|
||||||
@@ -839,6 +839,23 @@ IntervalVariableKind: {
|
|||||||
spec: IntervalVariableSpec
|
spec: IntervalVariableSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StackVariableSpec: {
|
||||||
|
name: string | *""
|
||||||
|
defaultValue?: VariableOption
|
||||||
|
current: VariableOption | *{
|
||||||
|
text: ""
|
||||||
|
value: ""
|
||||||
|
}
|
||||||
|
options: [...VariableValueOption] | *[]
|
||||||
|
label?: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
StackVariableKind: {
|
||||||
|
kind: "StackVariable"
|
||||||
|
spec: StackVariableSpec
|
||||||
|
}
|
||||||
|
|
||||||
// Custom variable specification
|
// Custom variable specification
|
||||||
CustomVariableSpec: {
|
CustomVariableSpec: {
|
||||||
name: string | *""
|
name: string | *""
|
||||||
|
|||||||
@@ -1055,7 +1055,7 @@ export const defaultTimeRangeOption = (): TimeRangeOption => ({
|
|||||||
to: "now",
|
to: "now",
|
||||||
});
|
});
|
||||||
|
|
||||||
export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind;
|
export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind | StackVariableKind;
|
||||||
|
|
||||||
export const defaultVariableKind = (): VariableKind => (defaultQueryVariableKind());
|
export const defaultVariableKind = (): VariableKind => (defaultQueryVariableKind());
|
||||||
|
|
||||||
@@ -1472,6 +1472,33 @@ export const defaultSwitchVariableSpec = (): SwitchVariableSpec => ({
|
|||||||
skipUrlSync: false,
|
skipUrlSync: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface StackVariableKind {
|
||||||
|
kind: "StackVariable";
|
||||||
|
spec: StackVariableSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultStackVariableKind = (): StackVariableKind => ({
|
||||||
|
kind: "StackVariable",
|
||||||
|
spec: defaultStackVariableSpec(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface StackVariableSpec {
|
||||||
|
name: string;
|
||||||
|
defaultValue?: VariableOption;
|
||||||
|
current: VariableOption;
|
||||||
|
options: VariableOption[];
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
properties: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultStackVariableSpec = (): StackVariableSpec => ({
|
||||||
|
name: "",
|
||||||
|
current: { text: "", value: "", },
|
||||||
|
options: [],
|
||||||
|
properties: {},
|
||||||
|
});
|
||||||
|
|
||||||
export interface Spec {
|
export interface Spec {
|
||||||
annotations: AnnotationQueryKind[];
|
annotations: AnnotationQueryKind[];
|
||||||
// Configuration of dashboard cursor sync behavior.
|
// Configuration of dashboard cursor sync behavior.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||||
|
|
||||||
|
import { VariableValueOption } from "../v2alpha0";
|
||||||
|
|
||||||
export interface AnnotationQueryKind {
|
export interface AnnotationQueryKind {
|
||||||
kind: "AnnotationQuery";
|
kind: "AnnotationQuery";
|
||||||
spec: AnnotationQuerySpec;
|
spec: AnnotationQuerySpec;
|
||||||
@@ -1069,7 +1071,7 @@ export const defaultTimeRangeOption = (): TimeRangeOption => ({
|
|||||||
to: "now",
|
to: "now",
|
||||||
});
|
});
|
||||||
|
|
||||||
export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind;
|
export type VariableKind = QueryVariableKind | TextVariableKind | ConstantVariableKind | DatasourceVariableKind | IntervalVariableKind | CustomVariableKind | GroupByVariableKind | AdhocVariableKind | SwitchVariableKind | StackVariableKind;
|
||||||
|
|
||||||
export const defaultVariableKind = (): VariableKind => (defaultQueryVariableKind());
|
export const defaultVariableKind = (): VariableKind => (defaultQueryVariableKind());
|
||||||
|
|
||||||
@@ -1335,6 +1337,7 @@ export interface CustomVariableSpec {
|
|||||||
skipUrlSync: boolean;
|
skipUrlSync: boolean;
|
||||||
description?: string;
|
description?: string;
|
||||||
allowCustomValue: boolean;
|
allowCustomValue: boolean;
|
||||||
|
valuesFormat?: "csv" | "json";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultCustomVariableSpec = (): CustomVariableSpec => ({
|
export const defaultCustomVariableSpec = (): CustomVariableSpec => ({
|
||||||
@@ -1492,6 +1495,31 @@ export const defaultSwitchVariableSpec = (): SwitchVariableSpec => ({
|
|||||||
skipUrlSync: false,
|
skipUrlSync: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export interface StackVariableKind {
|
||||||
|
kind: "StackVariable";
|
||||||
|
spec: StackVariableSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultStackVariableKind = (): StackVariableKind => ({
|
||||||
|
kind: "StackVariable",
|
||||||
|
spec: defaultStackVariableSpec(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface StackVariableSpec {
|
||||||
|
name: string;
|
||||||
|
defaultValue?: VariableOption;
|
||||||
|
current: VariableOption;
|
||||||
|
options: VariableValueOption[];
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultStackVariableSpec = (): StackVariableSpec => ({
|
||||||
|
name: "",
|
||||||
|
current: { text: "", value: "", },
|
||||||
|
options: [],
|
||||||
|
});
|
||||||
|
|
||||||
export interface Spec {
|
export interface Spec {
|
||||||
annotations: AnnotationQueryKind[];
|
annotations: AnnotationQueryKind[];
|
||||||
// Configuration of dashboard cursor sync behavior.
|
// Configuration of dashboard cursor sync behavior.
|
||||||
|
|||||||
@@ -150,6 +150,10 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
r.Get("/connections/datasources/edit/*", authorize(datasources.EditPageAccess), hs.Index)
|
r.Get("/connections/datasources/edit/*", authorize(datasources.EditPageAccess), hs.Index)
|
||||||
r.Get("/connections", authorize(datasources.ConfigurationPageAccess), hs.Index)
|
r.Get("/connections", authorize(datasources.ConfigurationPageAccess), hs.Index)
|
||||||
r.Get("/connections/add-new-connection", 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
|
// Plugin details pages
|
||||||
r.Get("/connections/datasources/:id", middleware.CanAdminPlugins(hs.Cfg, hs.AccessControl), hs.Index)
|
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)
|
r.Get("/connections/datasources/:id/page/:page", middleware.CanAdminPlugins(hs.Cfg, hs.AccessControl), hs.Index)
|
||||||
|
|||||||
1
pkg/kinds/dashboard/dashboard_spec_gen.go
generated
1
pkg/kinds/dashboard/dashboard_spec_gen.go
generated
@@ -874,6 +874,7 @@ const (
|
|||||||
VariableTypeSystem VariableType = "system"
|
VariableTypeSystem VariableType = "system"
|
||||||
VariableTypeSnapshot VariableType = "snapshot"
|
VariableTypeSnapshot VariableType = "snapshot"
|
||||||
VariableTypeSwitch VariableType = "switch"
|
VariableTypeSwitch VariableType = "switch"
|
||||||
|
VariableTypeStack VariableType = "stack"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Determine if the variable shows on dashboard
|
// Determine if the variable shows on dashboard
|
||||||
|
|||||||
88
pkg/registry/apis/collections/datasources_validator.go
Normal file
88
pkg/registry/apis/collections/datasources_validator.go
Normal file
@@ -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
|
||||||
|
|
||||||
|
}
|
||||||
212
pkg/registry/apis/collections/datasources_validator_test.go
Normal file
212
pkg/registry/apis/collections/datasources_validator_test.go
Normal file
@@ -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
|
package collections
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||||
@@ -14,13 +16,16 @@ import (
|
|||||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
|
|
||||||
collections "github.com/grafana/grafana/apps/collections/pkg/apis/collections/v1alpha1"
|
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"
|
grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic"
|
||||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||||
"github.com/grafana/grafana/pkg/infra/db"
|
"github.com/grafana/grafana/pkg/infra/db"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/collections/legacy"
|
"github.com/grafana/grafana/pkg/registry/apis/collections/legacy"
|
||||||
"github.com/grafana/grafana/pkg/registry/apis/preferences/utils"
|
"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/builder"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
"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/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/star"
|
"github.com/grafana/grafana/pkg/services/star"
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
@@ -29,13 +34,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ builder.APIGroupBuilder = (*APIBuilder)(nil)
|
_ builder.APIGroupBuilder = (*APIBuilder)(nil)
|
||||||
_ builder.APIGroupMutation = (*APIBuilder)(nil)
|
_ builder.APIGroupMutation = (*APIBuilder)(nil)
|
||||||
|
_ builder.APIGroupValidation = (*APIBuilder)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
type APIBuilder struct {
|
type APIBuilder struct {
|
||||||
authorizer authorizer.Authorizer
|
authorizer authorizer.Authorizer
|
||||||
legacyStars *legacy.DashboardStarsStorage
|
legacyStars *legacy.DashboardStarsStorage
|
||||||
|
datasourceStacksValidator builder.APIGroupValidation
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterAPIService(
|
func RegisterAPIService(
|
||||||
@@ -45,6 +52,8 @@ func RegisterAPIService(
|
|||||||
stars star.Service,
|
stars star.Service,
|
||||||
users user.Service,
|
users user.Service,
|
||||||
apiregistration builder.APIRegistrar,
|
apiregistration builder.APIRegistrar,
|
||||||
|
dsConnClientFactory datasourcesClient.DataSourceConnectionClientFactory,
|
||||||
|
restConfigProvider apiserver.RestConfigProvider,
|
||||||
) *APIBuilder {
|
) *APIBuilder {
|
||||||
// Requires development settings and clearly experimental
|
// Requires development settings and clearly experimental
|
||||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||||
@@ -52,11 +61,15 @@ func RegisterAPIService(
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dsConnClient := dsConnClientFactory(restConfigProvider)
|
||||||
|
|
||||||
sql := legacy.NewLegacySQL(legacysql.NewDatabaseProvider(db))
|
sql := legacy.NewLegacySQL(legacysql.NewDatabaseProvider(db))
|
||||||
builder := &APIBuilder{
|
builder := &APIBuilder{
|
||||||
|
datasourceStacksValidator: GetDatasourceStacksValidator(dsConnClient),
|
||||||
authorizer: &utils.AuthorizeFromName{
|
authorizer: &utils.AuthorizeFromName{
|
||||||
Resource: map[string][]utils.ResourceOwner{
|
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{}
|
storage := map[string]rest.Storage{}
|
||||||
|
|
||||||
// Configure Stars Dual writer
|
// Configure Stars Dual writer
|
||||||
resource := collections.StarsResourceInfo
|
starsResource := collections.StarsResourceInfo
|
||||||
var stars grafanarest.Storage
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stars = &starStorage{Storage: stars} // wrap List so we only return one value
|
stars = &starStorage{Storage: stars} // wrap List so we only return one value
|
||||||
if b.legacyStars != nil && opts.DualWriteBuilder != nil {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
storage[resource.StoragePath()] = stars
|
storage[starsResource.StoragePath()] = stars
|
||||||
storage[resource.StoragePath("update")] = &starsREST{store: 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
|
apiGroupInfo.VersionedResourcesStorageMap[collections.APIVersion] = storage
|
||||||
return nil
|
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 {
|
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 {
|
func (b *APIBuilder) GetOpenAPIDefinitions() common.GetOpenAPIDefinitions {
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func RegisterAPIService(
|
|||||||
accessControl,
|
accessControl,
|
||||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||||
features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes),
|
features.IsEnabledGlobally(featuremgmt.FlagDatasourceQueryTypes),
|
||||||
false,
|
true,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
queryV0 "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||||
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
|
gapiutil "github.com/grafana/grafana/pkg/services/apiserver/utils"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -28,11 +29,11 @@ var (
|
|||||||
// Get all datasource connections -- this will be backed by search or duplicated resource in unified storage
|
// Get all datasource connections -- this will be backed by search or duplicated resource in unified storage
|
||||||
type DataSourceConnectionProvider interface {
|
type DataSourceConnectionProvider interface {
|
||||||
// Get gets a specific datasource (that the user in context can see)
|
// 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)
|
GetConnection(ctx context.Context, namespace string, name string) (*queryV0.DataSourceConnection, error)
|
||||||
|
|
||||||
// List lists all data sources the user in context can see
|
// List lists all data sources the user in context can see. Optional field selectors can filter the results.
|
||||||
ListConnections(ctx context.Context, namespace string) (*queryV0.DataSourceConnectionList, error)
|
ListConnections(ctx context.Context, namespace string, fieldSelector fields.Selector) (*queryV0.DataSourceConnectionList, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type connectionAccess struct {
|
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) {
|
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 {
|
type connectionsProvider struct {
|
||||||
@@ -103,19 +108,47 @@ func (q *connectionsProvider) GetConnection(ctx context.Context, namespace strin
|
|||||||
return q.asConnection(ds, namespace)
|
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)
|
ns, err := authlib.ParseNamespace(namespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dss, err := q.dsService.GetDataSources(ctx, &datasources.GetDataSourcesQuery{
|
var dss []*datasources.DataSource
|
||||||
OrgID: ns.OrgID,
|
// if fieldSelector is not nil, find any uids in the metadata.name field and
|
||||||
DataSourceLimit: 10000,
|
// use them in the query
|
||||||
})
|
if fieldSelector != nil && !fieldSelector.Empty() {
|
||||||
if err != nil {
|
uids := []string{}
|
||||||
return nil, err
|
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{
|
result := &queryV0.DataSourceConnectionList{
|
||||||
Items: []queryV0.DataSourceConnection{},
|
Items: []queryV0.DataSourceConnection{},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
"github.com/grafana/grafana/pkg/services/datasourceproxy"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
|
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/dsquerierclient"
|
||||||
"github.com/grafana/grafana/pkg/services/encryption"
|
"github.com/grafana/grafana/pkg/services/encryption"
|
||||||
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
|
encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service"
|
||||||
@@ -476,6 +477,7 @@ var wireBasicSet = wire.NewSet(
|
|||||||
appregistry.WireSet,
|
appregistry.WireSet,
|
||||||
// Dashboard Kubernetes helpers
|
// Dashboard Kubernetes helpers
|
||||||
dashboardclient.ProvideK8sClientWithFallback,
|
dashboardclient.ProvideK8sClientWithFallback,
|
||||||
|
datasourcesclient.ProvideDataSourceConnectionClientFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
var wireSet = wire.NewSet(
|
var wireSet = wire.NewSet(
|
||||||
|
|||||||
9
pkg/server/wire_gen.go
generated
9
pkg/server/wire_gen.go
generated
File diff suppressed because one or more lines are too long
147
pkg/services/datasources/service/client/client.go
Normal file
147
pkg/services/datasources/service/client/client.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
96
pkg/services/datasources/service/client/client_mock.go
Normal file
96
pkg/services/datasources/service/client/client_mock.go
Normal file
@@ -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",
|
Url: baseUrl + "/datasources",
|
||||||
Children: []*navtree.NavLink{},
|
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 {
|
if len(children) > 0 {
|
||||||
|
|||||||
@@ -182,6 +182,8 @@ export function getNavTitle(navId: string | undefined) {
|
|||||||
return t('nav.connections.title', 'Connections');
|
return t('nav.connections.title', 'Connections');
|
||||||
case 'connections-add-new-connection':
|
case 'connections-add-new-connection':
|
||||||
return t('nav.add-new-connections.title', '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':
|
case 'standalone-plugin-page-/connections/collector':
|
||||||
return t('nav.collector.title', 'Collector');
|
return t('nav.collector.title', 'Collector');
|
||||||
case 'connections-datasources':
|
case 'connections-datasources':
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ import { CacheFeatureHighlightPage } from './pages/CacheFeatureHighlightPage';
|
|||||||
import ConnectionsHomePage from './pages/ConnectionsHomePage';
|
import ConnectionsHomePage from './pages/ConnectionsHomePage';
|
||||||
import { DataSourceDashboardsPage } from './pages/DataSourceDashboardsPage';
|
import { DataSourceDashboardsPage } from './pages/DataSourceDashboardsPage';
|
||||||
import { DataSourceDetailsPage } from './pages/DataSourceDetailsPage';
|
import { DataSourceDetailsPage } from './pages/DataSourceDetailsPage';
|
||||||
|
import { DataSourceStacksPage } from './pages/DataSourceStacksPage';
|
||||||
import { DataSourcesListPage } from './pages/DataSourcesListPage';
|
import { DataSourcesListPage } from './pages/DataSourcesListPage';
|
||||||
import { EditDataSourcePage } from './pages/EditDataSourcePage';
|
import { EditDataSourcePage } from './pages/EditDataSourcePage';
|
||||||
|
import { EditStackPage } from './pages/EditStackPage';
|
||||||
import { InsightsFeatureHighlightPage } from './pages/InsightsFeatureHighlightPage';
|
import { InsightsFeatureHighlightPage } from './pages/InsightsFeatureHighlightPage';
|
||||||
import { NewDataSourcePage } from './pages/NewDataSourcePage';
|
import { NewDataSourcePage } from './pages/NewDataSourcePage';
|
||||||
|
import { NewStackPage } from './pages/NewStackPage';
|
||||||
import { PermissionsFeatureHighlightPage } from './pages/PermissionsFeatureHighlightPage';
|
import { PermissionsFeatureHighlightPage } from './pages/PermissionsFeatureHighlightPage';
|
||||||
|
|
||||||
function RedirectToAddNewConnection() {
|
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 */}
|
{/* 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.DataSources.replace(ROUTES.Base, '')} element={<DataSourcesListPage />} />
|
||||||
<Route caseSensitive path={ROUTES.DataSourcesNew.replace(ROUTES.Base, '')} element={<NewDataSourcePage />} />
|
<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
|
<Route
|
||||||
caseSensitive
|
caseSensitive
|
||||||
path={ROUTES.DataSourcesDetails.replace(ROUTES.Base, '')}
|
path={ROUTES.DataSourcesDetails.replace(ROUTES.Base, '')}
|
||||||
|
|||||||
@@ -75,5 +75,11 @@ export function getOssCardData(): CardData[] {
|
|||||||
url: '/connections/datasources',
|
url: '/connections/datasources',
|
||||||
icon: 'database',
|
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`,
|
DataSourcesNew: `/${ROUTE_BASE_ID}/datasources/new`,
|
||||||
DataSourcesEdit: `/${ROUTE_BASE_ID}/datasources/edit/:uid`,
|
DataSourcesEdit: `/${ROUTE_BASE_ID}/datasources/edit/:uid`,
|
||||||
DataSourcesDashboards: `/${ROUTE_BASE_ID}/datasources/edit/:uid/dashboards`,
|
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
|
// Add new connection
|
||||||
AddNewConnection: `/${ROUTE_BASE_ID}/add-new-connection`,
|
AddNewConnection: `/${ROUTE_BASE_ID}/add-new-connection`,
|
||||||
|
|||||||
226
public/app/features/connections/pages/DataSourceStacksPage.tsx
Normal file
226
public/app/features/connections/pages/DataSourceStacksPage.tsx
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
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 { Resource } from 'app/features/apiserver/types';
|
||||||
|
import {
|
||||||
|
DataSourceStackSpec,
|
||||||
|
fetchStacks as fetchStacksApi,
|
||||||
|
deleteStack,
|
||||||
|
} from 'app/features/datasources/api/stacksApi';
|
||||||
|
|
||||||
|
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 items = await fetchStacksApi();
|
||||||
|
setStacks(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 deleteStack(stackName);
|
||||||
|
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);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
96
public/app/features/connections/pages/EditStackPage.tsx
Normal file
96
public/app/features/connections/pages/EditStackPage.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
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 { Resource } from 'app/features/apiserver/types';
|
||||||
|
import { DataSourceStackSpec, fetchStack } from 'app/features/datasources/api/stacksApi';
|
||||||
|
import {
|
||||||
|
StackForm,
|
||||||
|
transformStackSpecToFormValues,
|
||||||
|
} from 'app/features/datasources/components/new-stack-form/StackForm';
|
||||||
|
import { StackFormValues } from 'app/features/datasources/components/new-stack-form/types';
|
||||||
|
|
||||||
|
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 loadStack = async () => {
|
||||||
|
if (!uid) {
|
||||||
|
setError('No stack UID provided');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetchStack(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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadStack();
|
||||||
|
}, [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} />;
|
||||||
|
}
|
||||||
19
public/app/features/connections/pages/NewStackPage.tsx
Normal file
19
public/app/features/connections/pages/NewStackPage.tsx
Normal file
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -849,6 +849,7 @@ describe('DashboardSceneSerializer', () => {
|
|||||||
query: 'app1',
|
query: 'app1',
|
||||||
skipUrlSync: false,
|
skipUrlSync: false,
|
||||||
allowCustomValue: true,
|
allowCustomValue: true,
|
||||||
|
valuesFormat: 'csv',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ exports[`Given a scene with custom quick ranges should save quick ranges to save
|
|||||||
"options": [],
|
"options": [],
|
||||||
"query": "a, b, c",
|
"query": "a, b, c",
|
||||||
"type": "custom",
|
"type": "custom",
|
||||||
|
"valuesFormat": "csv",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current": {
|
"current": {
|
||||||
@@ -679,6 +680,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
|
|||||||
"options": [],
|
"options": [],
|
||||||
"query": "A,B,C,D,E,F,E,G,H,I,J,K,L",
|
"query": "A,B,C,D,E,F,E,G,H,I,J,K,L",
|
||||||
"type": "custom",
|
"type": "custom",
|
||||||
|
"valuesFormat": "csv",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current": {
|
"current": {
|
||||||
@@ -697,6 +699,7 @@ exports[`transformSceneToSaveModel Given a scene with rows Should transform back
|
|||||||
"options": [],
|
"options": [],
|
||||||
"query": "Bob : 1, Rob : 2,Sod : 3, Hod : 4, Cod : 5",
|
"query": "Bob : 1, Rob : 2,Sod : 3, Hod : 4, Cod : 5",
|
||||||
"type": "custom",
|
"type": "custom",
|
||||||
|
"valuesFormat": "csv",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -1019,6 +1022,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho
|
|||||||
"options": [],
|
"options": [],
|
||||||
"query": "a, b, c",
|
"query": "a, b, c",
|
||||||
"type": "custom",
|
"type": "custom",
|
||||||
|
"valuesFormat": "csv",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current": {
|
"current": {
|
||||||
@@ -1378,6 +1382,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr
|
|||||||
"options": [],
|
"options": [],
|
||||||
"query": "a, b, c",
|
"query": "a, b, c",
|
||||||
"type": "custom",
|
"type": "custom",
|
||||||
|
"valuesFormat": "csv",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current": {
|
"current": {
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ exports[`transformSceneToSaveModelSchemaV2 should transform scene to save model
|
|||||||
"options": [],
|
"options": [],
|
||||||
"query": "option1, option2",
|
"query": "option1, option2",
|
||||||
"skipUrlSync": false,
|
"skipUrlSync": false,
|
||||||
|
"valuesFormat": "csv",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ describe('sceneVariablesSetToVariables', () => {
|
|||||||
"options": [],
|
"options": [],
|
||||||
"query": "test,test1,test2",
|
"query": "test,test1,test2",
|
||||||
"type": "custom",
|
"type": "custom",
|
||||||
|
"valuesFormat": "csv",
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -1148,6 +1149,7 @@ describe('sceneVariablesSetToVariables', () => {
|
|||||||
"options": [],
|
"options": [],
|
||||||
"query": "test,test1,test2",
|
"query": "test,test1,test2",
|
||||||
"skipUrlSync": false,
|
"skipUrlSync": false,
|
||||||
|
"valuesFormat": "csv",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { isStack } from 'immutable';
|
||||||
|
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
AdHocFilterWithLabels as SceneAdHocFilterWithLabels,
|
AdHocFilterWithLabels as SceneAdHocFilterWithLabels,
|
||||||
@@ -28,9 +30,11 @@ import {
|
|||||||
AdHocFilterWithLabels,
|
AdHocFilterWithLabels,
|
||||||
SwitchVariableKind,
|
SwitchVariableKind,
|
||||||
defaultIntervalVariableSpec,
|
defaultIntervalVariableSpec,
|
||||||
|
StackVariableKind,
|
||||||
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
} from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||||
import { getDefaultDatasource } from 'app/features/dashboard/api/ResponseTransformers';
|
import { getDefaultDatasource } from 'app/features/dashboard/api/ResponseTransformers';
|
||||||
|
|
||||||
|
import { isStackVariable, StackVariable } from '../settings/variables/StackVariable';
|
||||||
import { getIntervalsQueryFromNewIntervalModel } from '../utils/utils';
|
import { getIntervalsQueryFromNewIntervalModel } from '../utils/utils';
|
||||||
|
|
||||||
import { DSReferencesMapping } from './DashboardSceneSerializer';
|
import { DSReferencesMapping } from './DashboardSceneSerializer';
|
||||||
@@ -110,6 +114,7 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
|
|||||||
allValue: variable.state.allValue,
|
allValue: variable.state.allValue,
|
||||||
includeAll: variable.state.includeAll,
|
includeAll: variable.state.includeAll,
|
||||||
allowCustomValue: variable.state.allowCustomValue,
|
allowCustomValue: variable.state.allowCustomValue,
|
||||||
|
valuesFormat: variable.state.valuesFormat,
|
||||||
});
|
});
|
||||||
} else if (sceneUtils.isDataSourceVariable(variable)) {
|
} else if (sceneUtils.isDataSourceVariable(variable)) {
|
||||||
variables.push({
|
variables.push({
|
||||||
@@ -230,6 +235,16 @@ export function sceneVariablesSetToVariables(set: SceneVariables, keepQueryOptio
|
|||||||
});
|
});
|
||||||
} else if (variable.state.type === 'system') {
|
} else if (variable.state.type === 'system') {
|
||||||
// Not persisted
|
// Not persisted
|
||||||
|
} else if (isStackVariable(variable)) {
|
||||||
|
variables.push({
|
||||||
|
...commonProperties,
|
||||||
|
current: {
|
||||||
|
text: variable.state.text || variable.state.value,
|
||||||
|
value: variable.state.value,
|
||||||
|
},
|
||||||
|
properties: variable.state.properties || {},
|
||||||
|
options: [],
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Unsupported variable type');
|
throw new Error('Unsupported variable type');
|
||||||
}
|
}
|
||||||
@@ -283,6 +298,7 @@ export function sceneVariablesSetToSchemaV2Variables(
|
|||||||
| GroupByVariableKind
|
| GroupByVariableKind
|
||||||
| AdhocVariableKind
|
| AdhocVariableKind
|
||||||
| SwitchVariableKind
|
| SwitchVariableKind
|
||||||
|
| StackVariableKind
|
||||||
> {
|
> {
|
||||||
let variables: Array<
|
let variables: Array<
|
||||||
| QueryVariableKind
|
| QueryVariableKind
|
||||||
@@ -294,6 +310,7 @@ export function sceneVariablesSetToSchemaV2Variables(
|
|||||||
| GroupByVariableKind
|
| GroupByVariableKind
|
||||||
| AdhocVariableKind
|
| AdhocVariableKind
|
||||||
| SwitchVariableKind
|
| SwitchVariableKind
|
||||||
|
| StackVariableKind
|
||||||
> = [];
|
> = [];
|
||||||
|
|
||||||
for (const variable of set.state.variables) {
|
for (const variable of set.state.variables) {
|
||||||
@@ -392,6 +409,7 @@ export function sceneVariablesSetToSchemaV2Variables(
|
|||||||
allValue: variable.state.allValue,
|
allValue: variable.state.allValue,
|
||||||
includeAll: variable.state.includeAll ?? false,
|
includeAll: variable.state.includeAll ?? false,
|
||||||
allowCustomValue: variable.state.allowCustomValue ?? true,
|
allowCustomValue: variable.state.allowCustomValue ?? true,
|
||||||
|
valuesFormat: variable.state.valuesFormat,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
variables.push(customVariable);
|
variables.push(customVariable);
|
||||||
@@ -556,6 +574,20 @@ export function sceneVariablesSetToSchemaV2Variables(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
variables.push(switchVariable);
|
variables.push(switchVariable);
|
||||||
|
} else if (isStackVariable(variable)) {
|
||||||
|
const stackVariable: StackVariableKind = {
|
||||||
|
kind: 'StackVariable',
|
||||||
|
spec: {
|
||||||
|
...commonProperties,
|
||||||
|
options: [],
|
||||||
|
current: {
|
||||||
|
text: variable.state.text || variable.state.value,
|
||||||
|
value: variable.state.value,
|
||||||
|
},
|
||||||
|
properties: variable.state.properties || {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
variables.push(stackVariable);
|
||||||
} else if (variable.state.type === 'system') {
|
} else if (variable.state.type === 'system') {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import { registerDashboardMacro } from '../scene/DashboardMacro';
|
|||||||
import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior';
|
import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior';
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
|
import { DashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
|
||||||
|
import { StackVariable } from '../settings/variables/StackVariable';
|
||||||
import { getIntervalsFromQueryString } from '../utils/utils';
|
import { getIntervalsFromQueryString } from '../utils/utils';
|
||||||
|
|
||||||
import { transformV2ToV1AnnotationQuery } from './annotations';
|
import { transformV2ToV1AnnotationQuery } from './annotations';
|
||||||
@@ -335,12 +336,12 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (variable.kind === defaultCustomVariableKind().kind) {
|
if (variable.kind === defaultCustomVariableKind().kind) {
|
||||||
return new CustomVariable({
|
return new CustomVariable({
|
||||||
...commonProperties,
|
...commonProperties,
|
||||||
value: variable.spec.current?.value ?? '',
|
value: variable.spec.current?.value ?? '',
|
||||||
text: variable.spec.current?.text ?? '',
|
text: variable.spec.current?.text ?? '',
|
||||||
|
|
||||||
query: variable.spec.query,
|
query: variable.spec.query,
|
||||||
isMulti: variable.spec.multi,
|
isMulti: variable.spec.multi,
|
||||||
allValue: variable.spec.allValue || undefined,
|
allValue: variable.spec.allValue || undefined,
|
||||||
@@ -348,6 +349,16 @@ function createSceneVariableFromVariableModel(variable: TypedVariableModelV2): S
|
|||||||
defaultToAll: Boolean(variable.spec.includeAll),
|
defaultToAll: Boolean(variable.spec.includeAll),
|
||||||
skipUrlSync: variable.spec.skipUrlSync,
|
skipUrlSync: variable.spec.skipUrlSync,
|
||||||
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||||
|
valuesFormat: variable.spec.valuesFormat || 'csv',
|
||||||
|
});
|
||||||
|
} else if (variable.kind === defaultStackVariableKind().kind) {
|
||||||
|
return new StackVariable({
|
||||||
|
...commonProperties,
|
||||||
|
value: variable.spec.current?.value ?? '',
|
||||||
|
text: variable.spec.current?.text ?? '',
|
||||||
|
properties: variable.spec.properties || {},
|
||||||
|
skipUrlSync: variable.spec.skipUrlSync,
|
||||||
|
hide: transformVariableHideToEnumV1(variable.spec.hide),
|
||||||
});
|
});
|
||||||
} else if (variable.kind === defaultQueryVariableKind().kind) {
|
} else if (variable.kind === defaultQueryVariableKind().kind) {
|
||||||
return new QueryVariable({
|
return new QueryVariable({
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
import { t } from '@grafana/i18n';
|
||||||
|
import {
|
||||||
|
MultiOrSingleValueSelect,
|
||||||
|
MultiValueVariable,
|
||||||
|
MultiValueVariableState,
|
||||||
|
SceneComponentProps,
|
||||||
|
SceneVariable,
|
||||||
|
VariableDependencyConfig,
|
||||||
|
VariableGetOptionsArgs,
|
||||||
|
VariableValue,
|
||||||
|
VariableValueOption,
|
||||||
|
} from '@grafana/scenes';
|
||||||
|
import { Resource } from 'app/features/apiserver/types';
|
||||||
|
import {
|
||||||
|
DataSourceStackModeSpec,
|
||||||
|
DataSourceStackSpec,
|
||||||
|
fetchStacks,
|
||||||
|
} from 'app/features/datasources/api/stacksApi';
|
||||||
|
|
||||||
|
export type StackVariableValue = Record<string, string>;
|
||||||
|
|
||||||
|
export interface StackVariableState extends MultiValueVariableState {
|
||||||
|
type: 'stack';
|
||||||
|
value: string; // mode value
|
||||||
|
|
||||||
|
label: string; // mode name (stg, prod etc.)
|
||||||
|
selectedStack: Resource<DataSourceStackSpec> | null;
|
||||||
|
stackList: Array<Resource<DataSourceStackSpec>>;
|
||||||
|
properties: StackVariableValue; // properties for the current selected mode
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StackVariable extends MultiValueVariable<StackVariableState> {
|
||||||
|
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||||
|
statePaths: ['value'], //stackList?
|
||||||
|
});
|
||||||
|
|
||||||
|
public constructor(initialState: Partial<StackVariableState>) {
|
||||||
|
super({
|
||||||
|
type: 'stack',
|
||||||
|
label: t('variables.stack.label', 'Stack'),
|
||||||
|
selectedStack: null,
|
||||||
|
name: 'stack',
|
||||||
|
value: '',
|
||||||
|
stackList: [],
|
||||||
|
text: '',
|
||||||
|
options: [],
|
||||||
|
properties: {},
|
||||||
|
...initialState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all available stacks from the API
|
||||||
|
*/
|
||||||
|
public async loadStackList(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.setState({ loading: true, error: null });
|
||||||
|
const stackList = await fetchStacks();
|
||||||
|
this.setState({ stackList, loading: false });
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to load stacks';
|
||||||
|
this.setState({ error: message, loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected stack by name and fetches its details
|
||||||
|
*/
|
||||||
|
public async setStack(stackName: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.setState({ loading: true, error: null });
|
||||||
|
const selectedStack = this.state.stackList.find((stack) => stack.metadata.name === stackName);
|
||||||
|
// Clear current value and properties when changing stack
|
||||||
|
this.setState({
|
||||||
|
selectedStack,
|
||||||
|
value: '',
|
||||||
|
properties: {},
|
||||||
|
loading: false,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : 'Failed to load stack';
|
||||||
|
this.setState({ error: message, loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public changeValueTo(value: VariableValue, text?: VariableValue, isUserAction?: boolean): void {
|
||||||
|
this.setValue(String(value));
|
||||||
|
super.changeValueTo(value, text, isUserAction);
|
||||||
|
}
|
||||||
|
public setValue(value: string): void {
|
||||||
|
const stack = this.state.selectedStack;
|
||||||
|
let properties: StackVariableValue = {};
|
||||||
|
|
||||||
|
if (stack) {
|
||||||
|
const selectedMode = stack.spec.modes.find((mode) => mode.name === value);
|
||||||
|
if (selectedMode) {
|
||||||
|
properties = getOptionValuesFromMode(stack.spec, selectedMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ value, properties });
|
||||||
|
}
|
||||||
|
|
||||||
|
public getValueOptions(args: VariableGetOptionsArgs): Observable<VariableValueOption[]> {
|
||||||
|
if (!this.state.selectedStack) {
|
||||||
|
return of([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const spec = this.state.selectedStack.spec;
|
||||||
|
const options = spec.modes.map((mode) => ({
|
||||||
|
label: mode.name,
|
||||||
|
text: mode.name,
|
||||||
|
value: mode.name,
|
||||||
|
properties: getOptionValuesFromMode(spec, mode),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return of(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component = ({ model }: SceneComponentProps<MultiValueVariable>) => {
|
||||||
|
return <MultiOrSingleValueSelect model={model} />;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOptionValuesFromMode = (spec: DataSourceStackSpec, mode: DataSourceStackModeSpec): StackVariableValue => {
|
||||||
|
return Object.keys(spec.template).reduce<StackVariableValue>((optionValue, templateId) => {
|
||||||
|
const templateName = spec.template[templateId].name;
|
||||||
|
const modeDefinition = mode.definition[templateId];
|
||||||
|
if (modeDefinition) {
|
||||||
|
optionValue[templateName] = modeDefinition.dataSourceRef;
|
||||||
|
}
|
||||||
|
return optionValue;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|
||||||
|
// in scenes
|
||||||
|
export function isStackVariable(variable: SceneVariable): variable is StackVariable {
|
||||||
|
return variable.state.type === 'stack';
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import { Trans, t } from '@grafana/i18n';
|
|||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { SceneVariable } from '@grafana/scenes';
|
import { SceneVariable } from '@grafana/scenes';
|
||||||
import { VariableHide, defaultVariableModel } from '@grafana/schema';
|
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 { VariableHideSelect } from 'app/features/dashboard-scene/settings/variables/components/VariableHideSelect';
|
||||||
import { VariableLegend } from 'app/features/dashboard-scene/settings/variables/components/VariableLegend';
|
import { VariableLegend } from 'app/features/dashboard-scene/settings/variables/components/VariableLegend';
|
||||||
import { VariableTextAreaField } from 'app/features/dashboard-scene/settings/variables/components/VariableTextAreaField';
|
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 onHideChange = (hide: VariableHide) => variable.setState({ hide });
|
||||||
|
|
||||||
const isHasVariableOptions = hasVariableOptions(variable);
|
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) => () => {
|
const onDeleteVariable = (hideModal: () => void) => () => {
|
||||||
reportInteraction('Delete variable');
|
reportInteraction('Delete variable');
|
||||||
@@ -123,7 +126,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
|
|||||||
|
|
||||||
{EditorToRender && <EditorToRender variable={variable} onRunQuery={onRunQuery} />}
|
{EditorToRender && <EditorToRender variable={variable} onRunQuery={onRunQuery} />}
|
||||||
|
|
||||||
{isHasVariableOptions && <VariableValuesPreview options={variable.getOptionsForSelect(false)} />}
|
{isHasVariableOptions && <VariableValuesPreview options={optionsForSelect} hasMultiProps={hasMultiProps} />}
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<Stack gap={2}>
|
<Stack gap={2}>
|
||||||
|
|||||||
@@ -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';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
|
||||||
@@ -130,4 +131,71 @@ describe('CustomVariableForm', () => {
|
|||||||
expect(onMultiChange).not.toHaveBeenCalled();
|
expect(onMultiChange).not.toHaveBeenCalled();
|
||||||
expect(onIncludeAllChange).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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { FormEvent } from 'react';
|
import { FormEvent } from 'react';
|
||||||
|
|
||||||
|
import { CustomVariableModel } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { Trans, t } from '@grafana/i18n';
|
import { Trans, t } from '@grafana/i18n';
|
||||||
|
import { FieldValidationMessage, Icon, RadioButtonGroup, Stack, TextLink, Tooltip } from '@grafana/ui';
|
||||||
|
|
||||||
import { SelectionOptionsForm } from './SelectionOptionsForm';
|
import { SelectionOptionsForm } from './SelectionOptionsForm';
|
||||||
import { VariableLegend } from './VariableLegend';
|
import { VariableLegend } from './VariableLegend';
|
||||||
@@ -9,10 +11,12 @@ import { VariableTextAreaField } from './VariableTextAreaField';
|
|||||||
|
|
||||||
interface CustomVariableFormProps {
|
interface CustomVariableFormProps {
|
||||||
query: string;
|
query: string;
|
||||||
|
valuesFormat?: CustomVariableModel['valuesFormat'];
|
||||||
multi: boolean;
|
multi: boolean;
|
||||||
allValue?: string | null;
|
allValue?: string | null;
|
||||||
includeAll: boolean;
|
includeAll: boolean;
|
||||||
allowCustomValue?: boolean;
|
allowCustomValue?: boolean;
|
||||||
|
queryValidationError?: Error;
|
||||||
onQueryChange: (event: FormEvent<HTMLTextAreaElement>) => void;
|
onQueryChange: (event: FormEvent<HTMLTextAreaElement>) => void;
|
||||||
onMultiChange: (event: FormEvent<HTMLInputElement>) => void;
|
onMultiChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
|
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
@@ -20,19 +24,23 @@ interface CustomVariableFormProps {
|
|||||||
onQueryBlur?: (event: FormEvent<HTMLTextAreaElement>) => void;
|
onQueryBlur?: (event: FormEvent<HTMLTextAreaElement>) => void;
|
||||||
onAllValueBlur?: (event: FormEvent<HTMLInputElement>) => void;
|
onAllValueBlur?: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
onAllowCustomValueChange?: (event: FormEvent<HTMLInputElement>) => void;
|
onAllowCustomValueChange?: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
|
onValuesFormatChange?: (format: CustomVariableModel['valuesFormat']) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CustomVariableForm({
|
export function CustomVariableForm({
|
||||||
query,
|
query,
|
||||||
|
valuesFormat,
|
||||||
multi,
|
multi,
|
||||||
allValue,
|
allValue,
|
||||||
includeAll,
|
includeAll,
|
||||||
allowCustomValue,
|
allowCustomValue,
|
||||||
|
queryValidationError,
|
||||||
onQueryChange,
|
onQueryChange,
|
||||||
onMultiChange,
|
onMultiChange,
|
||||||
onIncludeAllChange,
|
onIncludeAllChange,
|
||||||
onAllValueChange,
|
onAllValueChange,
|
||||||
onAllowCustomValueChange,
|
onAllowCustomValueChange,
|
||||||
|
onValuesFormatChange,
|
||||||
}: CustomVariableFormProps) {
|
}: CustomVariableFormProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -40,16 +48,27 @@ export function CustomVariableForm({
|
|||||||
<Trans i18nKey="dashboard-scene.custom-variable-form.custom-options">Custom options</Trans>
|
<Trans i18nKey="dashboard-scene.custom-variable-form.custom-options">Custom options</Trans>
|
||||||
</VariableLegend>
|
</VariableLegend>
|
||||||
|
|
||||||
|
<ValuesFormatSelector valuesFormat={valuesFormat} onValuesFormatChange={onValuesFormatChange} />
|
||||||
|
|
||||||
<VariableTextAreaField
|
<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}
|
defaultValue={query}
|
||||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
|
||||||
placeholder="1, 10, mykey : myvalue, myvalue, escaped\,value"
|
|
||||||
onBlur={onQueryChange}
|
onBlur={onQueryChange}
|
||||||
required
|
required
|
||||||
width={52}
|
width={52}
|
||||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput}
|
testId={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput}
|
||||||
/>
|
/>
|
||||||
|
{queryValidationError && <FieldValidationMessage>{queryValidationError.message}</FieldValidationMessage>}
|
||||||
|
|
||||||
<VariableLegend>
|
<VariableLegend>
|
||||||
<Trans i18nKey="dashboard-scene.custom-variable-form.selection-options">Selection options</Trans>
|
<Trans i18nKey="dashboard-scene.custom-variable-form.selection-options">Selection options</Trans>
|
||||||
</VariableLegend>
|
</VariableLegend>
|
||||||
@@ -58,6 +77,8 @@ export function CustomVariableForm({
|
|||||||
includeAll={includeAll}
|
includeAll={includeAll}
|
||||||
allValue={allValue}
|
allValue={allValue}
|
||||||
allowCustomValue={allowCustomValue}
|
allowCustomValue={allowCustomValue}
|
||||||
|
disableAllowCustomValue={valuesFormat === 'json'}
|
||||||
|
disableCustomAllValue={valuesFormat === 'json'}
|
||||||
onMultiChange={onMultiChange}
|
onMultiChange={onMultiChange}
|
||||||
onIncludeAllChange={onIncludeAllChange}
|
onIncludeAllChange={onIncludeAllChange}
|
||||||
onAllValueChange={onAllValueChange}
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Trans, t } from '@grafana/i18n';
|
|||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { QueryVariable } from '@grafana/scenes';
|
import { QueryVariable } from '@grafana/scenes';
|
||||||
import { DataSourceRef, VariableRefresh, VariableSort } from '@grafana/schema';
|
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 { QueryEditor } from 'app/features/dashboard-scene/settings/variables/components/QueryEditor';
|
||||||
import { SelectionOptionsForm } from 'app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm';
|
import { SelectionOptionsForm } from 'app/features/dashboard-scene/settings/variables/components/SelectionOptionsForm';
|
||||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
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 { QueryVariableRefreshSelect } from 'app/features/variables/query/QueryVariableRefreshSelect';
|
||||||
import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect';
|
import { QueryVariableSortSelect } from 'app/features/variables/query/QueryVariableSortSelect';
|
||||||
import {
|
import {
|
||||||
|
QueryVariableStaticOptions,
|
||||||
StaticOptionsOrderType,
|
StaticOptionsOrderType,
|
||||||
StaticOptionsType,
|
StaticOptionsType,
|
||||||
QueryVariableStaticOptions,
|
|
||||||
} from 'app/features/variables/query/QueryVariableStaticOptions';
|
} from 'app/features/variables/query/QueryVariableStaticOptions';
|
||||||
|
|
||||||
import { VariableLegend } from './VariableLegend';
|
import { VariableLegend } from './VariableLegend';
|
||||||
@@ -34,6 +34,7 @@ interface QueryVariableEditorFormProps {
|
|||||||
timeRange: TimeRange;
|
timeRange: TimeRange;
|
||||||
regex: string | null;
|
regex: string | null;
|
||||||
onRegExChange: (event: FormEvent<HTMLTextAreaElement>) => void;
|
onRegExChange: (event: FormEvent<HTMLTextAreaElement>) => void;
|
||||||
|
disableRegexEdition?: boolean;
|
||||||
sort: VariableSort;
|
sort: VariableSort;
|
||||||
onSortChange: (option: SelectableValue<VariableSort>) => void;
|
onSortChange: (option: SelectableValue<VariableSort>) => void;
|
||||||
refresh: VariableRefresh;
|
refresh: VariableRefresh;
|
||||||
@@ -42,14 +43,17 @@ interface QueryVariableEditorFormProps {
|
|||||||
onMultiChange: (event: FormEvent<HTMLInputElement>) => void;
|
onMultiChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
allowCustomValue?: boolean;
|
allowCustomValue?: boolean;
|
||||||
onAllowCustomValueChange?: (event: FormEvent<HTMLInputElement>) => void;
|
onAllowCustomValueChange?: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
|
disableAllowCustomValue?: boolean;
|
||||||
includeAll: boolean;
|
includeAll: boolean;
|
||||||
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
|
onIncludeAllChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
allValue: string;
|
allValue: string;
|
||||||
onAllValueChange: (event: FormEvent<HTMLInputElement>) => void;
|
onAllValueChange: (event: FormEvent<HTMLInputElement>) => void;
|
||||||
|
disableCustomAllValue?: boolean;
|
||||||
staticOptions?: StaticOptionsType;
|
staticOptions?: StaticOptionsType;
|
||||||
staticOptionsOrder?: StaticOptionsOrderType;
|
staticOptionsOrder?: StaticOptionsOrderType;
|
||||||
onStaticOptionsChange?: (staticOptions: StaticOptionsType) => void;
|
onStaticOptionsChange?: (staticOptions: StaticOptionsType) => void;
|
||||||
onStaticOptionsOrderChange?: (staticOptionsOrder: StaticOptionsOrderType) => void;
|
onStaticOptionsOrderChange?: (staticOptionsOrder: StaticOptionsOrderType) => void;
|
||||||
|
disableStaticOptions?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QueryVariableEditorForm({
|
export function QueryVariableEditorForm({
|
||||||
@@ -61,6 +65,7 @@ export function QueryVariableEditorForm({
|
|||||||
timeRange,
|
timeRange,
|
||||||
regex,
|
regex,
|
||||||
onRegExChange,
|
onRegExChange,
|
||||||
|
disableRegexEdition,
|
||||||
sort,
|
sort,
|
||||||
onSortChange,
|
onSortChange,
|
||||||
refresh,
|
refresh,
|
||||||
@@ -69,14 +74,17 @@ export function QueryVariableEditorForm({
|
|||||||
onMultiChange,
|
onMultiChange,
|
||||||
allowCustomValue,
|
allowCustomValue,
|
||||||
onAllowCustomValueChange,
|
onAllowCustomValueChange,
|
||||||
|
disableAllowCustomValue,
|
||||||
includeAll,
|
includeAll,
|
||||||
onIncludeAllChange,
|
onIncludeAllChange,
|
||||||
allValue,
|
allValue,
|
||||||
onAllValueChange,
|
onAllValueChange,
|
||||||
|
disableCustomAllValue,
|
||||||
staticOptions,
|
staticOptions,
|
||||||
staticOptionsOrder,
|
staticOptionsOrder,
|
||||||
onStaticOptionsChange,
|
onStaticOptionsChange,
|
||||||
onStaticOptionsOrderChange,
|
onStaticOptionsOrderChange,
|
||||||
|
disableStaticOptions,
|
||||||
}: QueryVariableEditorFormProps) {
|
}: QueryVariableEditorFormProps) {
|
||||||
const { value: dsConfig } = useAsync(async () => {
|
const { value: dsConfig } = useAsync(async () => {
|
||||||
const datasource = await getDataSourceSrv().get(datasourceRef ?? '');
|
const datasource = await getDataSourceSrv().get(datasourceRef ?? '');
|
||||||
@@ -116,48 +124,53 @@ export function QueryVariableEditorForm({
|
|||||||
<Field
|
<Field
|
||||||
label={t('dashboard-scene.query-variable-editor-form.label-data-source', 'Data source')}
|
label={t('dashboard-scene.query-variable-editor-form.label-data-source', 'Data source')}
|
||||||
htmlFor="data-source-picker"
|
htmlFor="data-source-picker"
|
||||||
|
noMargin
|
||||||
>
|
>
|
||||||
<DataSourcePicker current={datasourceRef} onChange={datasourceChangeHandler} variables={true} width={30} />
|
<DataSourcePicker current={datasourceRef} onChange={datasourceChangeHandler} variables={true} width={30} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{datasource && VariableQueryEditor && (
|
{datasource && VariableQueryEditor && (
|
||||||
<QueryEditor
|
<Box marginBottom={2}>
|
||||||
onQueryChange={onQueryChange}
|
<QueryEditor
|
||||||
onLegacyQueryChange={onLegacyQueryChange}
|
onQueryChange={onQueryChange}
|
||||||
datasource={datasource}
|
onLegacyQueryChange={onLegacyQueryChange}
|
||||||
query={query}
|
datasource={datasource}
|
||||||
VariableQueryEditor={VariableQueryEditor}
|
query={query}
|
||||||
timeRange={timeRange}
|
VariableQueryEditor={VariableQueryEditor}
|
||||||
/>
|
timeRange={timeRange}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<VariableTextAreaField
|
{!disableRegexEdition && (
|
||||||
defaultValue={regex ?? ''}
|
<VariableTextAreaField
|
||||||
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
|
defaultValue={regex ?? ''}
|
||||||
description={
|
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
|
||||||
<div>
|
description={
|
||||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
|
<div>
|
||||||
Optional, if you want to extract part of a series name or metric node segment.
|
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
|
||||||
</Trans>
|
Optional, if you want to extract part of a series name or metric node segment.
|
||||||
<br />
|
</Trans>
|
||||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
|
<br />
|
||||||
Named capture groups can be used to separate the display text and value (
|
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
|
||||||
<TextLink
|
Named capture groups can be used to separate the display text and value (
|
||||||
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
|
<TextLink
|
||||||
external
|
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>
|
see examples
|
||||||
).
|
</TextLink>
|
||||||
</Trans>
|
).
|
||||||
</div>
|
</Trans>
|
||||||
}
|
</div>
|
||||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
}
|
||||||
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
|
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||||
onBlur={onRegExChange}
|
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
|
||||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
|
onBlur={onRegExChange}
|
||||||
width={52}
|
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
|
||||||
/>
|
width={52}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<QueryVariableSortSelect
|
<QueryVariableSortSelect
|
||||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2}
|
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2}
|
||||||
@@ -171,7 +184,7 @@ export function QueryVariableEditorForm({
|
|||||||
refresh={refresh}
|
refresh={refresh}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{onStaticOptionsChange && onStaticOptionsOrderChange && (
|
{!disableStaticOptions && onStaticOptionsChange && onStaticOptionsOrderChange && (
|
||||||
<QueryVariableStaticOptions
|
<QueryVariableStaticOptions
|
||||||
staticOptions={staticOptions}
|
staticOptions={staticOptions}
|
||||||
staticOptionsOrder={staticOptionsOrder}
|
staticOptionsOrder={staticOptionsOrder}
|
||||||
@@ -187,6 +200,8 @@ export function QueryVariableEditorForm({
|
|||||||
multi={!!isMulti}
|
multi={!!isMulti}
|
||||||
includeAll={!!includeAll}
|
includeAll={!!includeAll}
|
||||||
allowCustomValue={allowCustomValue}
|
allowCustomValue={allowCustomValue}
|
||||||
|
disableAllowCustomValue={disableAllowCustomValue}
|
||||||
|
disableCustomAllValue={disableCustomAllValue}
|
||||||
allValue={allValue}
|
allValue={allValue}
|
||||||
onMultiChange={onMultiChange}
|
onMultiChange={onMultiChange}
|
||||||
onIncludeAllChange={onIncludeAllChange}
|
onIncludeAllChange={onIncludeAllChange}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ interface SelectionOptionsFormProps {
|
|||||||
multi: boolean;
|
multi: boolean;
|
||||||
includeAll: boolean;
|
includeAll: boolean;
|
||||||
allowCustomValue?: boolean;
|
allowCustomValue?: boolean;
|
||||||
|
disableAllowCustomValue?: boolean;
|
||||||
allValue?: string | null;
|
allValue?: string | null;
|
||||||
|
disableCustomAllValue?: boolean;
|
||||||
onMultiChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
onMultiChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
onAllowCustomValueChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
onAllowCustomValueChange?: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
onIncludeAllChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
onIncludeAllChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||||
@@ -20,8 +22,10 @@ interface SelectionOptionsFormProps {
|
|||||||
export function SelectionOptionsForm({
|
export function SelectionOptionsForm({
|
||||||
multi,
|
multi,
|
||||||
allowCustomValue,
|
allowCustomValue,
|
||||||
|
disableAllowCustomValue,
|
||||||
includeAll,
|
includeAll,
|
||||||
allValue,
|
allValue,
|
||||||
|
disableCustomAllValue,
|
||||||
onMultiChange,
|
onMultiChange,
|
||||||
onAllowCustomValueChange,
|
onAllowCustomValueChange,
|
||||||
onIncludeAllChange,
|
onIncludeAllChange,
|
||||||
@@ -39,18 +43,19 @@ export function SelectionOptionsForm({
|
|||||||
onChange={onMultiChange}
|
onChange={onMultiChange}
|
||||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch}
|
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch}
|
||||||
/>
|
/>
|
||||||
{onAllowCustomValueChange && ( // backwards compat with old arch, remove on cleanup
|
{!disableAllowCustomValue &&
|
||||||
<VariableCheckboxField
|
onAllowCustomValueChange && ( // backwards compat with old arch, remove on cleanup
|
||||||
value={allowCustomValue ?? true}
|
<VariableCheckboxField
|
||||||
name={t('dashboard-scene.selection-options-form.name-allow-custom-values', 'Allow custom values')}
|
value={allowCustomValue ?? true}
|
||||||
description={t(
|
name={t('dashboard-scene.selection-options-form.name-allow-custom-values', 'Allow custom values')}
|
||||||
'dashboard-scene.selection-options-form.description-enables-users-custom-values',
|
description={t(
|
||||||
'Enables users to add custom values to the list'
|
'dashboard-scene.selection-options-form.description-enables-users-custom-values',
|
||||||
)}
|
'Enables users to add custom values to the list'
|
||||||
onChange={onAllowCustomValueChange}
|
)}
|
||||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch}
|
onChange={onAllowCustomValueChange}
|
||||||
/>
|
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch}
|
||||||
)}
|
/>
|
||||||
|
)}
|
||||||
<VariableCheckboxField
|
<VariableCheckboxField
|
||||||
value={includeAll}
|
value={includeAll}
|
||||||
name={t('dashboard-scene.selection-options-form.name-include-all-option', 'Include All option')}
|
name={t('dashboard-scene.selection-options-form.name-include-all-option', 'Include All option')}
|
||||||
@@ -61,7 +66,7 @@ export function SelectionOptionsForm({
|
|||||||
onChange={onIncludeAllChange}
|
onChange={onIncludeAllChange}
|
||||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch}
|
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch}
|
||||||
/>
|
/>
|
||||||
{includeAll && (
|
{!disableCustomAllValue && includeAll && (
|
||||||
<VariableTextField
|
<VariableTextField
|
||||||
defaultValue={allValue ?? ''}
|
defaultValue={allValue ?? ''}
|
||||||
onBlur={onAllValueChange}
|
onBlur={onAllValueChange}
|
||||||
|
|||||||
@@ -5,13 +5,50 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { Trans } from '@grafana/i18n';
|
import { Trans } from '@grafana/i18n';
|
||||||
import { VariableValueOption } from '@grafana/scenes';
|
import { VariableValueOption } from '@grafana/scenes';
|
||||||
import { Button, InlineFieldRow, InlineLabel, useStyles2, Text } from '@grafana/ui';
|
import { Button, InlineFieldRow, InlineLabel, InteractiveTable, Text, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
export interface VariableValuesPreviewProps {
|
export interface Props {
|
||||||
options: VariableValueOption[];
|
options: VariableValueOption[];
|
||||||
|
hasMultiProps?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VariableValuesPreview = ({ options }: VariableValuesPreviewProps) => {
|
export const VariableValuesPreview = ({ options, hasMultiProps }: Props) => {
|
||||||
|
if (!options.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMultiProps) {
|
||||||
|
return <VariableValuesWithPropsPreview options={options} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <VariableValuesWithoutPropsPreview options={options} />;
|
||||||
|
};
|
||||||
|
VariableValuesPreview.displayName = 'VariableValuesPreview';
|
||||||
|
|
||||||
|
function VariableValuesWithPropsPreview({ options }: { options: VariableValueOption[] }) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const data = options.map((o) => ({ label: String(o.label), value: String(o.value), ...o.properties }));
|
||||||
|
// the first item in data may be the "All" option, which does not have any extra properties, so we try the 2nd item to determine the column names
|
||||||
|
const columns = Object.keys(data[1] || data[0]).map((id) => ({ id, header: id, sortType: 'alphanumeric' as const }));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.previewContainer} style={{ gap: '8px' }}>
|
||||||
|
<Text variant="bodySmall" weight="medium">
|
||||||
|
<Trans i18nKey="dashboard-scene.variable-values-preview.preview-of-values">Preview of values</Trans>
|
||||||
|
</Text>
|
||||||
|
<InteractiveTable
|
||||||
|
className={styles.table}
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
getRowId={(r) => String(r.value)}
|
||||||
|
pageSize={8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VariableValuesWithoutPropsPreview({ options }: { options: VariableValueOption[] }) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
const [previewLimit, setPreviewLimit] = useState(20);
|
const [previewLimit, setPreviewLimit] = useState(20);
|
||||||
const [previewOptions, setPreviewOptions] = useState<VariableValueOption[]>([]);
|
const [previewOptions, setPreviewOptions] = useState<VariableValueOption[]>([]);
|
||||||
const showMoreOptions = useCallback(
|
const showMoreOptions = useCallback(
|
||||||
@@ -21,15 +58,10 @@ export const VariableValuesPreview = ({ options }: VariableValuesPreviewProps) =
|
|||||||
},
|
},
|
||||||
[previewLimit, setPreviewLimit]
|
[previewLimit, setPreviewLimit]
|
||||||
);
|
);
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
useEffect(() => setPreviewOptions(options.slice(0, previewLimit)), [previewLimit, options]);
|
useEffect(() => setPreviewOptions(options.slice(0, previewLimit)), [previewLimit, options]);
|
||||||
|
|
||||||
if (!previewOptions.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', marginTop: '16px' }}>
|
<div className={styles.previewContainer}>
|
||||||
<Text variant="bodySmall" weight="medium">
|
<Text variant="bodySmall" weight="medium">
|
||||||
<Trans i18nKey="dashboard-scene.variable-values-preview.preview-of-values">Preview of values</Trans>
|
<Trans i18nKey="dashboard-scene.variable-values-preview.preview-of-values">Preview of values</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -51,12 +83,12 @@ export const VariableValuesPreview = ({ options }: VariableValuesPreviewProps) =
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
VariableValuesPreview.displayName = 'VariableValuesPreview';
|
VariableValuesWithoutPropsPreview.displayName = 'VariableValuesWithoutPropsPreview';
|
||||||
|
|
||||||
function getStyles(theme: GrafanaTheme2) {
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
return {
|
return {
|
||||||
wrapper: css({
|
previewContainer: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
marginTop: theme.spacing(2),
|
marginTop: theme.spacing(2),
|
||||||
@@ -71,5 +103,10 @@ function getStyles(theme: GrafanaTheme2) {
|
|||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
maxWidth: '50vw',
|
maxWidth: '50vw',
|
||||||
}),
|
}),
|
||||||
|
table: css({
|
||||||
|
td: css({
|
||||||
|
padding: theme.spacing(0.5, 1),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,117 +5,225 @@ import { CustomVariable } from '@grafana/scenes';
|
|||||||
|
|
||||||
import { CustomVariableEditor } from './CustomVariableEditor';
|
import { CustomVariableEditor } from './CustomVariableEditor';
|
||||||
|
|
||||||
|
function setup(options: Partial<ConstructorParameters<typeof CustomVariable>[0]> = {}) {
|
||||||
|
return {
|
||||||
|
variable: new CustomVariable({
|
||||||
|
name: 'customVar',
|
||||||
|
...options,
|
||||||
|
}),
|
||||||
|
onRunQuery: jest.fn(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEditor(ui: React.ReactNode) {
|
||||||
|
const renderResult = render(ui);
|
||||||
|
|
||||||
|
const elements = {
|
||||||
|
formatButton: (label: string) => renderResult.queryByLabelText(label) as HTMLElement,
|
||||||
|
queryInput: () =>
|
||||||
|
renderResult.queryByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput
|
||||||
|
) as HTMLTextAreaElement,
|
||||||
|
multiValueCheckbox: () =>
|
||||||
|
renderResult.queryByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
|
||||||
|
) as HTMLInputElement,
|
||||||
|
allowCustomValueCheckbox: () =>
|
||||||
|
renderResult.queryByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
|
||||||
|
) as HTMLInputElement,
|
||||||
|
includeAllCheckbox: () =>
|
||||||
|
renderResult.queryByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
|
||||||
|
) as HTMLInputElement,
|
||||||
|
customAllValueInput: () =>
|
||||||
|
renderResult.queryByTestId(
|
||||||
|
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
|
||||||
|
) as HTMLInputElement,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...renderResult,
|
||||||
|
elements,
|
||||||
|
actions: {
|
||||||
|
updateValuesInput(newQuery: string) {
|
||||||
|
fireEvent.change(elements.queryInput(), { target: { value: newQuery } });
|
||||||
|
fireEvent.blur(elements.queryInput());
|
||||||
|
},
|
||||||
|
changeValuesFormat(newFormat: 'csv' | 'json') {
|
||||||
|
const targetLabel = newFormat === 'json' ? 'Object values in a JSON array' : 'Values separated by comma';
|
||||||
|
|
||||||
|
const formatButton = elements.formatButton(targetLabel);
|
||||||
|
if (formatButton === null) {
|
||||||
|
throw new Error(`Unable to fire a "click" event - button with label "${targetLabel}" not found in DOM`);
|
||||||
|
}
|
||||||
|
|
||||||
|
fireEvent.click(formatButton);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
describe('CustomVariableEditor', () => {
|
describe('CustomVariableEditor', () => {
|
||||||
it('should render the CustomVariableForm with correct initial values', () => {
|
describe('CSV values format', () => {
|
||||||
const variable = new CustomVariable({
|
it('should render CustomVariableForm with the correct initial values', () => {
|
||||||
name: 'customVar',
|
const { variable, onRunQuery } = setup({
|
||||||
query: 'test, test2',
|
query: 'test, test2',
|
||||||
value: 'test',
|
value: 'test',
|
||||||
isMulti: true,
|
isMulti: true,
|
||||||
includeAll: true,
|
includeAll: true,
|
||||||
allValue: 'test',
|
allowCustomValue: true,
|
||||||
|
allValue: 'all',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { elements } = renderEditor(<CustomVariableEditor variable={variable} onRunQuery={onRunQuery} />);
|
||||||
|
|
||||||
|
expect(elements.queryInput().value).toBe('test, test2');
|
||||||
|
expect(elements.multiValueCheckbox().checked).toBe(true);
|
||||||
|
expect(elements.allowCustomValueCheckbox().checked).toBe(true);
|
||||||
|
expect(elements.includeAllCheckbox().checked).toBe(true);
|
||||||
|
expect(elements.customAllValueInput().value).toBe('all');
|
||||||
});
|
});
|
||||||
const onRunQuery = jest.fn();
|
|
||||||
|
|
||||||
const { getByTestId } = render(<CustomVariableEditor variable={variable} onRunQuery={onRunQuery} />);
|
it('should update the variable state when some input values change ("Multi-value", "Allow custom values" & "Include All option")', () => {
|
||||||
|
const { variable, onRunQuery } = setup({
|
||||||
|
query: 'test, test2',
|
||||||
|
value: 'test',
|
||||||
|
isMulti: false,
|
||||||
|
allowCustomValue: false,
|
||||||
|
includeAll: false,
|
||||||
|
});
|
||||||
|
|
||||||
const queryInput = getByTestId(
|
const { elements } = renderEditor(<CustomVariableEditor variable={variable} onRunQuery={onRunQuery} />);
|
||||||
selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const allValueInput = getByTestId(
|
|
||||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const multiCheckbox = getByTestId(
|
|
||||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
|
|
||||||
) as HTMLInputElement;
|
|
||||||
const includeAllCheckbox = getByTestId(
|
|
||||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
expect(queryInput.value).toBe('test, test2');
|
expect(elements.multiValueCheckbox().checked).toBe(false);
|
||||||
expect(allValueInput.value).toBe('test');
|
expect(elements.allowCustomValueCheckbox().checked).toBe(false);
|
||||||
expect(multiCheckbox.checked).toBe(true);
|
expect(elements.includeAllCheckbox().checked).toBe(false);
|
||||||
expect(includeAllCheckbox.checked).toBe(true);
|
// include-all-custom input appears after include-all checkbox is checked only
|
||||||
|
expect(elements.customAllValueInput()).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(elements.multiValueCheckbox());
|
||||||
|
fireEvent.click(elements.allowCustomValueCheckbox());
|
||||||
|
fireEvent.click(elements.includeAllCheckbox());
|
||||||
|
|
||||||
|
expect(variable.state.isMulti).toBe(true);
|
||||||
|
expect(variable.state.allowCustomValue).toBe(true);
|
||||||
|
expect(variable.state.includeAll).toBe(true);
|
||||||
|
expect(elements.customAllValueInput()).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the values textarea loses focus after its value has changed', () => {
|
||||||
|
it('should update the query in the variable state and call the onRunQuery callback', async () => {
|
||||||
|
const { variable, onRunQuery } = setup({ query: 'test, test2', value: 'test' });
|
||||||
|
|
||||||
|
const { actions } = renderEditor(<CustomVariableEditor variable={variable} onRunQuery={onRunQuery} />);
|
||||||
|
|
||||||
|
actions.updateValuesInput('test3, test4');
|
||||||
|
|
||||||
|
expect(variable.state.query).toBe('test3, test4');
|
||||||
|
expect(onRunQuery).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the "Custom all value" input loses focus after its value has changed', () => {
|
||||||
|
it('should update the variable state', () => {
|
||||||
|
const { variable, onRunQuery } = setup({
|
||||||
|
query: 'test, test2',
|
||||||
|
value: 'test',
|
||||||
|
isMulti: true,
|
||||||
|
includeAll: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { elements } = renderEditor(<CustomVariableEditor variable={variable} onRunQuery={onRunQuery} />);
|
||||||
|
|
||||||
|
fireEvent.change(elements.customAllValueInput(), { target: { value: 'new custom all' } });
|
||||||
|
fireEvent.blur(elements.customAllValueInput());
|
||||||
|
|
||||||
|
expect(variable.state.allValue).toBe('new custom all');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the variable state when input values change', () => {
|
describe('JSON values format', () => {
|
||||||
const variable = new CustomVariable({
|
const initialJsonQuery = `[
|
||||||
name: 'customVar',
|
{"value":1,"text":"Development","aws":"dev","azure":"development"},
|
||||||
query: 'test, test2',
|
{"value":2,"text":"Production","aws":"prod","azure":"production"}
|
||||||
value: 'test',
|
]`;
|
||||||
|
|
||||||
|
it('should render CustomVariableForm with the correct initial values', () => {
|
||||||
|
const { variable, onRunQuery } = setup({
|
||||||
|
valuesFormat: 'json',
|
||||||
|
query: initialJsonQuery,
|
||||||
|
isMulti: true,
|
||||||
|
includeAll: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { elements } = renderEditor(<CustomVariableEditor variable={variable} onRunQuery={onRunQuery} />);
|
||||||
|
|
||||||
|
expect(elements.queryInput().value).toBe(initialJsonQuery);
|
||||||
|
expect(elements.multiValueCheckbox().checked).toBe(true);
|
||||||
|
expect(elements.allowCustomValueCheckbox()).not.toBeInTheDocument();
|
||||||
|
expect(elements.includeAllCheckbox().checked).toBe(true);
|
||||||
|
expect(elements.customAllValueInput()).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
const onRunQuery = jest.fn();
|
|
||||||
|
|
||||||
const { getByTestId } = render(<CustomVariableEditor variable={variable} onRunQuery={onRunQuery} />);
|
describe('when the values textarea loses focus after its value has changed', () => {
|
||||||
|
describe('if the value is valid JSON', () => {
|
||||||
|
it('should update the query in the variable state and call the onRunQuery callback', async () => {
|
||||||
|
const { variable, onRunQuery } = setup({ valuesFormat: 'json', query: initialJsonQuery });
|
||||||
|
|
||||||
const multiCheckbox = getByTestId(
|
const { actions } = renderEditor(<CustomVariableEditor variable={variable} onRunQuery={onRunQuery} />);
|
||||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch
|
|
||||||
);
|
|
||||||
const includeAllCheckbox = getByTestId(
|
|
||||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch
|
|
||||||
);
|
|
||||||
|
|
||||||
const allowCustomValueCheckbox = getByTestId(
|
actions.updateValuesInput('[]');
|
||||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsAllowCustomValueSwitch
|
|
||||||
);
|
|
||||||
|
|
||||||
// It include-all-custom input appears after include-all checkbox is checked only
|
expect(variable.state.query).toBe('[]');
|
||||||
expect(() =>
|
expect(onRunQuery).toHaveBeenCalled();
|
||||||
getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput)
|
});
|
||||||
).toThrow('Unable to find an element');
|
});
|
||||||
|
|
||||||
fireEvent.click(allowCustomValueCheckbox);
|
describe('if the value is NOT valid JSON', () => {
|
||||||
|
it('should display a validation error message and neither update the query in the variable state nor call the onRunQuery callback', async () => {
|
||||||
|
const { variable, onRunQuery } = setup({ valuesFormat: 'json', query: initialJsonQuery });
|
||||||
|
|
||||||
fireEvent.click(multiCheckbox);
|
const { actions, getByRole } = renderEditor(
|
||||||
|
<CustomVariableEditor variable={variable} onRunQuery={onRunQuery} />
|
||||||
|
);
|
||||||
|
|
||||||
fireEvent.click(includeAllCheckbox);
|
actions.updateValuesInput('[x]');
|
||||||
const allValueInput = getByTestId(
|
|
||||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(variable.state.isMulti).toBe(true);
|
expect(getByRole('alert')).toHaveTextContent(`Unexpected token 'x', "[x]" is not valid JSON`);
|
||||||
expect(variable.state.includeAll).toBe(true);
|
expect(variable.state.query).toBe(initialJsonQuery);
|
||||||
expect(variable.state.allowCustomValue).toBe(false);
|
expect(onRunQuery).not.toHaveBeenCalled();
|
||||||
expect(allValueInput).toBeInTheDocument();
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call update query and re-run query when input loses focus', async () => {
|
describe('when switching values format', () => {
|
||||||
const variable = new CustomVariable({
|
it('should switch the visibility of the proper form inputs ("Allow custom values" and "Custom all value")', () => {
|
||||||
name: 'customVar',
|
const { variable, onRunQuery } = setup({
|
||||||
query: 'test, test2',
|
valuesFormat: 'csv',
|
||||||
value: 'test',
|
query: '',
|
||||||
|
isMulti: true,
|
||||||
|
includeAll: true,
|
||||||
|
allowCustomValue: true,
|
||||||
|
allValue: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { elements, actions } = renderEditor(<CustomVariableEditor variable={variable} onRunQuery={onRunQuery} />);
|
||||||
|
|
||||||
|
expect(elements.allowCustomValueCheckbox()).toBeInTheDocument();
|
||||||
|
expect(elements.customAllValueInput()).toBeInTheDocument();
|
||||||
|
|
||||||
|
actions.changeValuesFormat('json');
|
||||||
|
|
||||||
|
expect(elements.allowCustomValueCheckbox()).not.toBeInTheDocument();
|
||||||
|
expect(elements.customAllValueInput()).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
actions.changeValuesFormat('csv');
|
||||||
|
|
||||||
|
expect(elements.allowCustomValueCheckbox()).toBeInTheDocument();
|
||||||
|
expect(elements.customAllValueInput()).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
const onRunQuery = jest.fn();
|
|
||||||
|
|
||||||
const { getByTestId } = render(<CustomVariableEditor variable={variable} onRunQuery={onRunQuery} />);
|
|
||||||
|
|
||||||
const queryInput = getByTestId(selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput);
|
|
||||||
fireEvent.change(queryInput, { target: { value: 'test3, test4' } });
|
|
||||||
fireEvent.blur(queryInput);
|
|
||||||
|
|
||||||
expect(onRunQuery).toHaveBeenCalled();
|
|
||||||
expect(variable.state.query).toBe('test3, test4');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update the variable state when all-custom-value input loses focus', () => {
|
|
||||||
const variable = new CustomVariable({
|
|
||||||
name: 'customVar',
|
|
||||||
query: 'test, test2',
|
|
||||||
value: 'test',
|
|
||||||
isMulti: true,
|
|
||||||
includeAll: true,
|
|
||||||
});
|
|
||||||
const onRunQuery = jest.fn();
|
|
||||||
|
|
||||||
const { getByTestId } = render(<CustomVariableEditor variable={variable} onRunQuery={onRunQuery} />);
|
|
||||||
|
|
||||||
const allValueInput = getByTestId(
|
|
||||||
selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput
|
|
||||||
) as HTMLInputElement;
|
|
||||||
|
|
||||||
fireEvent.change(allValueInput, { target: { value: 'new custom all' } });
|
|
||||||
fireEvent.blur(allValueInput);
|
|
||||||
|
|
||||||
expect(variable.state.allValue).toBe('new custom all');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { FormEvent, useCallback } from 'react';
|
import { isObject } from 'lodash';
|
||||||
|
import { FormEvent, useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { CustomVariableModel, shallowCompare } from '@grafana/data';
|
||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||||
|
|
||||||
@@ -14,7 +16,26 @@ interface CustomVariableEditorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEditorProps) {
|
export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEditorProps) {
|
||||||
const { query, isMulti, allValue, includeAll, allowCustomValue } = variable.useState();
|
const { query, valuesFormat, isMulti, allValue, includeAll, allowCustomValue } = variable.useState();
|
||||||
|
const [queryValidationError, setQueryValidationError] = useState<Error>();
|
||||||
|
|
||||||
|
const [prevQuery, setPrevQuery] = useState('');
|
||||||
|
const onValuesFormatChange = useCallback(
|
||||||
|
(format: CustomVariableModel['valuesFormat']) => {
|
||||||
|
variable.setState({ query: prevQuery });
|
||||||
|
variable.setState({ value: isMulti ? [] : undefined });
|
||||||
|
variable.setState({ valuesFormat: format });
|
||||||
|
variable.setState({ allowCustomValue: false });
|
||||||
|
variable.setState({ allValue: undefined });
|
||||||
|
onRunQuery();
|
||||||
|
|
||||||
|
setQueryValidationError(undefined);
|
||||||
|
if (query !== prevQuery) {
|
||||||
|
setPrevQuery(query);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isMulti, onRunQuery, prevQuery, query, variable]
|
||||||
|
);
|
||||||
|
|
||||||
const onMultiChange = useCallback(
|
const onMultiChange = useCallback(
|
||||||
(event: FormEvent<HTMLInputElement>) => {
|
(event: FormEvent<HTMLInputElement>) => {
|
||||||
@@ -32,10 +53,20 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
|
|||||||
|
|
||||||
const onQueryChange = useCallback(
|
const onQueryChange = useCallback(
|
||||||
(event: FormEvent<HTMLTextAreaElement>) => {
|
(event: FormEvent<HTMLTextAreaElement>) => {
|
||||||
|
setPrevQuery('');
|
||||||
|
|
||||||
|
if (valuesFormat === 'json') {
|
||||||
|
const validationError = validateJsonQuery(event.currentTarget.value);
|
||||||
|
setQueryValidationError(validationError);
|
||||||
|
if (validationError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
variable.setState({ query: event.currentTarget.value });
|
variable.setState({ query: event.currentTarget.value });
|
||||||
onRunQuery();
|
onRunQuery();
|
||||||
},
|
},
|
||||||
[variable, onRunQuery]
|
[valuesFormat, variable, onRunQuery]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onAllValueChange = useCallback(
|
const onAllValueChange = useCallback(
|
||||||
@@ -55,15 +86,18 @@ export function CustomVariableEditor({ variable, onRunQuery }: CustomVariableEdi
|
|||||||
return (
|
return (
|
||||||
<CustomVariableForm
|
<CustomVariableForm
|
||||||
query={query ?? ''}
|
query={query ?? ''}
|
||||||
|
valuesFormat={valuesFormat ?? 'csv'}
|
||||||
multi={!!isMulti}
|
multi={!!isMulti}
|
||||||
allValue={allValue ?? ''}
|
allValue={allValue ?? ''}
|
||||||
includeAll={!!includeAll}
|
includeAll={!!includeAll}
|
||||||
allowCustomValue={allowCustomValue}
|
allowCustomValue={allowCustomValue}
|
||||||
|
queryValidationError={queryValidationError}
|
||||||
|
onQueryChange={onQueryChange}
|
||||||
onMultiChange={onMultiChange}
|
onMultiChange={onMultiChange}
|
||||||
onIncludeAllChange={onIncludeAllChange}
|
onIncludeAllChange={onIncludeAllChange}
|
||||||
onQueryChange={onQueryChange}
|
|
||||||
onAllValueChange={onAllValueChange}
|
onAllValueChange={onAllValueChange}
|
||||||
onAllowCustomValueChange={onAllowCustomValueChange}
|
onAllowCustomValueChange={onAllowCustomValueChange}
|
||||||
|
onValuesFormatChange={onValuesFormatChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -81,3 +115,47 @@ export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneIt
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const validateJsonQuery = (rawQuery: string): Error | undefined => {
|
||||||
|
const query = rawQuery.trim();
|
||||||
|
if (!query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = JSON.parse(query);
|
||||||
|
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
throw new Error('Enter a valid JSON array of objects');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorIndex = options.findIndex((item) => !isObject(item));
|
||||||
|
if (errorIndex !== -1) {
|
||||||
|
throw new Error(`All items must be objects. The item at index ${errorIndex} is not an object.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = Object.keys(options[0]);
|
||||||
|
if (!keys.includes('value')) {
|
||||||
|
throw new Error('Each object in the array must include at least a "value" property');
|
||||||
|
}
|
||||||
|
if (keys.includes('')) {
|
||||||
|
throw new Error('Object property names cannot be empty strings');
|
||||||
|
}
|
||||||
|
|
||||||
|
errorIndex = options.findIndex((o) => !shallowCompare(keys, Object.keys(o)));
|
||||||
|
if (errorIndex !== -1) {
|
||||||
|
throw new Error(
|
||||||
|
`All objects must have the same set of properties. The object at index ${errorIndex} does not match the expected properties`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
return error as Error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { FormEvent, useCallback, useState } from 'react';
|
||||||
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
import { CustomVariableModel } from '@grafana/data';
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { t, Trans } from '@grafana/i18n';
|
import { t, Trans } from '@grafana/i18n';
|
||||||
import { CustomVariable } from '@grafana/scenes';
|
import { CustomVariable } from '@grafana/scenes';
|
||||||
import { Button, Modal, Stack } from '@grafana/ui';
|
import { Button, FieldValidationMessage, Modal, Stack, TextArea } from '@grafana/ui';
|
||||||
|
|
||||||
import { VariableStaticOptionsFormRef } from '../../components/VariableStaticOptionsForm';
|
import { ValuesFormatSelector } from '../../components/CustomVariableForm';
|
||||||
import { VariableStaticOptionsFormAddButton } from '../../components/VariableStaticOptionsFormAddButton';
|
import { VariableValuesPreview } from '../../components/VariableValuesPreview';
|
||||||
|
|
||||||
import { ValuesBuilder } from './ValuesBuilder';
|
import { validateJsonQuery } from './CustomVariableEditor';
|
||||||
import { ValuesPreview } from './ValuesPreview';
|
|
||||||
|
|
||||||
interface ModalEditorProps {
|
interface ModalEditorProps {
|
||||||
variable: CustomVariable;
|
variable: CustomVariable;
|
||||||
@@ -18,9 +19,49 @@ interface ModalEditorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ModalEditor({ variable, isOpen, onClose }: ModalEditorProps) {
|
export function ModalEditor({ variable, isOpen, onClose }: ModalEditorProps) {
|
||||||
const formRef = useRef<VariableStaticOptionsFormRef | null>(null);
|
const { query, valuesFormat, isMulti } = variable.useState();
|
||||||
|
const [prevQuery, setPrevQuery] = useState('');
|
||||||
|
const [queryValidationError, setQueryValidationError] = useState<Error>();
|
||||||
|
|
||||||
const handleOnAdd = useCallback(() => formRef.current?.addItem(), []);
|
const onValuesFormatChange = useCallback(
|
||||||
|
async (format: CustomVariableModel['valuesFormat']) => {
|
||||||
|
variable.setState({ query: prevQuery });
|
||||||
|
variable.setState({ value: isMulti ? [] : undefined });
|
||||||
|
variable.setState({ valuesFormat: format });
|
||||||
|
variable.setState({ allowCustomValue: false });
|
||||||
|
variable.setState({ allValue: undefined });
|
||||||
|
|
||||||
|
await lastValueFrom(variable.validateAndUpdate());
|
||||||
|
|
||||||
|
setQueryValidationError(undefined);
|
||||||
|
if (query !== prevQuery) {
|
||||||
|
setPrevQuery(query);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isMulti, prevQuery, query, variable]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onQueryChange = useCallback(
|
||||||
|
async (event: FormEvent<HTMLTextAreaElement>) => {
|
||||||
|
setPrevQuery('');
|
||||||
|
|
||||||
|
if (valuesFormat === 'json') {
|
||||||
|
const validationError = validateJsonQuery(event.currentTarget.value);
|
||||||
|
setQueryValidationError(validationError);
|
||||||
|
if (validationError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable.setState({ query: event.currentTarget.value });
|
||||||
|
await lastValueFrom(variable.validateAndUpdate());
|
||||||
|
},
|
||||||
|
[valuesFormat, variable]
|
||||||
|
);
|
||||||
|
|
||||||
|
const optionsForSelect = variable.getOptionsForSelect(false);
|
||||||
|
const hasJsonValuesFormat = variable.state.valuesFormat === 'json';
|
||||||
|
const hasMultiProps = hasJsonValuesFormat || optionsForSelect.every((o) => Boolean(o.properties));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -29,10 +70,31 @@ export function ModalEditor({ variable, isOpen, onClose }: ModalEditorProps) {
|
|||||||
onDismiss={onClose}
|
onDismiss={onClose}
|
||||||
>
|
>
|
||||||
<Stack direction="column" gap={2}>
|
<Stack direction="column" gap={2}>
|
||||||
<ValuesBuilder variable={variable} ref={formRef} />
|
<ValuesFormatSelector valuesFormat={valuesFormat} onValuesFormatChange={onValuesFormatChange} />
|
||||||
<ValuesPreview variable={variable} />
|
<div>
|
||||||
|
<TextArea
|
||||||
|
id={valuesFormat}
|
||||||
|
key={valuesFormat}
|
||||||
|
rows={4}
|
||||||
|
defaultValue={query}
|
||||||
|
onBlur={onQueryChange}
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
required
|
||||||
|
data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput}
|
||||||
|
/>
|
||||||
|
{queryValidationError && <FieldValidationMessage>{queryValidationError.message}</FieldValidationMessage>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<VariableValuesPreview options={optionsForSelect} hasMultiProps={hasMultiProps} />
|
||||||
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Modal.ButtonRow leftItems={<VariableStaticOptionsFormAddButton onAdd={handleOnAdd} />}>
|
<Modal.ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
fill="outline"
|
fill="outline"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { t } from '@grafana/i18n';
|
|
||||||
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
import { CustomVariable, SceneVariable } from '@grafana/scenes';
|
||||||
|
|
||||||
import { OptionsPaneItemDescriptor } from '../../../../../dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
import { OptionsPaneItemDescriptor } from '../../../../../dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||||
@@ -12,7 +11,6 @@ export function getCustomVariableOptions(variable: SceneVariable): OptionsPaneIt
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
new OptionsPaneItemDescriptor({
|
new OptionsPaneItemDescriptor({
|
||||||
title: t('dashboard.edit-pane.variable.custom-options.values', 'Values separated by comma'),
|
|
||||||
id: 'custom-variable-values',
|
id: 'custom-variable-values',
|
||||||
render: ({ props }) => <PaneItem id={props.id} variable={variable} />,
|
render: ({ props }) => <PaneItem id={props.id} variable={variable} />,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, FormEvent } from 'react';
|
import { useState, FormEvent, useMemo, useEffect } from 'react';
|
||||||
import { useAsync } from 'react-use';
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
import { SelectableValue, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data';
|
import { SelectableValue, DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data';
|
||||||
@@ -7,7 +7,7 @@ import { Trans, t } from '@grafana/i18n';
|
|||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { QueryVariable, sceneGraph, SceneVariable } from '@grafana/scenes';
|
import { QueryVariable, sceneGraph, SceneVariable } from '@grafana/scenes';
|
||||||
import { VariableRefresh, VariableSort } from '@grafana/schema';
|
import { VariableRefresh, VariableSort } from '@grafana/schema';
|
||||||
import { Box, Button, Field, Modal, TextLink } from '@grafana/ui';
|
import { Box, Button, Field, Modal, Switch, TextLink } from '@grafana/ui';
|
||||||
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||||
import { QueryEditor } from 'app/features/dashboard-scene/settings/variables/components/QueryEditor';
|
import { QueryEditor } from 'app/features/dashboard-scene/settings/variables/components/QueryEditor';
|
||||||
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||||
@@ -44,6 +44,7 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito
|
|||||||
allowCustomValue,
|
allowCustomValue,
|
||||||
staticOptions,
|
staticOptions,
|
||||||
staticOptionsOrder,
|
staticOptionsOrder,
|
||||||
|
options,
|
||||||
} = variable.useState();
|
} = variable.useState();
|
||||||
const { value: timeRange } = sceneGraph.getTimeRange(variable).useState();
|
const { value: timeRange } = sceneGraph.getTimeRange(variable).useState();
|
||||||
|
|
||||||
@@ -93,6 +94,17 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito
|
|||||||
variable.setState({ staticOptionsOrder });
|
variable.setState({ staticOptionsOrder });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasMultiProps = useMemo(() => options.every((o) => Boolean(o.properties)), [options]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasMultiProps) {
|
||||||
|
variable.setState({ allowCustomValue: false });
|
||||||
|
variable.setState({ allValue: '' });
|
||||||
|
variable.setState({ regex: '' });
|
||||||
|
variable.setState({ staticOptions: [] });
|
||||||
|
}
|
||||||
|
}, [hasMultiProps, variable]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryVariableEditorForm
|
<QueryVariableEditorForm
|
||||||
datasource={datasource ?? undefined}
|
datasource={datasource ?? undefined}
|
||||||
@@ -103,6 +115,7 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito
|
|||||||
timeRange={timeRange}
|
timeRange={timeRange}
|
||||||
regex={regex}
|
regex={regex}
|
||||||
onRegExChange={onRegExChange}
|
onRegExChange={onRegExChange}
|
||||||
|
disableRegexEdition={hasMultiProps}
|
||||||
sort={sort}
|
sort={sort}
|
||||||
onSortChange={onSortChange}
|
onSortChange={onSortChange}
|
||||||
refresh={refresh}
|
refresh={refresh}
|
||||||
@@ -113,12 +126,15 @@ export function QueryVariableEditor({ variable, onRunQuery }: QueryVariableEdito
|
|||||||
onIncludeAllChange={onIncludeAllChange}
|
onIncludeAllChange={onIncludeAllChange}
|
||||||
allValue={allValue ?? ''}
|
allValue={allValue ?? ''}
|
||||||
onAllValueChange={onAllValueChange}
|
onAllValueChange={onAllValueChange}
|
||||||
|
disableCustomAllValue={hasMultiProps}
|
||||||
allowCustomValue={allowCustomValue}
|
allowCustomValue={allowCustomValue}
|
||||||
onAllowCustomValueChange={onAllowCustomValueChange}
|
onAllowCustomValueChange={onAllowCustomValueChange}
|
||||||
|
disableAllowCustomValue={hasMultiProps}
|
||||||
staticOptions={staticOptions}
|
staticOptions={staticOptions}
|
||||||
staticOptionsOrder={staticOptionsOrder}
|
staticOptionsOrder={staticOptionsOrder}
|
||||||
onStaticOptionsChange={onStaticOptionsChange}
|
onStaticOptionsChange={onStaticOptionsChange}
|
||||||
onStaticOptionsOrderChange={onStaticOptionsOrderChange}
|
onStaticOptionsOrderChange={onStaticOptionsOrderChange}
|
||||||
|
disableStaticOptions={hasMultiProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -250,6 +266,19 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
|||||||
|
|
||||||
const isHasVariableOptions = hasVariableOptions(variable);
|
const isHasVariableOptions = hasVariableOptions(variable);
|
||||||
|
|
||||||
|
// TODO: remove me after finished testing - each DS can/should implement their own UI
|
||||||
|
const [returnsMultiProps, setReturnsMultiProps] = useState(false);
|
||||||
|
const onChangeReturnsMultipleProps = (e: FormEvent<HTMLInputElement>) => {
|
||||||
|
setReturnsMultiProps(e.currentTarget.checked);
|
||||||
|
variable.setState({ allowCustomValue: false });
|
||||||
|
variable.setState({ allValue: '' });
|
||||||
|
variable.setState({ regex: '' });
|
||||||
|
onStaticOptionsChange?.([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionsForSelect = variable.getOptionsForSelect(false);
|
||||||
|
const hasMultiProps = returnsMultiProps || optionsForSelect.every((o) => Boolean(o.properties));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.editor}>
|
<div data-testid={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.editor}>
|
||||||
<Field
|
<Field
|
||||||
@@ -261,43 +290,64 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
|||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{selectedDatasource && VariableQueryEditor && (
|
{selectedDatasource && VariableQueryEditor && (
|
||||||
<QueryEditor
|
<Box marginBottom={2}>
|
||||||
onQueryChange={onQueryChange}
|
<QueryEditor
|
||||||
onLegacyQueryChange={onQueryChange}
|
onQueryChange={onQueryChange}
|
||||||
datasource={selectedDatasource}
|
onLegacyQueryChange={onQueryChange}
|
||||||
query={query}
|
datasource={selectedDatasource}
|
||||||
VariableQueryEditor={VariableQueryEditor}
|
query={query}
|
||||||
timeRange={timeRange}
|
VariableQueryEditor={VariableQueryEditor}
|
||||||
/>
|
timeRange={timeRange}
|
||||||
|
/>
|
||||||
|
{/* TODO: remove me after finished testing - each DS can/should implement their own UI */}
|
||||||
|
<Field
|
||||||
|
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||||
|
label="Enable access to all the fields of the query results"
|
||||||
|
description={
|
||||||
|
<Trans i18nKey="">
|
||||||
|
Check{' '}
|
||||||
|
<TextLink href="https://grafana.com/docs/grafana/latest/variables/xxx" external>
|
||||||
|
our docs
|
||||||
|
</TextLink>{' '}
|
||||||
|
for more information.
|
||||||
|
</Trans>
|
||||||
|
}
|
||||||
|
noMargin
|
||||||
|
>
|
||||||
|
<Switch onChange={onChangeReturnsMultipleProps} />
|
||||||
|
</Field>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<VariableTextAreaField
|
{!returnsMultiProps && (
|
||||||
defaultValue={regex ?? ''}
|
<VariableTextAreaField
|
||||||
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
|
defaultValue={regex ?? ''}
|
||||||
description={
|
name={t('dashboard-scene.query-variable-editor-form.name-regex', 'Regex')}
|
||||||
<div>
|
description={
|
||||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
|
<div>
|
||||||
Optional, if you want to extract part of a series name or metric node segment.
|
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-optional">
|
||||||
</Trans>
|
Optional, if you want to extract part of a series name or metric node segment.
|
||||||
<br />
|
</Trans>
|
||||||
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
|
<br />
|
||||||
Named capture groups can be used to separate the display text and value (
|
<Trans i18nKey="dashboard-scene.query-variable-editor-form.description-examples">
|
||||||
<TextLink
|
Named capture groups can be used to separate the display text and value (
|
||||||
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
|
<TextLink
|
||||||
external
|
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>
|
see examples
|
||||||
).
|
</TextLink>
|
||||||
</Trans>
|
).
|
||||||
</div>
|
</Trans>
|
||||||
}
|
</div>
|
||||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
}
|
||||||
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
|
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||||
onBlur={onRegExChange}
|
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
|
||||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
|
onBlur={onRegExChange}
|
||||||
width={52}
|
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
|
||||||
/>
|
width={52}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<QueryVariableSortSelect
|
<QueryVariableSortSelect
|
||||||
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2}
|
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2}
|
||||||
@@ -311,7 +361,7 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
|||||||
refresh={refresh}
|
refresh={refresh}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{onStaticOptionsChange && onStaticOptionsOrderChange && (
|
{!returnsMultiProps && onStaticOptionsChange && onStaticOptionsOrderChange && (
|
||||||
<QueryVariableStaticOptions
|
<QueryVariableStaticOptions
|
||||||
staticOptions={staticOptions}
|
staticOptions={staticOptions}
|
||||||
staticOptionsOrder={staticOptionsOrder}
|
staticOptionsOrder={staticOptionsOrder}
|
||||||
@@ -320,7 +370,7 @@ export function Editor({ variable }: { variable: QueryVariable }) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isHasVariableOptions && <VariableValuesPreview options={variable.getOptionsForSelect(false)} />}
|
{isHasVariableOptions && <VariableValuesPreview options={optionsForSelect} hasMultiProps={hasMultiProps} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useEffectOnce } from 'react-use';
|
||||||
|
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
import { t, Trans } from '@grafana/i18n';
|
||||||
|
import { SceneVariable, SceneVariableState } from '@grafana/scenes';
|
||||||
|
import { Input } from '@grafana/ui';
|
||||||
|
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
|
||||||
|
|
||||||
|
import { StackVariable } from '../StackVariable';
|
||||||
|
import { VariableLegend } from '../components/VariableLegend';
|
||||||
|
import { VariableSelectField } from '../components/VariableSelectField';
|
||||||
|
|
||||||
|
|
||||||
|
interface StackVariableEditorProps {
|
||||||
|
variable: StackVariable;
|
||||||
|
onRunQuery: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StackVariableEditor({ variable, onRunQuery }: StackVariableEditorProps) {
|
||||||
|
const [stackList, setStackList] = useState<Stack[]>([]);
|
||||||
|
const [selectedStack, setSelectedStack] = useState<string>(variable.useState().selectedStack?.id || '');
|
||||||
|
useEffectOnce(() => {
|
||||||
|
// fetch stacks
|
||||||
|
setStackList([stackMock]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSelectStack = (value: SelectableValue<string>) => {
|
||||||
|
variable.setStack(value.value || '')
|
||||||
|
setSelectedStack(value.value || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<VariableLegend>
|
||||||
|
<Trans i18nKey="dashboard-scene.nope">Stack selection</Trans>
|
||||||
|
</VariableLegend>
|
||||||
|
<VariableSelectField
|
||||||
|
name={t('variables.label.stack', 'Stack')}
|
||||||
|
description={t('variables.label-stack-sub', 'Select a datasource stack to use')}
|
||||||
|
value={{label: stackList.find((s) => s.uid === selectedStack)?.name, value: selectedStack}}
|
||||||
|
options={stackList.map((stack) => ({ label: stack.name, value: stack.uid }))}
|
||||||
|
onChange={onSelectStack}
|
||||||
|
//. testId={testId}
|
||||||
|
width={25}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Stack {
|
||||||
|
uid: string;
|
||||||
|
name: string;
|
||||||
|
modes: StackMode[];
|
||||||
|
template: StackTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StackMode {
|
||||||
|
name: string;
|
||||||
|
definition: Record<string, { uid: string }>;
|
||||||
|
}
|
||||||
|
interface StackTemplate {
|
||||||
|
[key: string]: {
|
||||||
|
group: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const stackMock: Stack = {
|
||||||
|
name: 'LGTM-stack',
|
||||||
|
uid: '123543453245645',
|
||||||
|
template: {
|
||||||
|
lokifromauid: {
|
||||||
|
group: 'loki',
|
||||||
|
name: 'lokifroma',
|
||||||
|
},
|
||||||
|
lokifrombuid: {
|
||||||
|
group: 'loki',
|
||||||
|
name: 'lokifromb',
|
||||||
|
},
|
||||||
|
tempouid: {
|
||||||
|
group: 'tempo',
|
||||||
|
name: 'tempo',
|
||||||
|
},
|
||||||
|
promuid: {
|
||||||
|
group: 'prometheus',
|
||||||
|
name: 'prometheus',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
name: 'prod',
|
||||||
|
definition: {
|
||||||
|
lokifromauid: {
|
||||||
|
uid: 'P0280BEB2D3524208',
|
||||||
|
},
|
||||||
|
lokifrombuid: {
|
||||||
|
uid: 'P7DC3E4760CFAC4AK',
|
||||||
|
},
|
||||||
|
tempouid: {
|
||||||
|
uid: 'P7DC3E4760CFAC4AK',
|
||||||
|
},
|
||||||
|
promuid: {
|
||||||
|
uid: 'P693D0419BB394192',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dev',
|
||||||
|
definition: {
|
||||||
|
lokifromauid: {
|
||||||
|
uid: 'P7DC3E4760CFAC4AK',
|
||||||
|
},
|
||||||
|
lokifrombuid: {
|
||||||
|
uid: 'P693D0419BB394192',
|
||||||
|
},
|
||||||
|
tempouid: {
|
||||||
|
uid: 'bf0cgxsa8lrswe',
|
||||||
|
},
|
||||||
|
promuid: {
|
||||||
|
uid: 'bf0cgxsa8lrswe',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export function getStackVariableOptions(variable: SceneVariable<SceneVariableState>): OptionsPaneItemDescriptor[] {
|
||||||
|
return [new OptionsPaneItemDescriptor({
|
||||||
|
title: t('dashboard-scene.textbox-variable-form.label-value', 'Value'),
|
||||||
|
id: `variable-${variable.state.name}-value`,
|
||||||
|
render: () => <Input label="get variable options"/>,
|
||||||
|
})]
|
||||||
|
|
||||||
|
}
|
||||||
@@ -45,7 +45,11 @@ export function useVariableSelectionOptionsCategory(variable: MultiValueVariable
|
|||||||
'A wildcard regex or other value to represent All'
|
'A wildcard regex or other value to represent All'
|
||||||
),
|
),
|
||||||
useShowIf: () => {
|
useShowIf: () => {
|
||||||
return variable.useState().includeAll ?? false;
|
const state = variable.useState();
|
||||||
|
const hasMultiProps =
|
||||||
|
('valuesFormat' in state && state.valuesFormat === 'json') ||
|
||||||
|
state.options.every((o) => Boolean(o.properties));
|
||||||
|
return hasMultiProps ? false : (state.includeAll ?? false);
|
||||||
},
|
},
|
||||||
render: (descriptor) => <CustomAllValueInput id={descriptor.props.id} variable={variable} />,
|
render: (descriptor) => <CustomAllValueInput id={descriptor.props.id} variable={variable} />,
|
||||||
})
|
})
|
||||||
@@ -58,6 +62,13 @@ export function useVariableSelectionOptionsCategory(variable: MultiValueVariable
|
|||||||
'dashboard.edit-pane.variable.selection-options.allow-custom-values-description',
|
'dashboard.edit-pane.variable.selection-options.allow-custom-values-description',
|
||||||
'Enables users to enter values'
|
'Enables users to enter values'
|
||||||
),
|
),
|
||||||
|
useShowIf: () => {
|
||||||
|
const state = variable.useState();
|
||||||
|
const hasMultiProps =
|
||||||
|
('valuesFormat' in state && state.valuesFormat === 'json') ||
|
||||||
|
state.options.every((o) => Boolean(o.properties));
|
||||||
|
return !hasMultiProps;
|
||||||
|
},
|
||||||
render: (descriptor) => <AllowCustomSwitch id={descriptor.props.id} variable={variable} />,
|
render: (descriptor) => <AllowCustomSwitch id={descriptor.props.id} variable={variable} />,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import { getIntervalVariableOptions, IntervalVariableEditor } from './editors/In
|
|||||||
import { getQueryVariableOptions, QueryVariableEditor } from './editors/QueryVariableEditor';
|
import { getQueryVariableOptions, QueryVariableEditor } from './editors/QueryVariableEditor';
|
||||||
import { getSwitchVariableOptions, SwitchVariableEditor } from './editors/SwitchVariableEditor';
|
import { getSwitchVariableOptions, SwitchVariableEditor } from './editors/SwitchVariableEditor';
|
||||||
import { TextBoxVariableEditor, getTextBoxVariableOptions } from './editors/TextBoxVariableEditor';
|
import { TextBoxVariableEditor, getTextBoxVariableOptions } from './editors/TextBoxVariableEditor';
|
||||||
|
import { getStackVariableOptions, StackVariableEditor } from './editors/StackVariableEditor';
|
||||||
|
import { isStackVariable, StackVariable } from './StackVariable';
|
||||||
|
|
||||||
interface EditableVariableConfig {
|
interface EditableVariableConfig {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -129,6 +131,12 @@ export const getEditableVariables: () => Record<EditableVariableType, EditableVa
|
|||||||
editor: SwitchVariableEditor,
|
editor: SwitchVariableEditor,
|
||||||
getOptions: getSwitchVariableOptions,
|
getOptions: getSwitchVariableOptions,
|
||||||
},
|
},
|
||||||
|
stack: {
|
||||||
|
name: "Stack",
|
||||||
|
description: "Users can define datasource stacks",
|
||||||
|
editor: StackVariableEditor,
|
||||||
|
getOptions: getStackVariableOptions,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export function getEditableVariableDefinition(type: string): EditableVariableConfig {
|
export function getEditableVariableDefinition(type: string): EditableVariableConfig {
|
||||||
@@ -152,6 +160,7 @@ export const EDITABLE_VARIABLES_SELECT_ORDER: EditableVariableType[] = [
|
|||||||
'adhoc',
|
'adhoc',
|
||||||
'switch',
|
'switch',
|
||||||
'groupby',
|
'groupby',
|
||||||
|
'stack'
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getVariableTypeSelectOptions(): Array<SelectableValue<EditableVariableType>> {
|
export function getVariableTypeSelectOptions(): Array<SelectableValue<EditableVariableType>> {
|
||||||
@@ -203,6 +212,8 @@ export function getVariableScene(type: EditableVariableType, initialState: Commo
|
|||||||
return new TextBoxVariable(initialState);
|
return new TextBoxVariable(initialState);
|
||||||
case 'switch':
|
case 'switch':
|
||||||
return new SwitchVariable(initialState);
|
return new SwitchVariable(initialState);
|
||||||
|
case 'stack':
|
||||||
|
return new StackVariable(initialState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,7 +290,8 @@ export function isSceneVariableInstance(sceneObject: SceneObject): sceneObject i
|
|||||||
sceneUtils.isQueryVariable(sceneObject) ||
|
sceneUtils.isQueryVariable(sceneObject) ||
|
||||||
sceneUtils.isTextBoxVariable(sceneObject) ||
|
sceneUtils.isTextBoxVariable(sceneObject) ||
|
||||||
sceneUtils.isGroupByVariable(sceneObject) ||
|
sceneUtils.isGroupByVariable(sceneObject) ||
|
||||||
sceneUtils.isSwitchVariable(sceneObject)
|
sceneUtils.isSwitchVariable(sceneObject) ||
|
||||||
|
isStackVariable(sceneObject)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export function getLocalVariableValueSet(
|
|||||||
name: variable.state.name,
|
name: variable.state.name,
|
||||||
value,
|
value,
|
||||||
text,
|
text,
|
||||||
|
properties: variable.state.options.find((o) => o.value === value)?.properties,
|
||||||
isMulti: variable.state.isMulti,
|
isMulti: variable.state.isMulti,
|
||||||
includeAll: variable.state.includeAll,
|
includeAll: variable.state.includeAll,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ describe('when creating variables objects', () => {
|
|||||||
text: 'a',
|
text: 'a',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
value: 'a',
|
value: 'a',
|
||||||
|
valuesFormat: 'csv',
|
||||||
hide: 0,
|
hide: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||||
|
|
||||||
import { SnapshotVariable } from '../serialization/custom-variables/SnapshotVariable';
|
import { SnapshotVariable } from '../serialization/custom-variables/SnapshotVariable';
|
||||||
|
import { StackVariable } from '../settings/variables/StackVariable';
|
||||||
|
|
||||||
|
|
||||||
import { getCurrentValueForOldIntervalModel, getIntervalsFromQueryString } from './utils';
|
import { getCurrentValueForOldIntervalModel, getIntervalsFromQueryString } from './utils';
|
||||||
|
|
||||||
@@ -311,6 +313,15 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
|
|||||||
skipUrlSync: variable.skipUrlSync,
|
skipUrlSync: variable.skipUrlSync,
|
||||||
hide: variable.hide,
|
hide: variable.hide,
|
||||||
});
|
});
|
||||||
|
} else if (variable.type === 'stack') {
|
||||||
|
return new StackVariable({
|
||||||
|
...commonProperties,
|
||||||
|
value: variable.current?.value ?? '',
|
||||||
|
text: variable.current?.text ?? '',
|
||||||
|
properties: (variable as any).properties || {},
|
||||||
|
skipUrlSync: variable.skipUrlSync,
|
||||||
|
hide: variable.hide,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Scenes: Unsupported variable type ${variable.type}`);
|
throw new Error(`Scenes: Unsupported variable type ${variable.type}`);
|
||||||
}
|
}
|
||||||
|
|||||||
89
public/app/features/datasources/api/stacksApi.ts
Normal file
89
public/app/features/datasources/api/stacksApi.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
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',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Singleton client instance
|
||||||
|
const datasourceStacksClient = new ScopedResourceClient<DataSourceStackSpec>(datasourceStacksGVR);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all datasource stacks from the API
|
||||||
|
*/
|
||||||
|
export async function fetchStacks(): Promise<Resource<DataSourceStackSpec>[]> {
|
||||||
|
const response: ResourceList<DataSourceStackSpec> = await datasourceStacksClient.list();
|
||||||
|
return response.items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a single datasource stack by name
|
||||||
|
*/
|
||||||
|
export async function fetchStack(name: string): Promise<Resource<DataSourceStackSpec>> {
|
||||||
|
return datasourceStacksClient.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new datasource stack
|
||||||
|
*/
|
||||||
|
export async function createStack(
|
||||||
|
name: string,
|
||||||
|
spec: DataSourceStackSpec
|
||||||
|
): Promise<Resource<DataSourceStackSpec>> {
|
||||||
|
return datasourceStacksClient.create({
|
||||||
|
metadata: { name },
|
||||||
|
spec,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an existing datasource stack
|
||||||
|
*/
|
||||||
|
export async function updateStack(
|
||||||
|
existingStack: Resource<DataSourceStackSpec>,
|
||||||
|
spec: DataSourceStackSpec
|
||||||
|
): Promise<Resource<DataSourceStackSpec>> {
|
||||||
|
return datasourceStacksClient.update({
|
||||||
|
...existingStack,
|
||||||
|
spec,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a datasource stack by name
|
||||||
|
*/
|
||||||
|
export async function deleteStack(name: string): Promise<void> {
|
||||||
|
await datasourceStacksClient.delete(name, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying client for advanced operations
|
||||||
|
*/
|
||||||
|
export function getStacksClient(): ScopedResourceClient<DataSourceStackSpec> {
|
||||||
|
return datasourceStacksClient;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { FormProvider, SubmitErrorHandler, useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router-dom-v5-compat';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { Trans } from '@grafana/i18n';
|
||||||
|
import { Button, Stack, useStyles2 } from '@grafana/ui';
|
||||||
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
|
import { ROUTES } from 'app/features/connections/constants';
|
||||||
|
import { DataSourceStackSpec, createStack, updateStack, fetchStack } from 'app/features/datasources/api/stacksApi';
|
||||||
|
|
||||||
|
import { StackModes } from './StackModes';
|
||||||
|
import { StackName } from './StackName';
|
||||||
|
import { StackTemplate } from './StackTemplate';
|
||||||
|
import { StackFormValues } from './types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
existing?: StackFormValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues: StackFormValues = {
|
||||||
|
name: '',
|
||||||
|
templates: [],
|
||||||
|
modes: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StackForm = ({ existing }: Props) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const notifyApp = useAppNotification();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const isEditing = !!existing;
|
||||||
|
|
||||||
|
const initialValues: StackFormValues = useMemo(() => {
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
return defaultValues;
|
||||||
|
}, [existing]);
|
||||||
|
|
||||||
|
const formAPI = useForm<StackFormValues>({
|
||||||
|
mode: 'onSubmit',
|
||||||
|
defaultValues: initialValues,
|
||||||
|
shouldFocusError: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
} = formAPI;
|
||||||
|
|
||||||
|
const submit = async (values: StackFormValues): Promise<void> => {
|
||||||
|
const spec = prepareCreateStackPayload(values);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEditing) {
|
||||||
|
// Update existing stack
|
||||||
|
const existingStack = await fetchStack(values.name);
|
||||||
|
await updateStack(existingStack, spec);
|
||||||
|
notifyApp.success('Stack updated successfully!');
|
||||||
|
} else {
|
||||||
|
// Create new stack
|
||||||
|
await createStack(values.name, spec);
|
||||||
|
notifyApp.success('Stack created successfully!');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to stacks list page
|
||||||
|
navigate(ROUTES.Stacks);
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'An unknown error occurred';
|
||||||
|
notifyApp.error(`Failed to save stack: ${message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInvalid: SubmitErrorHandler<StackFormValues> = () => {
|
||||||
|
notifyApp.error('There are errors in the form. Please correct them and try again!');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...formAPI}>
|
||||||
|
<form onSubmit={(e) => e.preventDefault()} className={styles.form}>
|
||||||
|
<div className={styles.contentOuter}>
|
||||||
|
<Stack direction="column" gap={3}>
|
||||||
|
{/* Step 1 - name */}
|
||||||
|
<StackName />
|
||||||
|
|
||||||
|
{/* Step 2 - Templates */}
|
||||||
|
<StackTemplate />
|
||||||
|
|
||||||
|
{/* Step 3 - Modes */}
|
||||||
|
<StackModes />
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<Stack direction="row" alignItems="center">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
type="button"
|
||||||
|
onClick={handleSubmit((values) => submit(values), onInvalid)}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
icon={isSubmitting ? 'spinner' : undefined}
|
||||||
|
>
|
||||||
|
<Trans i18nKey="datasources.stack-form.save">Save</Trans>
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
form: css({
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}),
|
||||||
|
contentOuter: css({
|
||||||
|
background: theme.colors.background.primary,
|
||||||
|
overflow: 'hidden',
|
||||||
|
maxWidth: theme.breakpoints.values.xl,
|
||||||
|
flex: 1,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const prepareCreateStackPayload = (formValues: StackFormValues): DataSourceStackSpec => {
|
||||||
|
// creates a mapping from template name to UUID
|
||||||
|
const templateNameToUuid: Record<string, string> = {};
|
||||||
|
formValues.templates.forEach((template) => {
|
||||||
|
templateNameToUuid[template.name] = uuidv4();
|
||||||
|
});
|
||||||
|
|
||||||
|
// builds the template record with UUIDs as keys
|
||||||
|
const template: DataSourceStackSpec['template'] = {};
|
||||||
|
formValues.templates.forEach((t) => {
|
||||||
|
const uuid = templateNameToUuid[t.name];
|
||||||
|
template[uuid] = {
|
||||||
|
group: t.type,
|
||||||
|
name: t.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// uses template ids to build modes
|
||||||
|
const modes: DataSourceStackSpec['modes'] = formValues.modes.map((mode) => {
|
||||||
|
const definition: Record<string, { dataSourceRef: string }> = {};
|
||||||
|
|
||||||
|
Object.entries(mode.datasources).forEach(([templateName, dataSourceUid]) => {
|
||||||
|
const uuid = templateNameToUuid[templateName];
|
||||||
|
if (uuid) {
|
||||||
|
definition[uuid] = { dataSourceRef: dataSourceUid };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: mode.name,
|
||||||
|
uid: uuidv4(),
|
||||||
|
definition,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return { template, modes };
|
||||||
|
};
|
||||||
|
|
||||||
|
//used when loading an existing stack for editing.
|
||||||
|
export const transformStackSpecToFormValues = (stackName: string, spec: DataSourceStackSpec): StackFormValues => {
|
||||||
|
const uuidToTemplateName: Record<string, string> = {};
|
||||||
|
Object.entries(spec.template).forEach(([uuid, templateItem]) => {
|
||||||
|
uuidToTemplateName[uuid] = templateItem.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
const templates = Object.values(spec.template).map((templateItem) => ({
|
||||||
|
name: templateItem.name,
|
||||||
|
type: templateItem.group,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const modes = spec.modes.map((mode) => {
|
||||||
|
const datasources: Record<string, string> = {};
|
||||||
|
|
||||||
|
Object.entries(mode.definition).forEach(([uuid, modeItem]) => {
|
||||||
|
const templateName = uuidToTemplateName[uuid];
|
||||||
|
if (templateName) {
|
||||||
|
datasources[templateName] = modeItem.dataSourceRef;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: mode.name,
|
||||||
|
datasources,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: stackName,
|
||||||
|
templates,
|
||||||
|
modes,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ReactElement } from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { FieldSet, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
export interface StackFormSectionProps {
|
||||||
|
title: string;
|
||||||
|
stepNo: number;
|
||||||
|
description?: string | ReactElement;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StackFormSection = ({
|
||||||
|
title,
|
||||||
|
stepNo,
|
||||||
|
children,
|
||||||
|
fullWidth = false,
|
||||||
|
description,
|
||||||
|
}: React.PropsWithChildren<StackFormSectionProps>) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.parent}>
|
||||||
|
<FieldSet
|
||||||
|
className={cx(fullWidth && styles.fullWidth)}
|
||||||
|
label={
|
||||||
|
<Stack direction="row" alignItems="center" justifyContent="space-between">
|
||||||
|
<Text variant="h3">
|
||||||
|
{stepNo}. {title}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack direction="column">
|
||||||
|
{description && <div className={styles.description}>{description}</div>}
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</FieldSet>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
parent: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
border: `solid 1px ${theme.colors.border.weak}`,
|
||||||
|
borderRadius: theme.shape.radius.lg,
|
||||||
|
padding: `${theme.spacing(2)} ${theme.spacing(3)}`,
|
||||||
|
}),
|
||||||
|
description: css({
|
||||||
|
marginTop: `-${theme.spacing(2)}`,
|
||||||
|
}),
|
||||||
|
fullWidth: css({
|
||||||
|
width: '100%',
|
||||||
|
}),
|
||||||
|
reverse: css({
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Trans, t } from '@grafana/i18n';
|
||||||
|
import { Button, Field, IconButton, Input, Stack, Text } from '@grafana/ui';
|
||||||
|
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
|
||||||
|
|
||||||
|
import { StackFormSection } from './StackFormSection';
|
||||||
|
import { ModeSection, StackFormValues } from './types';
|
||||||
|
|
||||||
|
const createEmptyMode = (): ModeSection => ({
|
||||||
|
name: '',
|
||||||
|
datasources: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StackModes = () => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
register,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<StackFormValues>();
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'modes',
|
||||||
|
});
|
||||||
|
|
||||||
|
const templates = watch('templates');
|
||||||
|
|
||||||
|
const hasTemplates = templates && templates.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StackFormSection
|
||||||
|
stepNo={3}
|
||||||
|
title={t('datasources.stack-modes.title', 'Add modes')}
|
||||||
|
description={
|
||||||
|
<Text variant="bodySmall" color="secondary">
|
||||||
|
<Trans i18nKey="datasources.stack-modes.description">
|
||||||
|
Define modes (e.g., dev, staging, prod) and select the actual datasources for each template entry.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack direction="column" gap={3}>
|
||||||
|
{!hasTemplates && (
|
||||||
|
<Text color="secondary" italic>
|
||||||
|
<Trans i18nKey="datasources.stack-modes.no-templates">
|
||||||
|
Add template sections first to define modes.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasTemplates &&
|
||||||
|
fields.map((field, index) => (
|
||||||
|
<ModeSectionRow
|
||||||
|
key={field.id}
|
||||||
|
index={index}
|
||||||
|
register={register}
|
||||||
|
control={control}
|
||||||
|
errors={errors}
|
||||||
|
templates={templates}
|
||||||
|
onRemove={() => remove(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{hasTemplates && (
|
||||||
|
<Button type="button" variant="secondary" icon="plus" onClick={() => append(createEmptyMode())}>
|
||||||
|
<Trans i18nKey="datasources.stack-modes.add-mode">Add mode</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</StackFormSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ModeSectionRowProps {
|
||||||
|
index: number;
|
||||||
|
register: ReturnType<typeof useFormContext<StackFormValues>>['register'];
|
||||||
|
control: ReturnType<typeof useFormContext<StackFormValues>>['control'];
|
||||||
|
errors: ReturnType<typeof useFormContext<StackFormValues>>['formState']['errors'];
|
||||||
|
templates: StackFormValues['templates'];
|
||||||
|
onRemove: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModeSectionRow = ({ index, register, control, errors, templates, onRemove }: ModeSectionRowProps) => {
|
||||||
|
return (
|
||||||
|
<Stack direction="column" gap={2}>
|
||||||
|
<Stack direction="row" gap={2} alignItems="center">
|
||||||
|
<Field
|
||||||
|
noMargin
|
||||||
|
label={t('datasources.stack-modes.mode-name-label', 'Mode name')}
|
||||||
|
error={errors?.modes?.[index]?.name?.message}
|
||||||
|
invalid={!!errors?.modes?.[index]?.name?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id={`modes.${index}.name`}
|
||||||
|
width={30}
|
||||||
|
{...register(`modes.${index}.name`, {
|
||||||
|
required: {
|
||||||
|
value: true,
|
||||||
|
message: t('datasources.stack-modes.mode-name-required', 'Mode name is required'),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
placeholder={t('datasources.stack-modes.mode-name-placeholder', 'e.g. production')}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name="trash-alt"
|
||||||
|
variant="destructive"
|
||||||
|
tooltip={t('datasources.stack-modes.remove-mode', 'Remove mode')}
|
||||||
|
onClick={onRemove}
|
||||||
|
aria-label={t('datasources.stack-modes.remove-mode', 'Remove mode')}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack direction="row" gap={2} wrap="wrap">
|
||||||
|
{templates.map((template) => (
|
||||||
|
<Field
|
||||||
|
noMargin
|
||||||
|
key={template.name}
|
||||||
|
label={template.name || t('datasources.stack-modes.unnamed-template', 'Unnamed template')}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name={`modes.${index}.datasources.${template.name}`}
|
||||||
|
control={control}
|
||||||
|
render={({ field: { ref, onChange, value, ...field } }) => (
|
||||||
|
<DataSourcePicker
|
||||||
|
{...field}
|
||||||
|
current={value}
|
||||||
|
onChange={(ds) => onChange(ds.uid)}
|
||||||
|
noDefault={true}
|
||||||
|
pluginId={template.type}
|
||||||
|
placeholder={t('datasources.stack-modes.select-datasource', 'Select datasource')}
|
||||||
|
width={30}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { Trans, t } from '@grafana/i18n';
|
||||||
|
import { Field, Input, Stack, Text } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { StackFormSection } from './StackFormSection';
|
||||||
|
import { StackFormValues } from './types';
|
||||||
|
|
||||||
|
export const StackName = () => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<StackFormValues>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StackFormSection
|
||||||
|
stepNo={1}
|
||||||
|
title={t('datasources.stack-name.title', 'Enter stack name')}
|
||||||
|
description={
|
||||||
|
<Text variant="bodySmall" color="secondary">
|
||||||
|
<Trans i18nKey="datasources.stack-name.description">Enter a name to identify your stack.</Trans>
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack direction="column">
|
||||||
|
<Field
|
||||||
|
label={t('datasources.stack-name.label', 'Name')}
|
||||||
|
error={errors?.name?.message}
|
||||||
|
invalid={!!errors.name?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
data-testid={selectors.components.AlertRules.ruleNameField}
|
||||||
|
id="name"
|
||||||
|
width={38}
|
||||||
|
{...register('name', {
|
||||||
|
required: {
|
||||||
|
value: true,
|
||||||
|
message: t('datasources.stack-name.required', 'Must enter a name'),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
aria-label={t('datasources.stack-name.aria-label', 'name')}
|
||||||
|
placeholder="example: LGTM"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Stack>
|
||||||
|
</StackFormSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { Trans, t } from '@grafana/i18n';
|
||||||
|
import { Button, Combobox, Field, IconButton, Input, Stack, Text } from '@grafana/ui';
|
||||||
|
import { getOptionDataSourceTypes } from 'app/features/dashboard-scene/settings/variables/utils';
|
||||||
|
|
||||||
|
import { StackFormSection } from './StackFormSection';
|
||||||
|
import { StackFormValues, TemplateSection } from './types';
|
||||||
|
|
||||||
|
const emptyTemplateSection: TemplateSection = {
|
||||||
|
name: '',
|
||||||
|
type: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StackTemplate = () => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
} = useFormContext<StackFormValues>();
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control,
|
||||||
|
name: 'templates',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StackFormSection
|
||||||
|
stepNo={2}
|
||||||
|
title={t('datasources.stack-template.title', 'Add template sections')}
|
||||||
|
description={
|
||||||
|
<Text variant="bodySmall" color="secondary">
|
||||||
|
<Trans i18nKey="datasources.stack-template.description">
|
||||||
|
Add which datasource types comprise your stack and add names to reference them in the query editor.
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack direction="column" gap={2}>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<TemplateSectionRow
|
||||||
|
key={field.id}
|
||||||
|
index={index}
|
||||||
|
register={register}
|
||||||
|
control={control}
|
||||||
|
errors={errors}
|
||||||
|
onRemove={() => remove(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button type="button" variant="secondary" icon="plus" onClick={() => append(emptyTemplateSection)}>
|
||||||
|
<Trans i18nKey="datasources.stack-template.add-section">Add datasource</Trans>
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</StackFormSection>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TemplateSectionRowProps {
|
||||||
|
index: number;
|
||||||
|
register: ReturnType<typeof useFormContext<StackFormValues>>['register'];
|
||||||
|
control: ReturnType<typeof useFormContext<StackFormValues>>['control'];
|
||||||
|
errors: ReturnType<typeof useFormContext<StackFormValues>>['formState']['errors'];
|
||||||
|
onRemove: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TemplateSectionRow = ({ index, register, control, errors, onRemove }: TemplateSectionRowProps) => {
|
||||||
|
const dataSourceOptions = getOptionDataSourceTypes();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="row" gap={2} alignItems="flex-start">
|
||||||
|
<Field
|
||||||
|
noMargin
|
||||||
|
label={t('datasources.stack-template.name-label', 'Name')}
|
||||||
|
error={errors?.templates?.[index]?.name?.message}
|
||||||
|
invalid={!!errors?.templates?.[index]?.name?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id={`templates.${index}.name`}
|
||||||
|
width={30}
|
||||||
|
{...register(`templates.${index}.name`, {
|
||||||
|
required: {
|
||||||
|
value: true,
|
||||||
|
message: t('datasources.stack-template.name-required', 'Name is required'),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
placeholder={t('datasources.stack-template.name-placeholder', 'e.g. logs-datasource')}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
noMargin
|
||||||
|
label={t('datasources.stack-template.type-label', 'Data source type')}
|
||||||
|
error={errors?.templates?.[index]?.type?.message}
|
||||||
|
invalid={!!errors?.templates?.[index]?.type?.message}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name={`templates.${index}.type`}
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
required: {
|
||||||
|
value: true,
|
||||||
|
message: t('datasources.stack-template.type-required', 'Type is required'),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
render={({ field: { ref, onChange, ...field } }) => (
|
||||||
|
<Combobox
|
||||||
|
id={`templates.${index}.type`}
|
||||||
|
width={30}
|
||||||
|
options={dataSourceOptions}
|
||||||
|
onChange={(option) => onChange(option?.value || '')}
|
||||||
|
placeholder={t('datasources.stack-template.type-placeholder', 'Select type')}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name="trash-alt"
|
||||||
|
variant="destructive"
|
||||||
|
tooltip={t('datasources.stack-template.remove-section', 'Remove section')}
|
||||||
|
onClick={onRemove}
|
||||||
|
aria-label={t('datasources.stack-template.remove-section', 'Remove section')}
|
||||||
|
style={{ marginTop: '28px' }}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export interface TemplateSection {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModeSection {
|
||||||
|
name: string;
|
||||||
|
/** template name to selected datasource UID */
|
||||||
|
datasources: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StackFormValues {
|
||||||
|
name: string;
|
||||||
|
templates: TemplateSection[];
|
||||||
|
modes: ModeSection[];
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
||||||
import {
|
import {
|
||||||
AppEvents,
|
AppEvents,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
@@ -29,6 +30,8 @@ import {
|
|||||||
} from 'app/features/expressions/ExpressionDatasource';
|
} from 'app/features/expressions/ExpressionDatasource';
|
||||||
import { ExpressionDatasourceUID } from 'app/features/expressions/types';
|
import { ExpressionDatasourceUID } from 'app/features/expressions/types';
|
||||||
|
|
||||||
|
import { StackVariableValue } from '../../../../packages/grafana-data/src/types/templateVars';
|
||||||
|
|
||||||
import { pluginImporter } from './importer/pluginImporter';
|
import { pluginImporter } from './importer/pluginImporter';
|
||||||
|
|
||||||
export class DatasourceSrv implements DataSourceService {
|
export class DatasourceSrv implements DataSourceService {
|
||||||
@@ -306,27 +309,41 @@ export class DatasourceSrv implements DataSourceService {
|
|||||||
|
|
||||||
if (filters.variables) {
|
if (filters.variables) {
|
||||||
for (const variable of this.templateSrv.getVariables()) {
|
for (const variable of this.templateSrv.getVariables()) {
|
||||||
if (variable.type !== 'datasource') {
|
if (variable.type === 'datasource') {
|
||||||
continue;
|
let dsValue = variable.current.value === 'default' ? this.defaultName : variable.current.value;
|
||||||
}
|
// Support for multi-value DataSource (ds) variables
|
||||||
let dsValue = variable.current.value === 'default' ? this.defaultName : variable.current.value;
|
if (Array.isArray(dsValue)) {
|
||||||
// Support for multi-value DataSource (ds) variables
|
// If the ds variable have multiple selected datasources
|
||||||
if (Array.isArray(dsValue)) {
|
// We will use the first one
|
||||||
// If the ds variable have multiple selected datasources
|
dsValue = dsValue[0];
|
||||||
// We will use the first one
|
}
|
||||||
dsValue = dsValue[0];
|
const dsSettings =
|
||||||
}
|
!Array.isArray(dsValue) && (this.settingsMapByName[dsValue] || this.settingsMapByUid[dsValue]);
|
||||||
const dsSettings =
|
|
||||||
!Array.isArray(dsValue) && (this.settingsMapByName[dsValue] || this.settingsMapByUid[dsValue]);
|
|
||||||
|
|
||||||
if (dsSettings) {
|
if (dsSettings) {
|
||||||
const key = `$\{${variable.name}\}`;
|
const key = `$\{${variable.name}\}`;
|
||||||
base.push({
|
base.push({
|
||||||
...dsSettings,
|
...dsSettings,
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
name: key,
|
name: key,
|
||||||
uid: key,
|
uid: key,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (variable.type === 'stack') {
|
||||||
|
const properties = variable.properties;
|
||||||
|
for (const key in properties) {
|
||||||
|
const dsSettings = this.settingsMapByUid[properties[key]];
|
||||||
|
|
||||||
|
if (dsSettings) {
|
||||||
|
base.push({
|
||||||
|
...dsSettings,
|
||||||
|
isDefault: false,
|
||||||
|
uid: `$\{${variable.name}.${key}\}`,
|
||||||
|
name: `$\{${variable.name}.${key}\}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,6 +179,7 @@ describe('type guards', () => {
|
|||||||
custom: { variable: createCustomVariable(), isMulti: true, hasOptions: true, hasCurrent: true },
|
custom: { variable: createCustomVariable(), isMulti: true, hasOptions: true, hasCurrent: true },
|
||||||
snapshot: { variable: createSnapshotVariable(), isMulti: false, hasOptions: true, hasCurrent: true },
|
snapshot: { variable: createSnapshotVariable(), isMulti: false, hasOptions: true, hasCurrent: true },
|
||||||
switch: { variable: createSwitchVariable(), isMulti: false, hasOptions: true, hasCurrent: true },
|
switch: { variable: createSwitchVariable(), isMulti: false, hasOptions: true, hasCurrent: true },
|
||||||
|
stack: { variable: createCustomVariable(), isMulti: true, hasOptions: true, hasCurrent: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
const variableFacts = Object.values(variableFactsObj);
|
const variableFacts = Object.values(variableFactsObj);
|
||||||
|
|||||||
@@ -5880,6 +5880,8 @@
|
|||||||
},
|
},
|
||||||
"custom-variable-form": {
|
"custom-variable-form": {
|
||||||
"custom-options": "Custom options",
|
"custom-options": "Custom options",
|
||||||
|
"json-values-tooltip": "Provide a JSON representing an array of objects, where each object can have any number of properties.<br/>Check <4>our docs</4> for more information.",
|
||||||
|
"name-json-values": "Object values in a JSON array",
|
||||||
"name-values-separated-comma": "Values separated by comma",
|
"name-values-separated-comma": "Values separated by comma",
|
||||||
"selection-options": "Selection options"
|
"selection-options": "Selection options"
|
||||||
},
|
},
|
||||||
|
|||||||
22
yarn.lock
22
yarn.lock
@@ -3603,11 +3603,11 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@grafana/scenes-react@npm:6.47.1":
|
"@grafana/scenes-react@npm:^6.48.0":
|
||||||
version: 6.47.1
|
version: 6.48.0
|
||||||
resolution: "@grafana/scenes-react@npm:6.47.1"
|
resolution: "@grafana/scenes-react@npm:6.48.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@grafana/scenes": "npm:6.47.1"
|
"@grafana/scenes": "npm:6.48.0"
|
||||||
lru-cache: "npm:^10.2.2"
|
lru-cache: "npm:^10.2.2"
|
||||||
react-use: "npm:^17.4.0"
|
react-use: "npm:^17.4.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3619,7 +3619,7 @@ __metadata:
|
|||||||
react: ^18.0.0
|
react: ^18.0.0
|
||||||
react-dom: ^18.0.0
|
react-dom: ^18.0.0
|
||||||
react-router-dom: ^6.28.0
|
react-router-dom: ^6.28.0
|
||||||
checksum: 10/dc20f9ee80eaf648665f7449e3ccb3b640a931f8f4a1be89599dce17eb0f52e763e3a603a4d491d9886b3e6cdf2ad3634124252c223315917206200f7cd6da16
|
checksum: 10/5afb2aa79271dd824cc35f0a59ec193ddcbd4e1f14e756551228ce218a19faad54923d3a83f8bbb10d38c1d23c49846df29e7fce8feba8ec9aec2d32d9c1cf8d
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -3649,9 +3649,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@grafana/scenes@npm:6.47.1":
|
"@grafana/scenes@npm:6.48.0, @grafana/scenes@npm:^6.48.0":
|
||||||
version: 6.47.1
|
version: 6.48.0
|
||||||
resolution: "@grafana/scenes@npm:6.47.1"
|
resolution: "@grafana/scenes@npm:6.48.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@floating-ui/react": "npm:^0.26.16"
|
"@floating-ui/react": "npm:^0.26.16"
|
||||||
"@leeoniya/ufuzzy": "npm:^1.0.16"
|
"@leeoniya/ufuzzy": "npm:^1.0.16"
|
||||||
@@ -3671,7 +3671,7 @@ __metadata:
|
|||||||
react: ^18.0.0
|
react: ^18.0.0
|
||||||
react-dom: ^18.0.0
|
react-dom: ^18.0.0
|
||||||
react-router-dom: ^6.28.0
|
react-router-dom: ^6.28.0
|
||||||
checksum: 10/bc0c76258955058e7493b04e7cdd5d59dcc4159adf06da0837e992716ea15700b54f8403614df04326350363dc3344fb2602a2e8f7807724571659b4bd95aded
|
checksum: 10/28cd64ea3c4faf87173ea71ffc136a7a525c33ec2e263ab2a98df718e3968ed7b7a12ecf0f309400af78e3c3269ae6048da8113412645a361a3e4925f9a2a810
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -19234,8 +19234,8 @@ __metadata:
|
|||||||
"@grafana/plugin-ui": "npm:^0.11.1"
|
"@grafana/plugin-ui": "npm:^0.11.1"
|
||||||
"@grafana/prometheus": "workspace:*"
|
"@grafana/prometheus": "workspace:*"
|
||||||
"@grafana/runtime": "workspace:*"
|
"@grafana/runtime": "workspace:*"
|
||||||
"@grafana/scenes": "npm:6.47.1"
|
"@grafana/scenes": "npm:^6.48.0"
|
||||||
"@grafana/scenes-react": "npm:6.47.1"
|
"@grafana/scenes-react": "npm:^6.48.0"
|
||||||
"@grafana/schema": "workspace:*"
|
"@grafana/schema": "workspace:*"
|
||||||
"@grafana/sql": "workspace:*"
|
"@grafana/sql": "workspace:*"
|
||||||
"@grafana/test-utils": "workspace:*"
|
"@grafana/test-utils": "workspace:*"
|
||||||
|
|||||||
Reference in New Issue
Block a user