mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Kindsys: Unify plugins, pfs with kind framework (#61192)
* New pfs impl * Reached codegen parity with old system * Update all models.cue inputs * Rename all models.cue files * Remove unused prefixfs * Changes Queries->DataQuery schema interface * Recodegen * All tests passing, nearly good now * Add SchemaInterface to kindsys props * Add pascal name deriver * Relocate plugin cue files again * Clarify use of injected fields * Remove unnecessary aliasing * Move DataQuery into mudball * Allow forcing ExpandReferences on go type generation * Move DataQuery def into kindsys, add generator to copy it to common * Fix copy generator to replace package name correctly * Fix duplicate type, test failure * Fix linting issues
This commit is contained in:
parent
6a7cbeae6c
commit
3b3059c9ce
2
embed.go
2
embed.go
@ -6,5 +6,5 @@ import (
|
||||
|
||||
// CueSchemaFS embeds all schema-related CUE files in the Grafana project.
|
||||
//
|
||||
//go:embed cue.mod/module.cue kinds/*.cue kinds/*/*.cue packages/grafana-schema/src/common/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json pkg/kindsys/*.cue pkg/plugins/plugindef/*.cue
|
||||
//go:embed cue.mod/module.cue kinds/*.cue kinds/*/*.cue packages/grafana-schema/src/common/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json pkg/kindsys/*.cue pkg/plugins/*/*.cue
|
||||
var CueSchemaFS embed.FS
|
||||
|
@ -151,7 +151,7 @@ lineage: seqs: [
|
||||
|
||||
// Specific datasource instance
|
||||
uid?: string @grafanamaturity(NeedsExpertReview)
|
||||
} @cuetsy(kind="interface") @grafanamaturity(NeedsExpertReview)
|
||||
} @cuetsy(kind="interface") @grafana(TSVeneer="type") @grafanamaturity(NeedsExpertReview)
|
||||
|
||||
// FROM public/app/features/dashboard/state/DashboardModels.ts - ish
|
||||
// TODO docs
|
||||
|
55
kinds/gen.go
55
kinds/gen.go
@ -11,12 +11,13 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"cuelang.org/go/cue/errors"
|
||||
"github.com/grafana/codejen"
|
||||
"github.com/grafana/cuetsy"
|
||||
|
||||
"github.com/grafana/grafana/pkg/codegen"
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
@ -83,11 +84,10 @@ func main() {
|
||||
if err != nil {
|
||||
die(fmt.Errorf("core kinddirs codegen failed: %w", err))
|
||||
}
|
||||
sharedf, err := dummyCommonJenny{}.Generate(nil)
|
||||
if err != nil {
|
||||
die(fmt.Errorf("common schemas failed"))
|
||||
}
|
||||
if err = jfs.Add(elsedie(header(*sharedf))("couldn't inject header")); err != nil {
|
||||
|
||||
commfsys := elsedie(genCommon(filepath.Join(groot, "pkg", "kindsys")))("common schemas failed")
|
||||
commfsys = elsedie(commfsys.Map(header))("failed gen header on common fsys")
|
||||
if err = jfs.Merge(commfsys); err != nil {
|
||||
die(err)
|
||||
}
|
||||
|
||||
@ -116,25 +116,48 @@ func nameFor(m kindsys.SomeKindProperties) string {
|
||||
|
||||
type dummyCommonJenny struct{}
|
||||
|
||||
func (j dummyCommonJenny) JennyName() string {
|
||||
return "CommonSchemaJenny"
|
||||
}
|
||||
func genCommon(kp string) (*codejen.FS, error) {
|
||||
fsys := codejen.NewFS()
|
||||
|
||||
func (j dummyCommonJenny) Generate(dummy any) (*codejen.File, error) {
|
||||
// kp := filepath.Join("pkg", "kindsys")
|
||||
path := filepath.Join("packages", "grafana-schema", "src", "common")
|
||||
// Grab all the common_* files from kindsys and load them in
|
||||
dfsys := os.DirFS(kp)
|
||||
matches := elsedie(fs.Glob(dfsys, "common_*.cue"))("could not glob kindsys cue files")
|
||||
for _, fname := range matches {
|
||||
fpath := filepath.Join(path, strings.TrimPrefix(fname, "common_"))
|
||||
fpath = fpath[:len(fpath)-4] + "_gen.cue"
|
||||
data := elsedie(fs.ReadFile(dfsys, fname))("error reading " + fname)
|
||||
_ = fsys.Add(*codejen.NewFile(fpath, data, dummyCommonJenny{}))
|
||||
}
|
||||
fsys = elsedie(fsys.Map(packageMapper))("failed remapping fs")
|
||||
|
||||
v, err := cuectx.BuildGrafanaInstance(nil, path, "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := cuetsy.Generate(v, cuetsy.Config{
|
||||
b := elsedie(cuetsy.Generate(v, cuetsy.Config{
|
||||
Export: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate TS: %w", err)
|
||||
}
|
||||
}))("failed to generate common schema TS")
|
||||
|
||||
return codejen.NewFile(filepath.Join(path, "common.gen.ts"), b, dummyCommonJenny{}), nil
|
||||
_ = fsys.Add(*codejen.NewFile(filepath.Join(path, "common.gen.ts"), b, dummyCommonJenny{}))
|
||||
return fsys, nil
|
||||
}
|
||||
|
||||
func (j dummyCommonJenny) JennyName() string {
|
||||
return "CommonSchemaJenny"
|
||||
}
|
||||
|
||||
func (j dummyCommonJenny) Generate(dummy any) ([]codejen.File, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var pkgReplace = regexp.MustCompile("^package kindsys")
|
||||
|
||||
func packageMapper(f codejen.File) (codejen.File, error) {
|
||||
f.Data = pkgReplace.ReplaceAllLiteral(f.Data, []byte("package common"))
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func elsedie[T any](t T, err error) func(msg string) T {
|
||||
|
@ -1,3 +1,15 @@
|
||||
import { DataQuery as SchemaDataQuery, DataSourceRef as SchemaDataSourceRef } from '@grafana/schema';
|
||||
|
||||
/**
|
||||
* @deprecated use the type from @grafana/schema
|
||||
*/
|
||||
export interface DataQuery extends SchemaDataQuery {}
|
||||
|
||||
/**
|
||||
* @deprecated use the type from @grafana/schema
|
||||
*/
|
||||
export interface DataSourceRef extends SchemaDataSourceRef {}
|
||||
|
||||
/**
|
||||
* Attached to query results (not persisted)
|
||||
*
|
||||
@ -7,57 +19,11 @@ export enum DataTopic {
|
||||
Annotations = 'annotations',
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface DataSourceRef {
|
||||
/** The plugin type-id */
|
||||
type?: string;
|
||||
|
||||
/** Specific datasource instance */
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* These are the common properties available to all queries in all datasources
|
||||
* Specific implementations will *extend* this interface adding the required properties
|
||||
* for the given context
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface DataQuery {
|
||||
/**
|
||||
* A - Z
|
||||
*/
|
||||
refId: string;
|
||||
|
||||
/**
|
||||
* true if query is disabled (ie should not be returned to the dashboard)
|
||||
*/
|
||||
hide?: boolean;
|
||||
|
||||
/**
|
||||
* Unique, guid like, string used in explore mode
|
||||
*/
|
||||
key?: string;
|
||||
|
||||
/**
|
||||
* Specify the query flavor
|
||||
*/
|
||||
queryType?: string;
|
||||
|
||||
/**
|
||||
* For mixed data sources the selected datasource is on the query level.
|
||||
* For non mixed scenarios this is undefined.
|
||||
*/
|
||||
datasource?: DataSourceRef | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract representation of any label-based query
|
||||
* @internal
|
||||
*/
|
||||
export interface AbstractQuery extends DataQuery {
|
||||
export interface AbstractQuery extends SchemaDataQuery {
|
||||
labelMatchers: AbstractLabelMatcher[];
|
||||
}
|
||||
|
||||
@ -83,21 +49,21 @@ export type AbstractLabelMatcher = {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface DataSourceWithQueryImportSupport<TQuery extends DataQuery> {
|
||||
export interface DataSourceWithQueryImportSupport<TQuery extends SchemaDataQuery> {
|
||||
importFromAbstractQueries(labelBasedQuery: AbstractQuery[]): Promise<TQuery[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export interface DataSourceWithQueryExportSupport<TQuery extends DataQuery> {
|
||||
export interface DataSourceWithQueryExportSupport<TQuery extends SchemaDataQuery> {
|
||||
exportToAbstractQueries(query: TQuery[]): Promise<AbstractQuery[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const hasQueryImportSupport = <TQuery extends DataQuery>(
|
||||
export const hasQueryImportSupport = <TQuery extends SchemaDataQuery>(
|
||||
datasource: unknown
|
||||
): datasource is DataSourceWithQueryImportSupport<TQuery> => {
|
||||
return (datasource as DataSourceWithQueryImportSupport<TQuery>).importFromAbstractQueries !== undefined;
|
||||
@ -106,7 +72,7 @@ export const hasQueryImportSupport = <TQuery extends DataQuery>(
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export const hasQueryExportSupport = <TQuery extends DataQuery>(
|
||||
export const hasQueryExportSupport = <TQuery extends SchemaDataQuery>(
|
||||
datasource: unknown
|
||||
): datasource is DataSourceWithQueryExportSupport<TQuery> => {
|
||||
return (datasource as DataSourceWithQueryExportSupport<TQuery>).exportToAbstractQueries !== undefined;
|
||||
|
@ -8,6 +8,38 @@
|
||||
// Run 'make gen-cue' from repository root to regenerate.
|
||||
|
||||
|
||||
/**
|
||||
* These are the common properties available to all queries in all datasources.
|
||||
* Specific implementations will *extend* this interface, adding the required
|
||||
* properties for the given context.
|
||||
*/
|
||||
export interface DataQuery {
|
||||
/**
|
||||
* For mixed data sources the selected datasource is on the query level.
|
||||
* For non mixed scenarios this is undefined.
|
||||
* TODO find a better way to do this ^ that's friendly to schema
|
||||
* TODO this shouldn't be unknown but DataSourceRef | null
|
||||
*/
|
||||
datasource?: unknown;
|
||||
/**
|
||||
* true if query is disabled (ie should not be returned to the dashboard)
|
||||
*/
|
||||
hide?: boolean;
|
||||
/**
|
||||
* Unique, guid like, string used in explore mode
|
||||
*/
|
||||
key?: string;
|
||||
/**
|
||||
* Specify the query flavor
|
||||
* TODO make this required and give it a default
|
||||
*/
|
||||
queryType?: string;
|
||||
/**
|
||||
* A - Z
|
||||
*/
|
||||
refId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO docs
|
||||
*/
|
||||
@ -552,6 +584,17 @@ export type TimeZoneUtc = 'utc';
|
||||
*/
|
||||
export type TimeZoneBrowser = 'browser';
|
||||
|
||||
export interface DataSourceRef {
|
||||
/**
|
||||
* The plugin type-id
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* Specific datasource instance
|
||||
*/
|
||||
uid?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO docs
|
||||
*/
|
||||
|
46
packages/grafana-schema/src/common/dataquery_gen.cue
Normal file
46
packages/grafana-schema/src/common/dataquery_gen.cue
Normal file
@ -0,0 +1,46 @@
|
||||
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
|
||||
//
|
||||
// Generated by:
|
||||
// kinds/gen.go
|
||||
// Using jennies:
|
||||
// CommonSchemaJenny
|
||||
//
|
||||
// Run 'make gen-cue' from repository root to regenerate.
|
||||
|
||||
package common
|
||||
|
||||
// Canonically defined in pkg/kindsys/dataquery.cue FOR NOW to avoid having any external imports
|
||||
// in kindsys. Code generation copies this file to the common schemas in packages/grafana-schema/src/common.
|
||||
//
|
||||
// NOTE make gen-cue must be run twice when updating this file
|
||||
|
||||
// These are the common properties available to all queries in all datasources.
|
||||
// Specific implementations will *extend* this interface, adding the required
|
||||
// properties for the given context.
|
||||
DataQuery: {
|
||||
// A - Z
|
||||
refId: string
|
||||
|
||||
// true if query is disabled (ie should not be returned to the dashboard)
|
||||
hide?: bool
|
||||
|
||||
// Unique, guid like, string used in explore mode
|
||||
key?: string
|
||||
|
||||
// Specify the query flavor
|
||||
// TODO make this required and give it a default
|
||||
queryType?: string
|
||||
|
||||
// For mixed data sources the selected datasource is on the query level.
|
||||
// For non mixed scenarios this is undefined.
|
||||
// TODO find a better way to do this ^ that's friendly to schema
|
||||
// TODO this shouldn't be unknown but DataSourceRef | null
|
||||
datasource?: _
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
DataSourceRef: {
|
||||
// The plugin type-id
|
||||
type?: string
|
||||
// Specific datasource instance
|
||||
uid?: string
|
||||
} @cuetsy(kind="interface")
|
@ -246,3 +246,5 @@ VizTooltipOptions: {
|
||||
mode: TooltipDisplayMode
|
||||
sort: SortOrder
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
|
||||
|
@ -11,7 +11,6 @@
|
||||
export type {
|
||||
AnnotationTarget,
|
||||
AnnotationQuery,
|
||||
DataSourceRef,
|
||||
DashboardLink,
|
||||
DashboardLinkType,
|
||||
VariableType,
|
||||
@ -63,6 +62,7 @@ export {
|
||||
export type {
|
||||
Dashboard,
|
||||
VariableModel,
|
||||
DataSourceRef,
|
||||
Panel,
|
||||
FieldConfigSource,
|
||||
FieldConfig
|
||||
|
@ -3,5 +3,5 @@
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
export * from './common/common.gen';
|
||||
export * from './veneer/common.types';
|
||||
export * from './index.gen';
|
||||
|
8
packages/grafana-schema/src/veneer/common.types.ts
Normal file
8
packages/grafana-schema/src/veneer/common.types.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import * as raw from '../common/common.gen';
|
||||
|
||||
export interface DataQuery extends raw.DataQuery {
|
||||
// TODO remove explicit nulls
|
||||
datasource?: raw.DataSourceRef | null;
|
||||
}
|
||||
|
||||
export * from '../common/common.gen';
|
@ -1,5 +1,8 @@
|
||||
import { DataSourceRef as CommonDataSourceRef } from '../common/common.gen';
|
||||
import * as raw from '../raw/dashboard/x/dashboard_types.gen';
|
||||
|
||||
export type { CommonDataSourceRef as DataSourceRef };
|
||||
|
||||
export interface Panel<TOptions = Record<string, unknown>, TCustomFieldConfig = Record<string, unknown>>
|
||||
extends raw.Panel {
|
||||
fieldConfig: FieldConfigSource<TCustomFieldConfig>;
|
||||
@ -14,11 +17,15 @@ export enum VariableHide {
|
||||
export interface VariableModel
|
||||
extends Omit<raw.VariableModel, 'rootStateKey' | 'error' | 'description' | 'hide' | 'datasource'> {
|
||||
// Overrides nullable properties because CUE doesn't support null values
|
||||
// TODO remove explicit nulls
|
||||
rootStateKey: string | null;
|
||||
// TODO remove explicit nulls
|
||||
error: any | null;
|
||||
// TODO remove explicit nulls
|
||||
description: string | null;
|
||||
hide: VariableHide;
|
||||
datasource: raw.DataSourceRef | null;
|
||||
// TODO remove explicit nulls
|
||||
datasource: CommonDataSourceRef | null;
|
||||
}
|
||||
|
||||
export interface Dashboard extends Omit<raw.Dashboard, 'templating'> {
|
||||
@ -39,11 +46,15 @@ export interface FieldConfigSource<TOptions = Record<string, unknown>> extends r
|
||||
export const defaultDashboard = raw.defaultDashboard as Dashboard;
|
||||
export const defaultVariableModel = {
|
||||
...raw.defaultVariableModel,
|
||||
// TODO remove explicit nulls
|
||||
rootStateKey: null,
|
||||
// TODO remove explicit nulls
|
||||
error: null,
|
||||
// TODO remove explicit nulls
|
||||
description: null,
|
||||
hide: VariableHide.dontHide,
|
||||
state: raw.LoadingState.NotStarted,
|
||||
// TODO remove explicit nulls
|
||||
datasource: null,
|
||||
} as VariableModel;
|
||||
export const defaultPanel: Partial<Panel> = raw.defaultPanel;
|
||||
|
@ -1,15 +1,18 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
copenapi "cuelang.org/go/encoding/openapi"
|
||||
"github.com/dave/dst/dstutil"
|
||||
"github.com/grafana/codejen"
|
||||
"github.com/grafana/thema/encoding/gocode"
|
||||
"github.com/grafana/thema/encoding/openapi"
|
||||
)
|
||||
|
||||
// GoTypesJenny creates a [OneToOne] that produces Go types for the provided
|
||||
// [thema.Schema].
|
||||
type GoTypesJenny struct {
|
||||
ApplyFuncs []dstutil.ApplyFunc
|
||||
ApplyFuncs []dstutil.ApplyFunc
|
||||
ExpandReferences bool
|
||||
}
|
||||
|
||||
func (j GoTypesJenny) JennyName() string {
|
||||
@ -20,6 +23,12 @@ func (j GoTypesJenny) Generate(sfg SchemaForGen) (*codejen.File, error) {
|
||||
// TODO allow using name instead of machine name in thema generator
|
||||
b, err := gocode.GenerateTypesOpenAPI(sfg.Schema, &gocode.TypeConfigOpenAPI{
|
||||
// TODO will need to account for sanitizing e.g. dashes here at some point
|
||||
Config: &openapi.Config{
|
||||
Group: sfg.IsGroup,
|
||||
Config: &copenapi.Config{
|
||||
ExpandReferences: j.ExpandReferences,
|
||||
},
|
||||
},
|
||||
PackageName: sfg.Schema.Lineage().Name(),
|
||||
ApplyFuncs: append(j.ApplyFuncs, PrefixDropper(sfg.Name), DecoderCompactor()),
|
||||
})
|
||||
|
@ -1,31 +0,0 @@
|
||||
{{ template "autogen_header.tmpl" .Header -}}
|
||||
package corelist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sync"
|
||||
"github.com/grafana/grafana"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs"
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
func makeTreeOrPanic(path string, pkgname string, rt *thema.Runtime) *pfs.Tree {
|
||||
sub, err := fs.Sub(grafana.CueSchemaFS, path)
|
||||
if err != nil {
|
||||
panic("could not create fs sub to " + path)
|
||||
}
|
||||
tree, err := pfs.ParsePluginFS(sub, rt)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error parsing plugin metadata for %s: %s", pkgname, err))
|
||||
}
|
||||
return tree
|
||||
}
|
||||
|
||||
func coreTreeList(rt *thema.Runtime) pfs.TreeList{
|
||||
return pfs.TreeList{
|
||||
{{- range .Plugins }}
|
||||
makeTreeOrPanic("{{ .Path }}", "{{ .PkgName }}", rt),
|
||||
{{- end }}
|
||||
}
|
||||
}
|
@ -95,13 +95,19 @@ func LoadGrafanaInstancesWithThema(path string, cueFS fs.FS, rt *thema.Runtime,
|
||||
//
|
||||
// The returned fs.FS is suitable for passing to a CUE loader, such as [load.InstanceWithThema].
|
||||
func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) {
|
||||
m := fstest.MapFS{
|
||||
// fstest can recognize only forward slashes.
|
||||
// filepath.ToSlash(filepath.Join("cue.mod", "module.cue")): &fstest.MapFile{Data: []byte(`module: "github.com/grafana/grafana"`)},
|
||||
m, err := prefixFS(prefix, inputfs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return merged_fs.NewMergedFS(m, grafana.CueSchemaFS), nil
|
||||
}
|
||||
|
||||
// TODO such a waste, replace with stateless impl that just transforms paths on the fly
|
||||
func prefixFS(prefix string, fsys fs.FS) (fs.FS, error) {
|
||||
m := make(fstest.MapFS)
|
||||
|
||||
prefix = filepath.FromSlash(prefix)
|
||||
err := fs.WalkDir(inputfs, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -110,7 +116,7 @@ func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := fs.ReadFile(inputfs, path)
|
||||
b, err := fs.ReadFile(fsys, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -118,10 +124,7 @@ func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) {
|
||||
m[filepath.ToSlash(filepath.Join(prefix, path))] = &fstest.MapFile{Data: b}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return merged_fs.NewMergedFS(m, grafana.CueSchemaFS), nil
|
||||
return m, err
|
||||
}
|
||||
|
||||
// LoadGrafanaInstance wraps [load.InstanceWithThema] to load a
|
||||
@ -141,7 +144,7 @@ func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) {
|
||||
// ["cuelang.org/go/cue/load".Config.Package]. If the CUE package to be loaded
|
||||
// is the same as the parent directory name, it should be omitted.
|
||||
//
|
||||
// NOTE this function will be removed in favor of a more generic loader
|
||||
// NOTE this function will be deprecated in favor of a more generic loader
|
||||
func LoadGrafanaInstance(relpath string, pkg string, overlay fs.FS) (*build.Instance, error) {
|
||||
// notes about how this crap needs to work
|
||||
//
|
||||
@ -175,10 +178,13 @@ func LoadGrafanaInstance(relpath string, pkg string, overlay fs.FS) (*build.Inst
|
||||
}
|
||||
|
||||
// BuildGrafanaInstance wraps [LoadGrafanaInstance], additionally building
|
||||
// the returned [*build.Instance], if valid, into a [cue.Value] that is checked
|
||||
// for errors before returning.
|
||||
// the returned [*build.Instance] into a [cue.Value].
|
||||
//
|
||||
// NOTE this function will be removed in favor of a more generic loader
|
||||
// An error is returned if:
|
||||
// - The underlying call to [LoadGrafanaInstance] returns an error
|
||||
// - The built [cue.Value] has an error ([cue.Value.Err] returns non-nil)
|
||||
//
|
||||
// NOTE this function will be deprecated in favor of a more generic builder
|
||||
func BuildGrafanaInstance(ctx *cue.Context, relpath string, pkg string, overlay fs.FS) (cue.Value, error) {
|
||||
bi, err := LoadGrafanaInstance(relpath, pkg, overlay)
|
||||
if err != nil {
|
||||
@ -194,3 +200,39 @@ func BuildGrafanaInstance(ctx *cue.Context, relpath string, pkg string, overlay
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// LoadInstanceWithGrafana loads a [*build.Instance] from .cue files
|
||||
// in the provided modFS as ["cuelang.org/go/cue/load".Instances], but
|
||||
// fulfilling any imports of CUE packages under:
|
||||
//
|
||||
// - github.com/grafana/grafana
|
||||
// - github.com/grafana/thema
|
||||
//
|
||||
// This function is modeled after [load.InstanceWithThema]. It has the same
|
||||
// signature and expectations for the modFS.
|
||||
//
|
||||
// Attempting to use this func to load files within the
|
||||
// github.com/grafana/grafana CUE module will result in an error. Use
|
||||
// [LoadGrafanaInstance] instead.
|
||||
//
|
||||
// NOTE This function will be deprecated in favor of a more generic loader
|
||||
func LoadInstanceWithGrafana(fsys fs.FS, dir string, opts ...load.Option) (*build.Instance, error) {
|
||||
if modf, err := fs.ReadFile(fsys, filepath.Join("cue.mod", "module.cue")); err != nil {
|
||||
// delegate error handling
|
||||
return load.InstanceWithThema(fsys, dir, opts...)
|
||||
} else if modname, err := cuecontext.New().CompileBytes(modf).LookupPath(cue.MakePath(cue.Str("module"))).String(); err != nil {
|
||||
// delegate error handling
|
||||
return load.InstanceWithThema(fsys, dir, opts...)
|
||||
} else if modname == "github.com/grafana/grafana" {
|
||||
return nil, fmt.Errorf("use cuectx.LoadGrafanaInstance to load .cue files within github.com/grafana/grafana CUE module")
|
||||
}
|
||||
|
||||
// TODO wasteful, doing this every time - make that stateless prefixfs!
|
||||
depFS, err := prefixFS("cue.mod/pkg/github.com/grafana/grafana", grafana.CueSchemaFS)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// FIXME remove grafana from cue.mod/pkg if it exists, otherwise external thing can inject files to be loaded
|
||||
return load.InstanceWithThema(merged_fs.NewMergedFS(depFS, fsys), dir, opts...)
|
||||
}
|
||||
|
48
pkg/kindsys/bind.go
Normal file
48
pkg/kindsys/bind.go
Normal file
@ -0,0 +1,48 @@
|
||||
package kindsys
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
var _ Composable = genericComposable{}
|
||||
|
||||
type genericComposable struct {
|
||||
decl Decl[ComposableProperties]
|
||||
lin thema.Lineage
|
||||
}
|
||||
|
||||
func (k genericComposable) Props() SomeKindProperties {
|
||||
return k.decl.Properties
|
||||
}
|
||||
|
||||
func (k genericComposable) Name() string {
|
||||
return k.decl.Properties.Name
|
||||
}
|
||||
|
||||
func (k genericComposable) MachineName() string {
|
||||
return k.decl.Properties.MachineName
|
||||
}
|
||||
|
||||
func (k genericComposable) Maturity() Maturity {
|
||||
return k.decl.Properties.Maturity
|
||||
}
|
||||
|
||||
func (k genericComposable) Decl() Decl[ComposableProperties] {
|
||||
return k.decl
|
||||
}
|
||||
|
||||
func (k genericComposable) Lineage() thema.Lineage {
|
||||
return k.lin
|
||||
}
|
||||
|
||||
func BindComposable(rt *thema.Runtime, decl Decl[ComposableProperties], opts ...thema.BindOption) (Composable, error) {
|
||||
lin, err := decl.Some().BindKindLineage(rt, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return genericComposable{
|
||||
decl: decl,
|
||||
lin: lin,
|
||||
}, nil
|
||||
}
|
37
pkg/kindsys/common_dataquery.cue
Normal file
37
pkg/kindsys/common_dataquery.cue
Normal file
@ -0,0 +1,37 @@
|
||||
package kindsys
|
||||
|
||||
// Canonically defined in pkg/kindsys/dataquery.cue FOR NOW to avoid having any external imports
|
||||
// in kindsys. Code generation copies this file to the common schemas in packages/grafana-schema/src/common.
|
||||
//
|
||||
// NOTE make gen-cue must be run twice when updating this file
|
||||
|
||||
// These are the common properties available to all queries in all datasources.
|
||||
// Specific implementations will *extend* this interface, adding the required
|
||||
// properties for the given context.
|
||||
DataQuery: {
|
||||
// A - Z
|
||||
refId: string
|
||||
|
||||
// true if query is disabled (ie should not be returned to the dashboard)
|
||||
hide?: bool
|
||||
|
||||
// Unique, guid like, string used in explore mode
|
||||
key?: string
|
||||
|
||||
// Specify the query flavor
|
||||
// TODO make this required and give it a default
|
||||
queryType?: string
|
||||
|
||||
// For mixed data sources the selected datasource is on the query level.
|
||||
// For non mixed scenarios this is undefined.
|
||||
// TODO find a better way to do this ^ that's friendly to schema
|
||||
// TODO this shouldn't be unknown but DataSourceRef | null
|
||||
datasource?: _
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
DataSourceRef: {
|
||||
// The plugin type-id
|
||||
type?: string
|
||||
// Specific datasource instance
|
||||
uid?: string
|
||||
} @cuetsy(kind="interface")
|
@ -9,27 +9,10 @@ var (
|
||||
ErrValueNotExist = errors.New("cue value does not exist")
|
||||
|
||||
// ErrValueNotAKind indicates that a provided CUE value is not any variety of
|
||||
// Interface. This is almost always an end-user error - they oops'd and provided the
|
||||
// Interface. This is almost always a user error - they oops'd and provided the
|
||||
// wrong path, file, etc.
|
||||
ErrValueNotAKind = errors.New("not a kind")
|
||||
|
||||
// ErrInvalidCUE indicates that the CUE representing the kind is invalid.
|
||||
ErrInvalidCUE = errors.New("CUE syntax error")
|
||||
)
|
||||
|
||||
func ewrap(actual, is error) error {
|
||||
return &errPassthrough{
|
||||
actual: actual,
|
||||
is: is,
|
||||
}
|
||||
}
|
||||
|
||||
type errPassthrough struct {
|
||||
actual error
|
||||
is error
|
||||
}
|
||||
|
||||
func (e *errPassthrough) Is(err error) bool {
|
||||
return errors.Is(err, e.actual) || errors.Is(err, e.is)
|
||||
}
|
||||
|
||||
func (e *errPassthrough) Error() string {
|
||||
return e.actual.Error()
|
||||
}
|
||||
|
@ -15,15 +15,17 @@ Composable: S={
|
||||
// schemaInterface is the name of the Grafana schema interface implemented by
|
||||
// this Composable kind. The set is open to ensure forward compatibility of
|
||||
// Grafana and tooling with any additional schema interfaces that may be added.
|
||||
// schemaInterface: or([ for k, _ in schemaInterfaces {k}, string])
|
||||
schemaInterface: string
|
||||
// TODO is it worth doing something like below, given that we have to keep this set open for forward compatibility?
|
||||
// schemaInterface: or([ for k, _ in schemaInterfaces {k}, string])
|
||||
|
||||
let schif = schemaInterfaces[S.schemaInterface]
|
||||
|
||||
// lineage is the Thema lineage containing all the schemas that have existed for this kind.
|
||||
// The name of the lineage is constrained to the name of the schema interface being implemented.
|
||||
// lineage: thema.#Lineage & {name: S.schemaInterface, joinSchema: schif.interface}
|
||||
lineage: { joinSchema: (schif.interface | *{}) }
|
||||
// FIXME cuetsy currently gets confused by all the unifications - maybe openapi too. Do something like the following after thema separates joinSchema/constraint expression
|
||||
// lineage: { joinSchema: schif.interface }
|
||||
// lineage: { joinSchema: (schif.interface | *{}) }
|
||||
|
||||
lineageIsGroup: schif.group | *false
|
||||
lineageIsGroup: schif.group
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ _sharedKind: {
|
||||
// In addition to lowercase normalization, dashes are transformed to underscores.
|
||||
machineName: strings.ToLower(strings.Replace(name, "-", "_", -1))
|
||||
|
||||
// pluralName is the pluralized form of name. Defaults to name + "s".
|
||||
// pluralName is the pluralized form of name. Defaults to name + "s".
|
||||
pluralName: =~"^([A-Z][a-zA-Z0-9-]{0,61}[a-zA-Z])$" | *(name + "s")
|
||||
|
||||
// pluralMachineName is the pluralized form of [machineName]. The same case
|
||||
@ -57,8 +57,8 @@ _sharedKind: {
|
||||
// grouped lineage, each top-level field in the schema specifies a discrete
|
||||
// object that is expected to exist in the wild
|
||||
//
|
||||
// This field is set at the framework level, and cannot be in the declaration of
|
||||
// any individual kind.
|
||||
// This value of this field is set by the kindsys framework. It cannot be changed
|
||||
// in the declaration of any individual kind.
|
||||
//
|
||||
// This is likely to eventually become a first-class property in Thema:
|
||||
// https://github.com/grafana/thema/issues/62
|
||||
@ -71,7 +71,7 @@ _sharedKind: {
|
||||
// schema in lineage.
|
||||
currentVersion: thema.#SyntacticVersion & (thema.#LatestVersion & {lin: lineage}).out
|
||||
|
||||
maturity: #Maturity
|
||||
maturity: Maturity
|
||||
|
||||
// The kind system itself is not mature enough yet for any single
|
||||
// kind to advance beyond "experimental"
|
||||
@ -81,7 +81,7 @@ _sharedKind: {
|
||||
|
||||
// Maturity indicates the how far a given kind declaration is in its initial
|
||||
// journey. Mature kinds still evolve, but with guarantees about compatibility.
|
||||
#Maturity: "merged" | "experimental" | "stable" | "mature"
|
||||
Maturity: "merged" | "experimental" | "stable" | "mature"
|
||||
|
||||
// Core specifies the kind category for core-defined arbitrary types.
|
||||
// Familiar types and functional resources in Grafana, such as dashboards and
|
||||
|
@ -47,7 +47,8 @@ func (m CustomProperties) Common() CommonProperties {
|
||||
// excludes Thema schemas.
|
||||
type ComposableProperties struct {
|
||||
CommonProperties
|
||||
CurrentVersion thema.SyntacticVersion `json:"currentVersion"`
|
||||
CurrentVersion thema.SyntacticVersion `json:"currentVersion"`
|
||||
SchemaInterface string `json:"schemaInterface"`
|
||||
}
|
||||
|
||||
func (m ComposableProperties) _private() {}
|
||||
|
@ -102,9 +102,10 @@ func ToKindProps[T KindProperties](v cue.Value) (T, error) {
|
||||
}
|
||||
|
||||
item := v.Unify(kdef)
|
||||
if err := item.Validate(cue.Concrete(false), cue.All()); err != nil {
|
||||
return *props, ewrap(item.Err(), ErrValueNotAKind)
|
||||
if item.Err() != nil {
|
||||
return *props, errors.Wrap(errors.Promote(ErrValueNotAKind, ""), item.Err())
|
||||
}
|
||||
|
||||
if err := item.Decode(props); err != nil {
|
||||
// Should only be reachable if CUE and Go framework types have diverged
|
||||
panic(errors.Details(err, nil))
|
||||
@ -113,11 +114,10 @@ func ToKindProps[T KindProperties](v cue.Value) (T, error) {
|
||||
return *props, nil
|
||||
}
|
||||
|
||||
// SomeDecl represents a single kind declaration, having been loaded
|
||||
// and validated by a func such as [LoadCoreKind].
|
||||
// SomeDecl represents a single kind declaration, having been loaded and
|
||||
// validated by a func such as [LoadCoreKind].
|
||||
//
|
||||
// The underlying type of the Properties field indicates the category of
|
||||
// kind.
|
||||
// The underlying type of the Properties field indicates the category of kind.
|
||||
type SomeDecl struct {
|
||||
// V is the cue.Value containing the entire Kind declaration.
|
||||
V cue.Value
|
||||
@ -134,12 +134,7 @@ func (decl SomeDecl) BindKindLineage(rt *thema.Runtime, opts ...thema.BindOption
|
||||
if rt == nil {
|
||||
rt = cuectx.GrafanaThemaRuntime()
|
||||
}
|
||||
switch decl.Properties.(type) {
|
||||
case CoreProperties, CustomProperties, ComposableProperties:
|
||||
return thema.BindLineage(decl.V.LookupPath(cue.MakePath(cue.Str("lineage"))), rt, opts...)
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
return thema.BindLineage(decl.V.LookupPath(cue.MakePath(cue.Str("lineage"))), rt, opts...)
|
||||
}
|
||||
|
||||
// IsCore indicates whether the represented kind is a core kind.
|
||||
|
@ -14,9 +14,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/codejen"
|
||||
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs/corelist"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/grafana/pkg/registry/corekind"
|
||||
)
|
||||
|
||||
@ -140,13 +140,14 @@ func buildKindStateReport() *KindStateReport {
|
||||
}
|
||||
|
||||
all := kindsys.SchemaInterfaces(nil)
|
||||
// TODO this is all hacks until #59001, which will unite plugins with kindsys
|
||||
for _, tree := range corelist.New(nil) {
|
||||
rp := tree.RootPlugin()
|
||||
for _, pp := range corelist.New(nil) {
|
||||
for _, si := range all {
|
||||
if si.Should(string(rp.Meta().Type)) {
|
||||
n := fmt.Sprintf("%s-%s", strings.Title(rp.Meta().Id), si.Name())
|
||||
if ck, has := pp.ComposableKinds[si.Name()]; has {
|
||||
r.add(ck.Props(), "composable")
|
||||
} else if may := si.Should(string(pp.Properties.Type)); may {
|
||||
n := plugindef.DerivePascalName(pp.Properties) + si.Name()
|
||||
props := kindsys.ComposableProperties{
|
||||
SchemaInterface: si.Name(),
|
||||
CommonProperties: kindsys.CommonProperties{
|
||||
Name: n,
|
||||
PluralName: n + "s",
|
||||
@ -156,10 +157,6 @@ func buildKindStateReport() *KindStateReport {
|
||||
Maturity: "planned",
|
||||
},
|
||||
}
|
||||
if ck, has := rp.SlotImplementations()[si.Name()]; has {
|
||||
props.CommonProperties.Maturity = "merged"
|
||||
props.CurrentVersion = ck.Latest().Version()
|
||||
}
|
||||
r.add(props, "composable")
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -21,7 +21,7 @@ package kindsys
|
||||
// On the producer side, Grafana plugin authors may provide Thema lineages
|
||||
// within Composable kinds declared in .cue files adjacent to their
|
||||
// plugin.json, following a pattern (see
|
||||
// github.com/grafana/grafana/pkg/plugins/pfs.#GrafanaPlugin.composableKinds)
|
||||
// github.com/grafana/grafana/pkg/plugins/pfs.GrafanaPlugin.composableKinds)
|
||||
// corresponding to the name of the schema interface. Each such definition is
|
||||
// an answer to "what."
|
||||
//
|
||||
@ -87,10 +87,14 @@ SchemaInterface: {
|
||||
group: bool | *true
|
||||
}
|
||||
|
||||
// alias the exported type because DataQuery is shadowed by the schema interface
|
||||
// name where we need to use the type
|
||||
let dq = DataQuery
|
||||
|
||||
// The canonical list of all Grafana schema interfaces.
|
||||
schemaInterfaces: [N=string]: SchemaInterface & { name: N }
|
||||
schemaInterfaces: {
|
||||
Panel: {
|
||||
PanelCfg: {
|
||||
interface: {
|
||||
// Defines plugin-specific options for a panel that should be persisted. Required,
|
||||
// though a panel without any options may specify an empty struct.
|
||||
@ -109,22 +113,25 @@ schemaInterfaces: {
|
||||
// grouped b/c separate non-cross-referring elements always occur together in larger structure (panel)
|
||||
group: true
|
||||
}
|
||||
Query: {
|
||||
// The contract for the queries schema interface is itself a pattern:
|
||||
// Each of its top-level fields must be represent a distinct query type for
|
||||
// the datasource plugin. The queryType field acts as a discriminator, and
|
||||
// is constrained to be the same as the name of the top-level field declaring it.
|
||||
interface: [QT=string]: {
|
||||
queryType?: QT
|
||||
|
||||
// The DataQuery schema interface specifies how (datasource) plugins are expected to define
|
||||
// the shape of their queries.
|
||||
//
|
||||
// It is expected that plugins may support multiple logically distinct query types within
|
||||
// their single DataQuery composable kind. Implementations are generally free to model
|
||||
// this as they please, with understanding that Grafana systems will look to the queryType
|
||||
// field as a discriminator - each distinct value will be assumed, where possible, to
|
||||
// identify a distinct type of query supported by the plugin.
|
||||
DataQuery: {
|
||||
interface: {
|
||||
dq
|
||||
}
|
||||
|
||||
pluginTypes: ["datasource"]
|
||||
|
||||
// grouped b/c separate, non-cross-referring elements are actually themselves each impls of the concept
|
||||
// and it avoids us having to put more levels in the slot system (uggghhh)
|
||||
group: true
|
||||
group: false
|
||||
}
|
||||
DSOptions: {
|
||||
|
||||
DataSourceCfg: {
|
||||
interface: {
|
||||
// Normal datasource configuration options.
|
||||
Options: {}
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
const prefix = "github.com/grafana/grafana/public/app/plugins"
|
||||
|
||||
// PluginTreeListJenny creates a [codejen.ManyToOne] that produces Go code
|
||||
// for loading a [pfs.TreeList] given [*kindsys.PluginDecl] as inputs.
|
||||
// for loading a [pfs.PluginList] given [*kindsys.PluginDecl] as inputs.
|
||||
func PluginTreeListJenny() codejen.ManyToOne[*pfs.PluginDecl] {
|
||||
outputFile := filepath.Join("pkg", "plugins", "pfs", "corelist", "corelist_load_gen.go")
|
||||
|
||||
|
@ -9,22 +9,22 @@ import (
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
func makeTreeOrPanic(path string, pkgname string, rt *thema.Runtime) *pfs.Tree {
|
||||
func parsePluginOrPanic(path string, pkgname string, rt *thema.Runtime) pfs.ParsedPlugin {
|
||||
sub, err := fs.Sub(grafana.CueSchemaFS, path)
|
||||
if err != nil {
|
||||
panic("could not create fs sub to " + path)
|
||||
}
|
||||
tree, err := pfs.ParsePluginFS(sub, rt)
|
||||
pp, err := pfs.ParsePluginFS(sub, rt)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error parsing plugin metadata for %s: %s", pkgname, err))
|
||||
}
|
||||
return tree
|
||||
return pp
|
||||
}
|
||||
|
||||
func coreTreeList(rt *thema.Runtime) pfs.TreeList{
|
||||
return pfs.TreeList{
|
||||
func corePlugins(rt *thema.Runtime) []pfs.ParsedPlugin{
|
||||
return []pfs.ParsedPlugin{
|
||||
{{- range .Plugins }}
|
||||
makeTreeOrPanic("{{ .Path }}", "{{ .PkgName }}", rt),
|
||||
parsePluginOrPanic("{{ .Path }}", "{{ .PkgName }}", rt),
|
||||
{{- end }}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,10 @@ import (
|
||||
// indicates the import path should be dropped in the conversion to TS. Imports
|
||||
// not present in the list are not not allowed, and code generation will fail.
|
||||
var importMap = map[string]string{
|
||||
"github.com/grafana/thema": "",
|
||||
"github.com/grafana/thema": "",
|
||||
|
||||
"github.com/grafana/grafana/pkg/kindsys": "",
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs": "",
|
||||
"github.com/grafana/grafana/packages/grafana-schema/src/common": "@grafana/schema",
|
||||
}
|
||||
|
||||
|
25
pkg/plugins/manager/testdata/disallowed-cue-import/composable.cue
vendored
Normal file
25
pkg/plugins/manager/testdata/disallowed-cue-import/composable.cue
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
"github.com/grafana/grafana/kinds/dashboard:kind"
|
||||
)
|
||||
|
||||
_dummy: coremodel.slots
|
||||
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
name: "disallowed_cue_import"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
"github.com/grafana/grafana/kinds/dashboard:kind"
|
||||
)
|
||||
|
||||
_dummy: coremodel.slots
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "disallowed_cue_import"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Slot impl testing",
|
||||
"id": "mismatch-panel",
|
||||
"backend": true,
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,6 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Query: thema.#Lineage & {
|
||||
name: "missing_kind_datasource"
|
||||
composableKinds: DataQuery: lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
@ -1,8 +1,6 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
composableKinds: PanelCfg: lineage: {
|
||||
name: "doesnamatch"
|
||||
seqs: [
|
||||
{
|
25
pkg/plugins/manager/testdata/panel-conflicting-joinschema/composable.cue
vendored
Normal file
25
pkg/plugins/manager/testdata/panel-conflicting-joinschema/composable.cue
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
joinSchema: {
|
||||
PanelOptions: {...}
|
||||
PanelFieldConfig: string
|
||||
}
|
||||
name: "panel_conflicting_joinschema"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
joinSchema: {
|
||||
PanelOptions: {...}
|
||||
PanelFieldConfig: string
|
||||
}
|
||||
name: "panel_conflicting_joinschema"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
21
pkg/plugins/manager/testdata/panel-does-not-follow-slot-joinschema/composable.cue
vendored
Normal file
21
pkg/plugins/manager/testdata/panel-does-not-follow-slot-joinschema/composable.cue
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
name: "panel_does_not_follow_slot_joinschema"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "panel_does_not_follow_slot_joinschema"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
13
pkg/plugins/manager/testdata/valid-model-datasource/composable_dataquery.cue
vendored
Normal file
13
pkg/plugins/manager/testdata/valid-model-datasource/composable_dataquery.cue
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
package grafanaplugin
|
||||
|
||||
composableKinds: DataQuery: lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
foo: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
18
pkg/plugins/manager/testdata/valid-model-datasource/composable_datasourcecfg.cue
vendored
Normal file
18
pkg/plugins/manager/testdata/valid-model-datasource/composable_datasourcecfg.cue
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
package grafanaplugin
|
||||
|
||||
composableKinds: DataSourceCfg: lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
Options: {
|
||||
foo: string
|
||||
}
|
||||
SecureOptions: {
|
||||
bar: string
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Query: thema.#Lineage & {
|
||||
name: "valid_model_datasource"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
foo: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
DSOptions: thema.#Lineage & {
|
||||
name: "valid_model_datasource"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
Options: {
|
||||
foo: string
|
||||
}
|
||||
SecureOptions: {
|
||||
bar: string
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
@ -1,9 +1,6 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "mismatch"
|
||||
composableKinds: PanelCfg: lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
@ -1,18 +0,0 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "valid_model_panel"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Query: thema.#Lineage & {
|
||||
name: "wrong_slot_panel"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
foo: string
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "wrong_slot_panel"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
foo: string
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Wrong slot for type",
|
||||
"id": "wrong-slot-panel",
|
||||
"backend": true,
|
||||
"state": "alpha",
|
||||
"info": {
|
||||
"description": "Test",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
}
|
||||
}
|
||||
}
|
@ -8,23 +8,23 @@ import (
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
var coreTrees pfs.TreeList
|
||||
var coreTrees []pfs.ParsedPlugin
|
||||
var coreOnce sync.Once
|
||||
|
||||
// New returns a pfs.TreeList containing the plugin trees for all core plugins
|
||||
// New returns a pfs.PluginList containing the plugin trees for all core plugins
|
||||
// in the current version of Grafana.
|
||||
//
|
||||
// Go code within the grafana codebase should only ever call this with nil.
|
||||
func New(rt *thema.Runtime) pfs.TreeList {
|
||||
var tl pfs.TreeList
|
||||
func New(rt *thema.Runtime) []pfs.ParsedPlugin {
|
||||
var pl []pfs.ParsedPlugin
|
||||
if rt == nil {
|
||||
coreOnce.Do(func() {
|
||||
coreTrees = coreTreeList(cuectx.GrafanaThemaRuntime())
|
||||
coreTrees = corePlugins(cuectx.GrafanaThemaRuntime())
|
||||
})
|
||||
tl = make(pfs.TreeList, len(coreTrees))
|
||||
copy(tl, coreTrees)
|
||||
pl = make([]pfs.ParsedPlugin, len(coreTrees))
|
||||
copy(pl, coreTrees)
|
||||
} else {
|
||||
return coreTreeList(rt)
|
||||
return corePlugins(rt)
|
||||
}
|
||||
return tl
|
||||
return pl
|
||||
}
|
||||
|
@ -18,63 +18,63 @@ import (
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
|
||||
func makeTreeOrPanic(path string, pkgname string, rt *thema.Runtime) *pfs.Tree {
|
||||
func parsePluginOrPanic(path string, pkgname string, rt *thema.Runtime) pfs.ParsedPlugin {
|
||||
sub, err := fs.Sub(grafana.CueSchemaFS, path)
|
||||
if err != nil {
|
||||
panic("could not create fs sub to " + path)
|
||||
}
|
||||
tree, err := pfs.ParsePluginFS(sub, rt)
|
||||
pp, err := pfs.ParsePluginFS(sub, rt)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error parsing plugin metadata for %s: %s", pkgname, err))
|
||||
}
|
||||
return tree
|
||||
return pp
|
||||
}
|
||||
|
||||
func coreTreeList(rt *thema.Runtime) pfs.TreeList {
|
||||
return pfs.TreeList{
|
||||
makeTreeOrPanic("public/app/plugins/datasource/alertmanager", "alertmanager", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/cloud-monitoring", "stackdriver", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/cloudwatch", "cloudwatch", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/dashboard", "dashboard", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/elasticsearch", "elasticsearch", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/grafana", "grafana", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/grafana-azure-monitor-datasource", "grafana_azure_monitor_datasource", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/graphite", "graphite", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/jaeger", "jaeger", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/loki", "loki", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/mssql", "mssql", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/mysql", "mysql", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/parca", "parca", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/phlare", "phlare", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/postgres", "postgres", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/prometheus", "prometheus", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/tempo", "tempo", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/testdata", "testdata", rt),
|
||||
makeTreeOrPanic("public/app/plugins/datasource/zipkin", "zipkin", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/alertGroups", "alertGroups", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/alertlist", "alertlist", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/annolist", "annolist", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/barchart", "barchart", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/bargauge", "bargauge", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/dashlist", "dashlist", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/debug", "debug", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/flamegraph", "flamegraph", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/gauge", "gauge", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/geomap", "geomap", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/gettingstarted", "gettingstarted", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/graph", "graph", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/histogram", "histogram", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/icon", "icon", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/live", "live", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/logs", "logs", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/news", "news", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/nodeGraph", "nodeGraph", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/piechart", "piechart", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/stat", "stat", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/table-old", "table_old", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/text", "text", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/traces", "traces", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/welcome", "welcome", rt),
|
||||
makeTreeOrPanic("public/app/plugins/panel/xychart", "xychart", rt),
|
||||
func corePlugins(rt *thema.Runtime) []pfs.ParsedPlugin {
|
||||
return []pfs.ParsedPlugin{
|
||||
parsePluginOrPanic("public/app/plugins/datasource/alertmanager", "alertmanager", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/cloud-monitoring", "stackdriver", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/cloudwatch", "cloudwatch", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/dashboard", "dashboard", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/elasticsearch", "elasticsearch", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/grafana", "grafana", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/grafana-azure-monitor-datasource", "grafana_azure_monitor_datasource", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/graphite", "graphite", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/jaeger", "jaeger", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/loki", "loki", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/mssql", "mssql", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/mysql", "mysql", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/parca", "parca", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/phlare", "phlare", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/postgres", "postgres", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/prometheus", "prometheus", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/tempo", "tempo", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/testdata", "testdata", rt),
|
||||
parsePluginOrPanic("public/app/plugins/datasource/zipkin", "zipkin", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/alertGroups", "alertGroups", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/alertlist", "alertlist", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/annolist", "annolist", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/barchart", "barchart", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/bargauge", "bargauge", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/dashlist", "dashlist", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/debug", "debug", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/flamegraph", "flamegraph", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/gauge", "gauge", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/geomap", "geomap", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/gettingstarted", "gettingstarted", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/graph", "graph", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/histogram", "histogram", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/icon", "icon", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/live", "live", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/logs", "logs", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/news", "news", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/nodeGraph", "nodeGraph", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/piechart", "piechart", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/stat", "stat", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/table-old", "table_old", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/text", "text", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/traces", "traces", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/welcome", "welcome", rt),
|
||||
parsePluginOrPanic("public/app/plugins/panel/xychart", "xychart", rt),
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package pfs
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@ -24,7 +23,9 @@ func NewDeclParser(rt *thema.Runtime, skip map[string]bool) *declParser {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO convert this to be the new parser for Tree
|
||||
func (psr *declParser) Parse(root fs.FS) ([]*PluginDecl, error) {
|
||||
// TODO remove hardcoded tree structure assumption, work from root of provided fs
|
||||
plugins, err := fs.Glob(root, "**/**/plugin.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error finding plugin dirs: %w", err)
|
||||
@ -39,31 +40,26 @@ func (psr *declParser) Parse(root fs.FS) ([]*PluginDecl, error) {
|
||||
}
|
||||
|
||||
dir := os.DirFS(path)
|
||||
ptree, err := ParsePluginFS(dir, psr.rt)
|
||||
pp, err := ParsePluginFS(dir, psr.rt)
|
||||
if err != nil {
|
||||
log.Println(fmt.Errorf("parsing plugin failed for %s: %s", dir, err))
|
||||
return nil, fmt.Errorf("parsing plugin failed for %s: %s", dir, err)
|
||||
}
|
||||
|
||||
if len(pp.ComposableKinds) == 0 {
|
||||
decls = append(decls, EmptyPluginDecl(path, pp.Properties))
|
||||
continue
|
||||
}
|
||||
|
||||
p := ptree.RootPlugin()
|
||||
slots := p.SlotImplementations()
|
||||
|
||||
if len(slots) == 0 {
|
||||
decls = append(decls, EmptyPluginDecl(path, p.Meta()))
|
||||
continue
|
||||
}
|
||||
|
||||
for slotName, lin := range slots {
|
||||
for slotName, kind := range pp.ComposableKinds {
|
||||
slot, err := kindsys.FindSchemaInterface(slotName)
|
||||
if err != nil {
|
||||
log.Println(fmt.Errorf("parsing plugin failed for %s: %s", dir, err))
|
||||
continue
|
||||
return nil, fmt.Errorf("parsing plugin failed for %s: %s", dir, err)
|
||||
}
|
||||
decls = append(decls, &PluginDecl{
|
||||
SchemaInterface: &slot,
|
||||
Lineage: lin,
|
||||
Imports: p.CUEImports(),
|
||||
PluginMeta: p.Meta(),
|
||||
Lineage: kind.Lineage(),
|
||||
Imports: pp.CUEImports,
|
||||
PluginMeta: pp.Properties,
|
||||
PluginPath: path,
|
||||
})
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
// Package pfs ("Plugin FS") defines a virtual filesystem representation of Grafana plugins.
|
||||
// Package pfs ("ParsedPlugin FS") defines a virtual filesystem representation of Grafana plugins.
|
||||
|
||||
package pfs
|
||||
|
@ -11,23 +11,26 @@ var ErrNoRootFile = errors.New("no plugin.json at root of fs.fS")
|
||||
// ErrInvalidRootFile indicates that the root plugin.json file is invalid.
|
||||
var ErrInvalidRootFile = errors.New("plugin.json is invalid")
|
||||
|
||||
// ErrImplementedSlots indicates that a plugin has implemented the wrong set of
|
||||
// slots for its type in models.cue. Either:
|
||||
// - A slot is implemented that is not allowed for its type (e.g. datasource plugin implements Panel)
|
||||
// - A required slot for its type is not implemented (e.g. panel plugin does not implemented Panel)
|
||||
var ErrImplementedSlots = errors.New("slot implementation not allowed for this plugin type")
|
||||
// ErrComposableNotExpected indicates that a plugin has a composable kind for a
|
||||
// schema interface that is not expected, given the type of the plugin. (For
|
||||
// example, a datasource plugin has a panelcfg composable kind)
|
||||
var ErrComposableNotExpected = errors.New("plugin type should not produce composable kind for schema interface")
|
||||
|
||||
// ErrInvalidCUE indicates that a plugin's model.cue file contained invalid CUE.
|
||||
var ErrInvalidCUE = errors.New("CUE syntax error")
|
||||
// ErrExpectedComposable indicates that a plugin lacks a composable kind
|
||||
// implementation for a schema interface that is expected for that plugin's
|
||||
// type. (For example, a datasource plugin lacks a queries composable kind)
|
||||
var ErrExpectedComposable = errors.New("plugin type should produce composable kind for schema interface")
|
||||
|
||||
// ErrInvalidGrafanaPluginInstance indicates a plugin's set of .cue
|
||||
// grafanaplugin package files are invalid with respect to the GrafanaPlugin
|
||||
// spec.
|
||||
var ErrInvalidGrafanaPluginInstance = errors.New("grafanaplugin cue instance is invalid")
|
||||
|
||||
// ErrInvalidLineage indicates that the plugin contains an invalid lineage
|
||||
// declaration, according to Thema's validation rules in
|
||||
// ["github.com/grafana/thema".BindLineage].
|
||||
var ErrInvalidLineage = errors.New("invalid lineage")
|
||||
|
||||
// ErrLineageNameMismatch indicates a plugin slot lineage name did not match the id of the plugin.
|
||||
var ErrLineageNameMismatch = errors.New("lineage name not the same as plugin id")
|
||||
|
||||
// ErrDisallowedCUEImport indicates that a plugin's models.cue file imports a
|
||||
// CUE package that is not on the whitelist for safe imports.
|
||||
// ErrDisallowedCUEImport indicates that a plugin's grafanaplugin cue package
|
||||
// contains that are not on the allowlist.
|
||||
var ErrDisallowedCUEImport = errors.New("CUE import is not allowed")
|
||||
|
31
pkg/plugins/pfs/grafanaplugin.cue
Normal file
31
pkg/plugins/pfs/grafanaplugin.cue
Normal file
@ -0,0 +1,31 @@
|
||||
package pfs
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
)
|
||||
|
||||
// GrafanaPlugin specifies what plugins may declare in .cue files in a
|
||||
// `grafanaplugin` CUE package in the plugin root directory (adjacent to plugin.json).
|
||||
GrafanaPlugin: {
|
||||
// id and pascalName are injected from plugin.json. Plugin authors can write
|
||||
// values for them in .cue files, but the only valid values will be the ones
|
||||
// given in plugin.json.
|
||||
id: string
|
||||
pascalName: string
|
||||
|
||||
// A plugin defines its Composable kinds under this key.
|
||||
//
|
||||
// This struct is open for forwards compatibility - older versions of Grafana (or
|
||||
// dependent tooling) should not break if new versions introduce additional schema interfaces.
|
||||
composableKinds?: [Iface=string]: kindsys.Composable & {
|
||||
name: pascalName + Iface
|
||||
schemaInterface: Iface
|
||||
lineage: name: pascalName + Iface
|
||||
}
|
||||
|
||||
// A plugin defines its Custom kinds under this key.
|
||||
customKinds?: [Name=string]: kindsys.Custom & {
|
||||
name: Name
|
||||
}
|
||||
...
|
||||
}
|
@ -3,14 +3,19 @@ package pfs
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing/fstest"
|
||||
|
||||
"cuelang.org/go/cue"
|
||||
"cuelang.org/go/cue/ast"
|
||||
"cuelang.org/go/cue/build"
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
"cuelang.org/go/cue/errors"
|
||||
"cuelang.org/go/cue/parser"
|
||||
"github.com/grafana/grafana"
|
||||
"cuelang.org/go/cue/token"
|
||||
"github.com/grafana/grafana/pkg/cuectx"
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
"github.com/grafana/thema"
|
||||
@ -19,13 +24,41 @@ import (
|
||||
"github.com/yalue/merged_fs"
|
||||
)
|
||||
|
||||
// PermittedCUEImports returns the list of packages that may be imported in a
|
||||
// plugin models.cue file.
|
||||
// PackageName is the name of the CUE package that Grafana will load when
|
||||
// looking for a Grafana plugin's kind declarations.
|
||||
const PackageName = "grafanaplugin"
|
||||
|
||||
var onceGP sync.Once
|
||||
var defaultGP cue.Value
|
||||
|
||||
func doLoadGP(ctx *cue.Context) cue.Value {
|
||||
v, err := cuectx.BuildGrafanaInstance(ctx, filepath.Join("pkg", "plugins", "pfs"), "pfs", nil)
|
||||
if err != nil {
|
||||
// should be unreachable
|
||||
panic(err)
|
||||
}
|
||||
return v.LookupPath(cue.MakePath(cue.Str("GrafanaPlugin")))
|
||||
}
|
||||
|
||||
func loadGP(ctx *cue.Context) cue.Value {
|
||||
if ctx == nil || ctx == cuectx.GrafanaCUEContext() {
|
||||
onceGP.Do(func() {
|
||||
defaultGP = doLoadGP(ctx)
|
||||
})
|
||||
return defaultGP
|
||||
}
|
||||
return doLoadGP(ctx)
|
||||
}
|
||||
|
||||
// PermittedCUEImports returns the list of import paths that may be used in a
|
||||
// plugin's grafanaplugin cue package.
|
||||
//
|
||||
// TODO probably move this into kindsys
|
||||
func PermittedCUEImports() []string {
|
||||
return []string{
|
||||
"github.com/grafana/thema",
|
||||
"github.com/grafana/grafana/pkg/kindsys",
|
||||
"github.com/grafana/grafana/pkg/plugins/pfs",
|
||||
"github.com/grafana/grafana/packages/grafana-schema/src/common",
|
||||
}
|
||||
}
|
||||
@ -41,12 +74,7 @@ func importAllowed(path string) bool {
|
||||
|
||||
var allowedImportsStr string
|
||||
|
||||
type slotandname struct {
|
||||
name string
|
||||
slot kindsys.SchemaInterface
|
||||
}
|
||||
|
||||
var allslots []slotandname
|
||||
var allsi []kindsys.SchemaInterface
|
||||
|
||||
func init() {
|
||||
all := make([]string, 0, len(PermittedCUEImports()))
|
||||
@ -55,268 +83,168 @@ func init() {
|
||||
}
|
||||
allowedImportsStr = strings.Join(all, "\n")
|
||||
|
||||
for n, s := range kindsys.SchemaInterfaces(nil) {
|
||||
allslots = append(allslots, slotandname{
|
||||
name: n,
|
||||
slot: s,
|
||||
})
|
||||
for _, s := range kindsys.SchemaInterfaces(nil) {
|
||||
allsi = append(allsi, s)
|
||||
}
|
||||
|
||||
sort.Slice(allslots, func(i, j int) bool {
|
||||
return allslots[i].name < allslots[j].name
|
||||
sort.Slice(allsi, func(i, j int) bool {
|
||||
return allsi[i].Name() < allsi[j].Name()
|
||||
})
|
||||
}
|
||||
|
||||
// Tree represents the contents of a plugin filesystem tree.
|
||||
type Tree struct {
|
||||
raw fs.FS
|
||||
rootinfo PluginInfo
|
||||
}
|
||||
|
||||
func (t *Tree) FS() fs.FS {
|
||||
return t.raw
|
||||
}
|
||||
|
||||
func (t *Tree) RootPlugin() PluginInfo {
|
||||
return t.rootinfo
|
||||
}
|
||||
|
||||
// SubPlugins returned a map of the PluginInfos for subplugins
|
||||
// within the tree, if any, keyed by subpath.
|
||||
func (t *Tree) SubPlugins() map[string]PluginInfo {
|
||||
// TODO implement these once ParsePluginFS descends
|
||||
return nil
|
||||
}
|
||||
|
||||
// TreeList is a slice of validated plugin fs Trees with helper methods
|
||||
// for filtering to particular subsets of its members.
|
||||
type TreeList []*Tree
|
||||
|
||||
// LineagesForSlot returns the set of plugin-defined lineages that implement a
|
||||
// particular named Grafana slot (See ["github.com/grafana/grafana/pkg/framework/coremodel".SchemaInterface]).
|
||||
func (tl TreeList) LineagesForSlot(slotname string) map[string]thema.Lineage {
|
||||
m := make(map[string]thema.Lineage)
|
||||
for _, tree := range tl {
|
||||
rootp := tree.RootPlugin()
|
||||
rid := rootp.Meta().Id
|
||||
|
||||
if lin, has := rootp.SlotImplementations()[slotname]; has {
|
||||
m[rid] = lin
|
||||
}
|
||||
// ParsePluginFS takes a virtual filesystem and checks that it contains a valid
|
||||
// set of files that statically define a Grafana plugin.
|
||||
//
|
||||
// The fsys must contain a plugin.json at the root, which must be valid
|
||||
// according to the [plugindef] schema. If any .cue files exist in the
|
||||
// grafanaplugin package, these will also be loaded and validated according to
|
||||
// the [GrafanaPlugin] specification. This includes the validation of any custom
|
||||
// or composable kinds and their contained lineages, via [thema.BindLineage].
|
||||
//
|
||||
// This function parses exactly one plugin. It does not descend into
|
||||
// subdirectories to search for additional plugin.json or .cue files.
|
||||
//
|
||||
// Calling this with a nil [thema.Runtime] (the singleton returned from
|
||||
// [cuectx.GrafanaThemaRuntime] is used) will memoize certain CUE operations.
|
||||
// Prefer passing nil unless a different thema.Runtime is specifically required.
|
||||
//
|
||||
// [GrafanaPlugin]: https://github.com/grafana/grafana/blob/main/pkg/plugins/pfs/grafanaplugin.cue
|
||||
func ParsePluginFS(fsys fs.FS, rt *thema.Runtime) (ParsedPlugin, error) {
|
||||
if fsys == nil {
|
||||
return ParsedPlugin{}, ErrEmptyFS
|
||||
}
|
||||
if rt == nil {
|
||||
rt = cuectx.GrafanaThemaRuntime()
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// PluginInfo represents everything knowable about a single plugin from static
|
||||
// analysis of its filesystem tree contents.
|
||||
type PluginInfo struct {
|
||||
meta plugindef.PluginDef
|
||||
slotimpls map[string]thema.Lineage
|
||||
imports []*ast.ImportSpec
|
||||
}
|
||||
|
||||
// CUEImports lists the CUE import statements in the plugin's models.cue file,
|
||||
// if any.
|
||||
func (pi PluginInfo) CUEImports() []*ast.ImportSpec {
|
||||
return pi.imports
|
||||
}
|
||||
|
||||
// SlotImplementations returns a map of the plugin's Thema lineages that
|
||||
// implement particular slots, keyed by the name of the slot.
|
||||
//
|
||||
// Returns an empty map if the plugin has not implemented any slots.
|
||||
func (pi PluginInfo) SlotImplementations() map[string]thema.Lineage {
|
||||
return pi.slotimpls
|
||||
}
|
||||
|
||||
// Meta returns the metadata declared in the plugin's plugin.json file.
|
||||
func (pi PluginInfo) Meta() plugindef.PluginDef {
|
||||
return pi.meta
|
||||
}
|
||||
|
||||
// ParsePluginFS takes an fs.FS and checks that it represents exactly one valid
|
||||
// plugin fs tree, with the fs.FS root as the root of the tree.
|
||||
//
|
||||
// It does not descend into subdirectories to search for additional plugin.json
|
||||
// files.
|
||||
//
|
||||
// Calling this with a nil thema.Runtime will take advantage of memoization.
|
||||
// Prefer this approach unless a different thema.Runtime is specifically
|
||||
// required.
|
||||
//
|
||||
// TODO no descent is ok for core plugins, but won't cut it in general
|
||||
func ParsePluginFS(f fs.FS, rt *thema.Runtime) (*Tree, error) {
|
||||
if f == nil {
|
||||
return nil, ErrEmptyFS
|
||||
}
|
||||
lin, err := plugindef.Lineage(rt)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("plugindef lineage is invalid or broken, needs dev attention: %s", err))
|
||||
}
|
||||
mux := vmux.NewValueMux(lin.TypedSchema(), vmux.NewJSONCodec("plugin.json"))
|
||||
ctx := rt.Context()
|
||||
|
||||
b, err := fs.ReadFile(f, "plugin.json")
|
||||
b, err := fs.ReadFile(fsys, "plugin.json")
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, ErrNoRootFile
|
||||
return ParsedPlugin{}, ErrNoRootFile
|
||||
}
|
||||
return nil, fmt.Errorf("error reading plugin.json: %w", err)
|
||||
return ParsedPlugin{}, fmt.Errorf("error reading plugin.json: %w", err)
|
||||
}
|
||||
|
||||
tree := &Tree{
|
||||
raw: f,
|
||||
rootinfo: PluginInfo{
|
||||
slotimpls: make(map[string]thema.Lineage),
|
||||
},
|
||||
pp := ParsedPlugin{
|
||||
ComposableKinds: make(map[string]kindsys.Composable),
|
||||
// CustomKinds: make(map[string]kindsys.Custom),
|
||||
}
|
||||
r := &tree.rootinfo
|
||||
|
||||
// Pass the raw bytes into the muxer, get the populated PluginDef type out that we want.
|
||||
// TODO stop ignoring second return. (for now, lacunas are a WIP and can't occur until there's >1 schema in the plugindef lineage)
|
||||
pmeta, _, err := mux(b)
|
||||
pinst, _, err := vmux.NewTypedMux(lin.TypedSchema(), vmux.NewJSONCodec("plugin.json"))(b)
|
||||
if err != nil {
|
||||
// TODO more nuanced error handling by class of Thema failure
|
||||
return nil, ewrap(err, ErrInvalidRootFile)
|
||||
return ParsedPlugin{}, errors.Wrap(errors.Promote(err, ""), ErrInvalidRootFile)
|
||||
}
|
||||
pp.Properties = *(pinst.ValueP())
|
||||
// FIXME remove this once it's being correctly populated coming out of lineage
|
||||
if pp.Properties.PascalName == "" {
|
||||
pp.Properties.PascalName = plugindef.DerivePascalName(pp.Properties)
|
||||
}
|
||||
r.meta = *pmeta
|
||||
|
||||
if modbyt, err := fs.ReadFile(f, "models.cue"); err == nil {
|
||||
// TODO introduce layered CUE dependency-injecting loader
|
||||
//
|
||||
// Until CUE has proper dependency management (and possibly even after), loading
|
||||
// CUE files with non-stdlib imports requires injecting the imported packages
|
||||
// into cue.mod/pkg/<import path>, unless the imports are within the same CUE
|
||||
// module. Thema introduced a system for this for its dependers, which we use
|
||||
// here, but we'll need to layer the same on top for importable Grafana packages.
|
||||
// Needing to do this twice strongly suggests it needs a generic, standalone
|
||||
// library.
|
||||
if cuefiles, err := fs.Glob(fsys, "*.cue"); err != nil {
|
||||
return ParsedPlugin{}, fmt.Errorf("error globbing for cue files in fsys: %w", err)
|
||||
} else if len(cuefiles) == 0 {
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
mfs := merged_fs.NewMergedFS(f, grafana.CueSchemaFS)
|
||||
gpv := loadGP(rt.Context())
|
||||
|
||||
// Note that this actually will load any .cue files in the fs.FS root dir in the plugindef.PkgName.
|
||||
// That's...maybe good? But not what it says on the tin
|
||||
bi, err := load.InstanceWithThema(mfs, "", load.Package(plugindef.PkgName))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading models.cue failed: %w", err)
|
||||
fsys, err = ensureCueMod(fsys, pp.Properties)
|
||||
if err != nil {
|
||||
return ParsedPlugin{}, fmt.Errorf("%s has invalid cue.mod: %w", pp.Properties.Id, err)
|
||||
}
|
||||
|
||||
bi, err := cuectx.LoadInstanceWithGrafana(fsys, "", load.Package(PackageName))
|
||||
if err != nil || bi.Err != nil {
|
||||
if err == nil {
|
||||
err = bi.Err
|
||||
}
|
||||
return ParsedPlugin{}, errors.Wrap(errors.Newf(token.NoPos, "%s did not load", pp.Properties.Id), err)
|
||||
}
|
||||
|
||||
pf, _ := parser.ParseFile("models.cue", modbyt, parser.ParseComments)
|
||||
f, _ := parser.ParseFile("plugin.json", fmt.Sprintf(`{
|
||||
"id": %q,
|
||||
"pascalName": %q
|
||||
}`, pp.Properties.Id, pp.Properties.PascalName))
|
||||
|
||||
for _, im := range pf.Imports {
|
||||
for _, f := range bi.Files {
|
||||
for _, im := range f.Imports {
|
||||
ip := strings.Trim(im.Path.Value, "\"")
|
||||
if !importAllowed(ip) {
|
||||
return nil, ewrap(errors.Newf(im.Pos(), "import %q in models.cue not allowed, plugins may only import from:\n%s\n", ip, allowedImportsStr), ErrDisallowedCUEImport)
|
||||
}
|
||||
r.imports = append(r.imports, im)
|
||||
}
|
||||
|
||||
val := ctx.BuildInstance(bi)
|
||||
if val.Err() != nil {
|
||||
return nil, ewrap(fmt.Errorf("models.cue is invalid CUE: %w", val.Err()), ErrInvalidCUE)
|
||||
}
|
||||
for _, s := range allslots {
|
||||
iv := val.LookupPath(cue.ParsePath(s.slot.Name()))
|
||||
if iv.Exists() {
|
||||
lin, err := bindSlotLineage(iv, s.slot, r.meta, rt)
|
||||
if lin != nil {
|
||||
r.slotimpls[s.slot.Name()] = lin
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParsedPlugin{}, errors.Wrap(errors.Newf(im.Pos(),
|
||||
"import of %q in grafanaplugin cue package not allowed, plugins may only import from:\n%s\n", ip, allowedImportsStr),
|
||||
ErrDisallowedCUEImport)
|
||||
}
|
||||
pp.CUEImports = append(pp.CUEImports, im)
|
||||
}
|
||||
}
|
||||
|
||||
return tree, nil
|
||||
}
|
||||
// build.Instance.Files has a comment indicating the CUE authors want to change
|
||||
// its behavior. This is a tripwire to tell us if/when they do that - otherwise, if
|
||||
// the change they make ends up making bi.Files empty, the above loop will silently
|
||||
// become a no-op, and we'd lose enforcement of import restrictions in plugins without
|
||||
// realizing it.
|
||||
if len(bi.Files) != len(bi.BuildFiles) {
|
||||
panic("Refactor required - upstream CUE implementation changed, bi.Files is no longer populated")
|
||||
}
|
||||
|
||||
func bindSlotLineage(v cue.Value, s kindsys.SchemaInterface, meta plugindef.PluginDef, rt *thema.Runtime, opts ...thema.BindOption) (thema.Lineage, error) {
|
||||
// temporarily keep this around, there are IMMEDIATE plans to refactor
|
||||
var required bool
|
||||
accept := s.Should(string(meta.Type))
|
||||
exists := v.Exists()
|
||||
// Inject the JSON directly into the build so it gets loaded together
|
||||
bi.BuildFiles = append(bi.BuildFiles, &build.File{
|
||||
Filename: "plugin.json",
|
||||
Encoding: build.JSON,
|
||||
Form: build.Data,
|
||||
Source: b,
|
||||
})
|
||||
bi.Files = append(bi.Files, f)
|
||||
|
||||
if !accept {
|
||||
if exists {
|
||||
// If it's not accepted for the type, but is declared, error out. This keeps a
|
||||
// precise boundary on what's actually expected for plugins to do, which makes
|
||||
// for clearer docs and guarantees for users.
|
||||
return nil, ewrap(fmt.Errorf("%s: %s plugins may not provide a %s slot implementation in models.cue", meta.Id, meta.Type, s.Name()), ErrImplementedSlots)
|
||||
gpi := ctx.BuildInstance(bi).Unify(gpv)
|
||||
if gpi.Err() != nil {
|
||||
return ParsedPlugin{}, errors.Wrap(errors.Promote(ErrInvalidGrafanaPluginInstance, pp.Properties.Id), gpi.Err())
|
||||
}
|
||||
|
||||
for _, si := range allsi {
|
||||
iv := gpi.LookupPath(cue.MakePath(cue.Str("composableKinds"), cue.Str(si.Name())))
|
||||
if !iv.Exists() {
|
||||
continue
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !exists && required {
|
||||
return nil, ewrap(fmt.Errorf("%s: %s plugins must provide a %s slot implementation in models.cue", meta.Id, meta.Type, s.Name()), ErrImplementedSlots)
|
||||
}
|
||||
|
||||
// TODO make this opt real in thema, then uncomment to enforce joinSchema
|
||||
// lin, err := thema.BindLineage(iv, rt, thema.SatisfiesJoinSchema(s.MetaSchema()))
|
||||
lin, err := thema.BindLineage(v, rt, opts...)
|
||||
if err != nil {
|
||||
return nil, ewrap(fmt.Errorf("%s: invalid thema lineage for slot %s: %w", meta.Id, s.Name(), err), ErrInvalidLineage)
|
||||
}
|
||||
|
||||
sanid := sanitizePluginId(meta.Id)
|
||||
if lin.Name() != sanid {
|
||||
errf := func(format string, args ...interface{}) error {
|
||||
var errin error
|
||||
if n := v.LookupPath(cue.ParsePath("name")).Source(); n != nil {
|
||||
errin = errors.Newf(n.Pos(), format, args...)
|
||||
} else {
|
||||
errin = fmt.Errorf(format, args...)
|
||||
}
|
||||
return ewrap(errin, ErrLineageNameMismatch)
|
||||
props, err := kindsys.ToKindProps[kindsys.ComposableProperties](iv)
|
||||
if err != nil {
|
||||
return ParsedPlugin{}, err
|
||||
}
|
||||
if sanid != meta.Id {
|
||||
return nil, errf("%s: %q slot lineage name must be the sanitized plugin id (%q), got %q", meta.Id, s.Name(), sanid, lin.Name())
|
||||
} else {
|
||||
return nil, errf("%s: %q slot lineage name must be the plugin id, got %q", meta.Id, s.Name(), lin.Name())
|
||||
|
||||
compo, err := kindsys.BindComposable(rt, kindsys.Decl[kindsys.ComposableProperties]{
|
||||
Properties: props,
|
||||
V: iv,
|
||||
})
|
||||
if err != nil {
|
||||
return ParsedPlugin{}, err
|
||||
}
|
||||
pp.ComposableKinds[si.Name()] = compo
|
||||
}
|
||||
return lin, nil
|
||||
|
||||
// TODO custom kinds
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
// Plugin IDs are allowed to contain characters that aren't allowed in thema
|
||||
// Lineage names, CUE package names, Go package names, TS or Go type names, etc.
|
||||
func sanitizePluginId(s string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
fallthrough
|
||||
case r >= 'A' && r <= 'Z':
|
||||
fallthrough
|
||||
case r >= '0' && r <= '9':
|
||||
fallthrough
|
||||
case r == '_':
|
||||
return r
|
||||
case r == '-':
|
||||
return '_'
|
||||
default:
|
||||
return -1
|
||||
func ensureCueMod(fsys fs.FS, pdef plugindef.PluginDef) (fs.FS, error) {
|
||||
if modf, err := fs.ReadFile(fsys, filepath.Join("cue.mod", "module.cue")); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
}, s)
|
||||
}
|
||||
|
||||
func ewrap(actual, is error) error {
|
||||
return &errPassthrough{
|
||||
actual: actual,
|
||||
is: is,
|
||||
return merged_fs.NewMergedFS(fsys, fstest.MapFS{
|
||||
"cue.mod/module.cue": &fstest.MapFile{Data: []byte(fmt.Sprintf(`module: "grafana.com/grafana/plugins/%s"`, pdef.Id))},
|
||||
}), nil
|
||||
} else if _, err := cuecontext.New().CompileBytes(modf).LookupPath(cue.MakePath(cue.Str("module"))).String(); err != nil {
|
||||
return nil, fmt.Errorf("error reading cue module name: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
type errPassthrough struct {
|
||||
actual error
|
||||
is error
|
||||
}
|
||||
|
||||
func (e *errPassthrough) Is(err error) bool {
|
||||
return errors.Is(err, e.actual) || errors.Is(err, e.is)
|
||||
}
|
||||
|
||||
func (e *errPassthrough) Error() string {
|
||||
return e.actual.Error()
|
||||
return fsys, nil
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseTreeTestdata(t *testing.T) {
|
||||
func TestParsePluginTestdata(t *testing.T) {
|
||||
type tt struct {
|
||||
tfs fs.FS
|
||||
// TODO could remove this by getting rid of inconsistent subdirs
|
||||
@ -110,11 +110,8 @@ func TestParseTreeTestdata(t *testing.T) {
|
||||
"no-rootfile": {
|
||||
err: ErrNoRootFile,
|
||||
},
|
||||
"valid-model-panel": {},
|
||||
"valid-model-datasource": {},
|
||||
"wrong-slot-panel": {
|
||||
err: ErrImplementedSlots,
|
||||
},
|
||||
"valid-model-panel": {},
|
||||
"valid-model-datasource": {},
|
||||
"missing-kind-datasource": {},
|
||||
"panel-conflicting-joinschema": {
|
||||
err: ErrInvalidLineage,
|
||||
@ -124,11 +121,8 @@ func TestParseTreeTestdata(t *testing.T) {
|
||||
err: ErrInvalidLineage,
|
||||
skip: "TODO implement BindOption in thema, SatisfiesJoinSchema, then use it here",
|
||||
},
|
||||
"name-id-mismatch": {
|
||||
err: ErrLineageNameMismatch,
|
||||
},
|
||||
"mismatch": {
|
||||
err: ErrLineageNameMismatch,
|
||||
"name-mismatch-panel": {
|
||||
err: ErrInvalidGrafanaPluginInstance,
|
||||
},
|
||||
"disallowed-cue-import": {
|
||||
err: ErrDisallowedCUEImport,
|
||||
@ -170,11 +164,12 @@ func TestParseTreeTestdata(t *testing.T) {
|
||||
t.Skip(tst.skip)
|
||||
}
|
||||
|
||||
tree, err := ParsePluginFS(tst.tfs, lib)
|
||||
pp, err := ParsePluginFS(tst.tfs, lib)
|
||||
if tst.err == nil {
|
||||
require.NoError(t, err, "unexpected error while parsing plugin tree")
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
t.Logf("%T %s", err, err)
|
||||
require.ErrorIs(t, err, tst.err, "unexpected error type while parsing plugin tree")
|
||||
return
|
||||
}
|
||||
@ -183,8 +178,7 @@ func TestParseTreeTestdata(t *testing.T) {
|
||||
tst.rootid = name
|
||||
}
|
||||
|
||||
rootp := tree.RootPlugin()
|
||||
require.Equal(t, tst.rootid, rootp.Meta().Id, "expected root plugin id and actual root plugin id differ")
|
||||
require.Equal(t, tst.rootid, pp.Properties.Id, "expected plugin id and actual plugin id differ")
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -270,11 +264,11 @@ func TestParseTreeZips(t *testing.T) {
|
||||
t.Skip(tst.skip)
|
||||
}
|
||||
|
||||
tree, err := ParsePluginFS(tst.tfs, lib)
|
||||
pp, err := ParsePluginFS(tst.tfs, lib)
|
||||
if tst.err == nil {
|
||||
require.NoError(t, err, "unexpected error while parsing plugin tree")
|
||||
require.NoError(t, err, "unexpected error while parsing plugin fs")
|
||||
} else {
|
||||
require.ErrorIs(t, err, tst.err, "unexpected error type while parsing plugin tree")
|
||||
require.ErrorIs(t, err, tst.err, "unexpected error type while parsing plugin fs")
|
||||
return
|
||||
}
|
||||
|
||||
@ -282,8 +276,7 @@ func TestParseTreeZips(t *testing.T) {
|
||||
tst.rootid = name
|
||||
}
|
||||
|
||||
rootp := tree.RootPlugin()
|
||||
require.Equal(t, tst.rootid, rootp.Meta().Id, "expected root plugin id and actual root plugin id differ")
|
||||
require.Equal(t, tst.rootid, pp.Properties.Id, "expected plugin id and actual plugin id differ")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
48
pkg/plugins/pfs/plugin.go
Normal file
48
pkg/plugins/pfs/plugin.go
Normal file
@ -0,0 +1,48 @@
|
||||
package pfs
|
||||
|
||||
import (
|
||||
"cuelang.org/go/cue/ast"
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
"github.com/grafana/grafana/pkg/plugins/plugindef"
|
||||
)
|
||||
|
||||
// ParsedPlugin represents everything knowable about a single plugin from static
|
||||
// analysis of its filesystem tree contents, as performed by [ParsePluginFS].
|
||||
//
|
||||
// Guarantees described in the below comments only exist for instances of this
|
||||
// struct returned from [ParsePluginFS].
|
||||
type ParsedPlugin struct {
|
||||
// Properties contains the plugin's definition, as declared in plugin.json.
|
||||
Properties plugindef.PluginDef
|
||||
|
||||
// ComposableKinds is a map of all the composable kinds declared in this plugin.
|
||||
// Keys are the name of the [kindsys.SchemaInterface] implemented by the value.
|
||||
//
|
||||
// Composable kind defs are only populated in this map by [ParsePluginFS] if
|
||||
// they are implementations of a known schema interface, or are for
|
||||
// an unknown schema interface.
|
||||
ComposableKinds map[string]kindsys.Composable
|
||||
|
||||
// CustomKinds is a map of all the custom kinds declared in this plugin.
|
||||
// Keys are the machineName of the custom kind.
|
||||
// CustomKinds map[string]kindsys.Custom
|
||||
|
||||
// CUEImports lists the CUE import statements in the plugin's grafanaplugin CUE
|
||||
// package, if any.
|
||||
CUEImports []*ast.ImportSpec
|
||||
}
|
||||
|
||||
// TODO is this static approach worth using, akin to core generated registries? instead of the ParsedPlugins.ComposableKinds map? in addition to it?
|
||||
// ComposableKinds represents all the possible composable kinds that may be
|
||||
// defined in a Grafana plugin.
|
||||
//
|
||||
// The value of each field, if non-nil, is a standard [kindsys.Decl]
|
||||
// representing a CUE definition of a composable kind that implements the
|
||||
// schema interface corresponding to the field's name. (This invariant is
|
||||
// only enforced in [ComposableKinds] returned from [ParsePluginFS].)
|
||||
//
|
||||
// type ComposableKinds struct {
|
||||
// PanelCfg kindsys.Decl[kindsys.ComposableProperties]
|
||||
// Queries kindsys.Decl[kindsys.ComposableProperties]
|
||||
// DSCfg kindsys.Decl[kindsys.ComposableProperties]
|
||||
// }
|
31
pkg/plugins/pfs/plugin_test.go
Normal file
31
pkg/plugins/pfs/plugin_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package pfs
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/grafana/grafana/pkg/kindsys"
|
||||
)
|
||||
|
||||
// This is a brick-dumb test that just ensures known schema interfaces are being
|
||||
// loaded correctly from their declarations in .cue files.
|
||||
//
|
||||
// If this test fails, it's either because:
|
||||
// - They're not being loaded correctly - there's a bug in kindsys or pfs somewhere, fix it
|
||||
// - The set of schema interfaces has been modified - update the static list here
|
||||
func TestSchemaInterfacesAreLoaded(t *testing.T) {
|
||||
knownSI := []string{"PanelCfg", "DataQuery", "DataSourceCfg"}
|
||||
all := kindsys.SchemaInterfaces(nil)
|
||||
var loadedSI []string
|
||||
for k := range all {
|
||||
loadedSI = append(loadedSI, k)
|
||||
}
|
||||
|
||||
sort.Strings(knownSI)
|
||||
sort.Strings(loadedSI)
|
||||
|
||||
if diff := cmp.Diff(knownSI, loadedSI); diff != "" {
|
||||
t.Fatalf("kindsys cue-declared schema interfaces differ from ComposableKinds go struct:\n%s", diff)
|
||||
}
|
||||
}
|
43
pkg/plugins/plugindef/pascal_test.go
Normal file
43
pkg/plugins/plugindef/pascal_test.go
Normal file
@ -0,0 +1,43 @@
|
||||
package plugindef
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDerivePascal(t *testing.T) {
|
||||
table := []struct {
|
||||
id, name, out string
|
||||
}{
|
||||
{
|
||||
name: "-- Grafana --",
|
||||
out: "Grafana",
|
||||
},
|
||||
{
|
||||
name: "A weird/Thing",
|
||||
out: "AWeirdThing",
|
||||
},
|
||||
{
|
||||
name: "/",
|
||||
out: "Empty",
|
||||
},
|
||||
{
|
||||
name: "some really Long thing WHY would38883 anyone do this i don't know but hey It seems like it this is just going on and",
|
||||
out: "SomeReallyLongThingWHYWouldAnyoneDoThisIDonTKnowButHeyItSeemsLi",
|
||||
},
|
||||
}
|
||||
|
||||
for _, row := range table {
|
||||
if row.id == "" {
|
||||
row.id = "default-empty-panel"
|
||||
}
|
||||
|
||||
pd := PluginDef{
|
||||
Id: row.id,
|
||||
Name: row.name,
|
||||
}
|
||||
|
||||
require.Equal(t, row.out, DerivePascalName(pd))
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package plugindef
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"regexp"
|
||||
|
||||
"github.com/grafana/thema"
|
||||
)
|
||||
@ -34,6 +35,18 @@ seqs: [
|
||||
// the UI.
|
||||
name: string
|
||||
|
||||
// FIXME there appears to be a bug in thema that prevents this from working. Maybe it'd
|
||||
// help to refer to it with an alias, but thema can't support using current list syntax.
|
||||
// syntax (fixed by grafana/thema#82). Either way, for now, pascalName gets populated in Go.
|
||||
let sani = (strings.ToTitle(regexp.ReplaceAllLiteral("[^a-zA-Z]+", name, "")))
|
||||
|
||||
// The PascalCase name for the plugin. Used for creating machine-friendly
|
||||
// identifiers, typically in code generation.
|
||||
//
|
||||
// If not provided, defaults to name, but title-cased and sanitized (only
|
||||
// alphabetical characters allowed).
|
||||
pascalName: string & =~"^([A-Z][a-zA-Z]{1,62})$" | *sani
|
||||
|
||||
// Plugin category used on the Add data source page.
|
||||
category?: "tsdb" | "logging" | "cloud" | "tracing" | "sql" | "enterprise" | "profiling" | "other"
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package plugindef
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"cuelang.org/go/cue/build"
|
||||
@ -10,10 +11,6 @@ import (
|
||||
|
||||
//go:generate go run gen.go
|
||||
|
||||
// PkgName is the name of the CUE package that Grafana will load when looking
|
||||
// for kind declarations by a Grafana plugin.
|
||||
const PkgName = "grafanaplugin"
|
||||
|
||||
func loadInstanceForplugindef() (*build.Instance, error) {
|
||||
return cuectx.LoadGrafanaInstance("pkg/plugins/plugindef", "", nil)
|
||||
}
|
||||
@ -36,3 +33,41 @@ func Lineage(rt *thema.Runtime, opts ...thema.BindOption) (thema.ConvergentLinea
|
||||
}
|
||||
return doLineage(rt, opts...)
|
||||
}
|
||||
|
||||
// DerivePascalName derives a PascalCase name from a PluginDef.
|
||||
//
|
||||
// This function does not mutate the input PluginDef; as such, it ignores
|
||||
// whether there exists any value for PluginDef.PascalName.
|
||||
//
|
||||
// FIXME this should be removable once CUE logic for it works/unmarshals correctly.
|
||||
func DerivePascalName(pd PluginDef) string {
|
||||
sani := func(s string) string {
|
||||
ret := strings.Title(strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z':
|
||||
return r
|
||||
case r >= 'A' && r <= 'Z':
|
||||
return r
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}, strings.Title(strings.Map(func(r rune) rune {
|
||||
switch r {
|
||||
case '-', '_':
|
||||
return ' '
|
||||
default:
|
||||
return r
|
||||
}
|
||||
}, s))))
|
||||
if len(ret) > 63 {
|
||||
return ret[:63]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
fromname := sani(pd.Name)
|
||||
if len(fromname) != 0 {
|
||||
return fromname
|
||||
}
|
||||
return sani(strings.Split(pd.Id, "-")[1])
|
||||
}
|
||||
|
@ -432,6 +432,13 @@ type PluginDef struct {
|
||||
// the UI.
|
||||
Name string `json:"name"`
|
||||
|
||||
// The PascalCase name for the plugin. Used for creating machine-friendly
|
||||
// identifiers, typically in code generation.
|
||||
//
|
||||
// If not provided, defaults to name, but title-cased and sanitized (only
|
||||
// alphabetical characters allowed).
|
||||
PascalName string `json:"pascalName"`
|
||||
|
||||
// Initialize plugin on startup. By default, the plugin
|
||||
// initializes on first use.
|
||||
Preload *bool `json:"preload,omitempty"`
|
||||
|
@ -54,7 +54,7 @@ func main() {
|
||||
|
||||
pluginKindGen.Append(
|
||||
codegen.PluginTreeListJenny(),
|
||||
codegen.PluginGoTypesJenny("pkg/tsdb", adaptToPipeline(corecodegen.GoTypesJenny{})),
|
||||
codegen.PluginGoTypesJenny("pkg/tsdb", adaptToPipeline(corecodegen.GoTypesJenny{ExpandReferences: true})),
|
||||
codegen.PluginTSTypesJenny("public/app/plugins", adaptToPipeline(corecodegen.TSTypesJenny{})),
|
||||
)
|
||||
|
||||
|
@ -14,28 +14,29 @@
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
composableKinds: PanelCfg: {
|
||||
maturity: "experimental"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "annolist"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
onlyFromThisDashboard: bool | *false
|
||||
onlyInTimeRange: bool | *false
|
||||
tags: [...string]
|
||||
limit: uint32 | *10
|
||||
showUser: bool | *true
|
||||
showTime: bool | *true
|
||||
showTags: bool | *true
|
||||
navigateToPanel: bool | *true
|
||||
navigateBefore: string | *"10m"
|
||||
navigateAfter: string | *"10m"
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
onlyFromThisDashboard: bool | *false
|
||||
onlyInTimeRange: bool | *false
|
||||
tags: [...string]
|
||||
limit: uint32 | *10
|
||||
showUser: bool | *true
|
||||
showTime: bool | *true
|
||||
showTags: bool | *true
|
||||
navigateToPanel: bool | *true
|
||||
navigateBefore: string | *"10m"
|
||||
navigateAfter: string | *"10m"
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
//
|
||||
// Run 'make gen-cue' from repository root to regenerate.
|
||||
|
||||
export const PanelModelVersion = Object.freeze([0, 0]);
|
||||
export const PanelCfgModelVersion = Object.freeze([0, 0]);
|
||||
|
||||
export interface PanelOptions {
|
||||
limit: number;
|
||||
|
81
public/app/plugins/panel/barchart/composable_panelcfg.cue
Normal file
81
public/app/plugins/panel/barchart/composable_panelcfg.cue
Normal file
@ -0,0 +1,81 @@
|
||||
// Copyright 2022 Grafana Labs
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
composableKinds: PanelCfg: {
|
||||
maturity: "experimental"
|
||||
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
// v0.0
|
||||
{
|
||||
PanelOptions: {
|
||||
ui.OptionsWithLegend
|
||||
ui.OptionsWithTooltip
|
||||
ui.OptionsWithTextFormatting
|
||||
|
||||
// TODO docs
|
||||
xField?: string
|
||||
// TODO docs
|
||||
colorByField?: string
|
||||
// TODO docs
|
||||
orientation: ui.VizOrientation | *"auto"
|
||||
// TODO docs
|
||||
barRadius?: float64 & >=0 & <=0.5 | *0
|
||||
// TODO docs
|
||||
xTickLabelRotation: int32 & >=-90 & <=90 | *0
|
||||
// TODO docs
|
||||
xTickLabelMaxLength: int32 & >=0
|
||||
// TODO docs
|
||||
// negative values indicate backwards skipping behavior
|
||||
xTickLabelSpacing?: int32 | *0
|
||||
// TODO docs
|
||||
stacking: ui.StackingMode | *"none"
|
||||
// This controls whether values are shown on top or to the left of bars.
|
||||
showValue: ui.VisibilityMode | *"auto"
|
||||
// Controls the width of bars. 1 = Max width, 0 = Min width.
|
||||
barWidth: float64 & >=0 & <=1 | *0.97
|
||||
// Controls the width of groups. 1 = max with, 0 = min width.
|
||||
groupWidth: float64 & >=0 & <=1 | *0.7
|
||||
// Enables mode which highlights the entire bar area and shows tooltip when cursor
|
||||
// hovers over highlighted area
|
||||
fullHighlight: bool | *false
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: {
|
||||
ui.AxisConfig
|
||||
ui.HideableFieldConfig
|
||||
|
||||
// Controls line width of the bars.
|
||||
lineWidth?: int32 & >=0 & <=10 | *1
|
||||
// Controls the fill opacity of the bars.
|
||||
fillOpacity?: int32 & >=0 & <=100 | *80
|
||||
// Set the mode of the gradient fill. Fill gradient is based on the line color. To change the color, use the standard color scheme field option.
|
||||
// Gradient appearance is influenced by the Fill opacity setting.
|
||||
gradientMode?: ui.GraphGradientMode | *"none"
|
||||
// Threshold rendering
|
||||
thresholdsStyle?: ui.GraphThresholdsStyleConfig
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
// Copyright 2022 Grafana Labs
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "barchart"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
// v0.0
|
||||
{
|
||||
PanelOptions: {
|
||||
ui.OptionsWithLegend
|
||||
ui.OptionsWithTooltip
|
||||
ui.OptionsWithTextFormatting
|
||||
// TODO docs
|
||||
xField?: string
|
||||
// TODO docs
|
||||
colorByField?: string
|
||||
// TODO docs
|
||||
orientation: ui.VizOrientation | *"auto"
|
||||
// TODO docs
|
||||
barRadius?: float64 & >= 0 & <= 0.5 | *0
|
||||
// TODO docs
|
||||
xTickLabelRotation: int32 & >= -90 & <= 90 | *0
|
||||
// TODO docs
|
||||
xTickLabelMaxLength: int32 & >= 0
|
||||
// TODO docs
|
||||
// negative values indicate backwards skipping behavior
|
||||
xTickLabelSpacing?: int32 | *0
|
||||
// TODO docs
|
||||
stacking: ui.StackingMode | *"none"
|
||||
// This controls whether values are shown on top or to the left of bars.
|
||||
showValue: ui.VisibilityMode | *"auto"
|
||||
// Controls the width of bars. 1 = Max width, 0 = Min width.
|
||||
barWidth: float64 & >= 0 & <= 1 | *0.97
|
||||
// Controls the width of groups. 1 = max with, 0 = min width.
|
||||
groupWidth: float64 & >= 0 & <= 1 | *0.7
|
||||
// Enables mode which highlights the entire bar area and shows tooltip when cursor hovers over highlighted area
|
||||
fullHighlight: bool | *false
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: {
|
||||
ui.AxisConfig
|
||||
ui.HideableFieldConfig
|
||||
// Controls line width of the bars.
|
||||
lineWidth?: int32 & >= 0 & <= 10 | *1
|
||||
// Controls the fill opacity of the bars.
|
||||
fillOpacity?: int32 & >= 0 & <= 100 | *80
|
||||
// Set the mode of the gradient fill. Fill gradient is based on the line color. To change the color, use the standard color scheme field option.
|
||||
// Gradient appearance is influenced by the Fill opacity setting.
|
||||
gradientMode?: ui.GraphGradientMode | *"none"
|
||||
// Threshold rendering
|
||||
thresholdsStyle?: ui.GraphThresholdsStyleConfig
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
|
||||
import * as ui from '@grafana/schema';
|
||||
|
||||
export const PanelModelVersion = Object.freeze([0, 0]);
|
||||
export const PanelCfgModelVersion = Object.freeze([0, 0]);
|
||||
|
||||
export interface PanelOptions extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTextFormatting {
|
||||
/**
|
||||
@ -26,7 +26,8 @@ export interface PanelOptions extends ui.OptionsWithLegend, ui.OptionsWithToolti
|
||||
*/
|
||||
colorByField?: string;
|
||||
/**
|
||||
* Enables mode which highlights the entire bar area and shows tooltip when cursor hovers over highlighted area
|
||||
* Enables mode which highlights the entire bar area and shows tooltip when cursor
|
||||
* hovers over highlighted area
|
||||
*/
|
||||
fullHighlight: boolean;
|
||||
/**
|
||||
|
@ -15,25 +15,27 @@
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "bargauge"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
ui.SingleStatBaseOptions
|
||||
displayMode: ui.BarGaugeDisplayMode | *"gradient"
|
||||
showUnfilled: bool | *true
|
||||
minVizWidth: uint32 | *0
|
||||
minVizHeight: uint32 | *10
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
composableKinds: PanelCfg: {
|
||||
maturity: "experimental"
|
||||
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
ui.SingleStatBaseOptions
|
||||
displayMode: ui.BarGaugeDisplayMode | *"gradient"
|
||||
showUnfilled: bool | *true
|
||||
minVizWidth: uint32 | *0
|
||||
minVizHeight: uint32 | *10
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
|
||||
import * as ui from '@grafana/schema';
|
||||
|
||||
export const PanelModelVersion = Object.freeze([0, 0]);
|
||||
export const PanelCfgModelVersion = Object.freeze([0, 0]);
|
||||
|
||||
export interface PanelOptions extends ui.SingleStatBaseOptions {
|
||||
displayMode: ui.BarGaugeDisplayMode;
|
||||
|
@ -14,21 +14,23 @@
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "news"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
// empty/missing will default to grafana blog
|
||||
feedUrl?: string
|
||||
showImage?: bool | *true
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
// anything for now
|
||||
...
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: {
|
||||
// anything for now
|
||||
...
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -14,20 +14,19 @@
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "canvas"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
// anything for now
|
||||
...
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
// anything for now
|
||||
...
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -14,28 +14,29 @@
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
composableKinds: PanelCfg: {
|
||||
maturity: "experimental"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "dashlist"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelLayout: "list" | "previews" @cuetsy(kind="enum")
|
||||
PanelOptions: {
|
||||
layout?: PanelLayout | *"list"
|
||||
showStarred: bool | *true
|
||||
showRecentlyViewed: bool | *false
|
||||
showSearch: bool | *false
|
||||
showHeadings: bool | *true
|
||||
maxItems: int | *10
|
||||
query: string | *""
|
||||
folderId?: int
|
||||
tags: [...string] | *[]
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelLayout: "list" | "previews" @cuetsy(kind="enum")
|
||||
PanelOptions: {
|
||||
layout?: PanelLayout | *"list"
|
||||
showStarred: bool | *true
|
||||
showRecentlyViewed: bool | *false
|
||||
showSearch: bool | *false
|
||||
showHeadings: bool | *true
|
||||
maxItems: int | *10
|
||||
query: string | *""
|
||||
folderId?: int
|
||||
tags: [...string] | *[]
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
//
|
||||
// Run 'make gen-cue' from repository root to regenerate.
|
||||
|
||||
export const PanelModelVersion = Object.freeze([0, 0]);
|
||||
export const PanelCfgModelVersion = Object.freeze([0, 0]);
|
||||
|
||||
export enum PanelLayout {
|
||||
List = 'list',
|
||||
|
@ -15,23 +15,25 @@
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "gauge"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
ui.SingleStatBaseOptions
|
||||
showThresholdLabels: bool | *false
|
||||
showThresholdMarkers: bool | *true
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
composableKinds: PanelCfg: {
|
||||
maturity: "experimental"
|
||||
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
ui.SingleStatBaseOptions
|
||||
showThresholdLabels: bool | *false
|
||||
showThresholdMarkers: bool | *true
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
|
||||
import * as ui from '@grafana/schema';
|
||||
|
||||
export const PanelModelVersion = Object.freeze([0, 0]);
|
||||
export const PanelCfgModelVersion = Object.freeze([0, 0]);
|
||||
|
||||
export interface PanelOptions extends ui.SingleStatBaseOptions {
|
||||
showThresholdLabels: boolean;
|
||||
|
@ -14,22 +14,23 @@
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "candlestick"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
// anything for now
|
||||
...
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: {
|
||||
// anything for now
|
||||
...
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
// anything for now
|
||||
...
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: {
|
||||
// anything for now
|
||||
...
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
58
public/app/plugins/panel/histogram/composable_panelcfg.cue
Normal file
58
public/app/plugins/panel/histogram/composable_panelcfg.cue
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright 2022 Grafana Labs
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
composableKinds: PanelCfg: {
|
||||
maturity: "experimental"
|
||||
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
ui.OptionsWithLegend
|
||||
ui.OptionsWithTooltip
|
||||
|
||||
//Size of each bucket
|
||||
bucketSize?: int32
|
||||
//Offset buckets by this amount
|
||||
bucketOffset?: int32 | *0
|
||||
//Combines multiple series into a single histogram
|
||||
combine?: bool
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
PanelFieldConfig: {
|
||||
ui.AxisConfig
|
||||
ui.HideableFieldConfig
|
||||
|
||||
// Controls line width of the bars.
|
||||
lineWidth?: uint32 & <=10 | *1
|
||||
// Controls the fill opacity of the bars.
|
||||
fillOpacity?: uint32 & <=100 | *80
|
||||
// Set the mode of the gradient fill. Fill gradient is based on the line color. To change the color, use the standard color scheme field option.
|
||||
// Gradient appearance is influenced by the Fill opacity setting.
|
||||
gradientMode?: ui.GraphGradientMode | *"none"
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
// Copyright 2022 Grafana Labs
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "histogram"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
ui.OptionsWithLegend
|
||||
ui.OptionsWithTooltip
|
||||
//Size of each bucket
|
||||
bucketSize?: int32
|
||||
//Offset buckets by this amount
|
||||
bucketOffset?: int32 | *0
|
||||
//Combines multiple series into a single histogram
|
||||
combine?: bool
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
PanelFieldConfig: {
|
||||
ui.AxisConfig
|
||||
ui.HideableFieldConfig
|
||||
// Controls line width of the bars.
|
||||
lineWidth?: uint32 & <= 10 | *1
|
||||
// Controls the fill opacity of the bars.
|
||||
fillOpacity?: uint32 & <= 100 | *80
|
||||
// Set the mode of the gradient fill. Fill gradient is based on the line color. To change the color, use the standard color scheme field option.
|
||||
// Gradient appearance is influenced by the Fill opacity setting.
|
||||
gradientMode?: ui.GraphGradientMode | *"none"
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
|
||||
import * as ui from '@grafana/schema';
|
||||
|
||||
export const PanelModelVersion = Object.freeze([0, 0]);
|
||||
export const PanelCfgModelVersion = Object.freeze([0, 0]);
|
||||
|
||||
export interface PanelOptions extends ui.OptionsWithLegend, ui.OptionsWithTooltip {
|
||||
/**
|
||||
|
@ -14,24 +14,22 @@
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
composableKinds: PanelCfg: {
|
||||
maturity: "experimental"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "heatmap"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
// anything for now
|
||||
...
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: {
|
||||
// anything for now
|
||||
...
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
// empty/missing will default to grafana blog
|
||||
feedUrl?: string
|
||||
showImage?: bool | *true
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
//
|
||||
// Run 'make gen-cue' from repository root to regenerate.
|
||||
|
||||
export const PanelModelVersion = Object.freeze([0, 0]);
|
||||
export const PanelCfgModelVersion = Object.freeze([0, 0]);
|
||||
|
||||
export interface PanelOptions {
|
||||
/**
|
||||
|
58
public/app/plugins/panel/piechart/composable_panelcfg.cue
Normal file
58
public/app/plugins/panel/piechart/composable_panelcfg.cue
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright 2022 Grafana Labs
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
composableKinds: PanelCfg: {
|
||||
maturity: "experimental"
|
||||
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
// v0.0
|
||||
{
|
||||
// Select the pie chart display style.
|
||||
PieChartType: "pie" | "donut" @cuetsy(kind="enum")
|
||||
// Select labels to display on the pie chart.
|
||||
// - Name - The series or field name.
|
||||
// - Percent - The percentage of the whole.
|
||||
// - Value - The raw numerical value.
|
||||
PieChartLabels: "name" | "value" | "percent" @cuetsy(kind="enum")
|
||||
// Select values to display in the legend.
|
||||
// - Percent: The percentage of the whole.
|
||||
// - Value: The raw numerical value.
|
||||
PieChartLegendValues: "value" | "percent" @cuetsy(kind="enum")
|
||||
PieChartLegendOptions: {
|
||||
ui.VizLegendOptions
|
||||
values: [...PieChartLegendValues]
|
||||
} @cuetsy(kind="interface")
|
||||
PanelOptions: {
|
||||
ui.OptionsWithTooltip
|
||||
ui.SingleStatBaseOptions
|
||||
pieType: PieChartType
|
||||
displayLabels: [...PieChartLabels]
|
||||
legend: PieChartLegendOptions
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: ui.HideableFieldConfig @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
// Copyright 2022 Grafana Labs
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "piechart"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
// v0.0
|
||||
{
|
||||
// Select the pie chart display style.
|
||||
PieChartType: "pie" | "donut" @cuetsy(kind="enum")
|
||||
// Select labels to display on the pie chart.
|
||||
// - Name - The series or field name.
|
||||
// - Percent - The percentage of the whole.
|
||||
// - Value - The raw numerical value.
|
||||
PieChartLabels: "name" | "value" | "percent" @cuetsy(kind="enum")
|
||||
// Select values to display in the legend.
|
||||
// - Percent: The percentage of the whole.
|
||||
// - Value: The raw numerical value.
|
||||
PieChartLegendValues: "value" | "percent" @cuetsy(kind="enum")
|
||||
PieChartLegendOptions: {
|
||||
ui.VizLegendOptions
|
||||
values: [...PieChartLegendValues]
|
||||
} @cuetsy(kind="interface")
|
||||
PanelOptions: {
|
||||
ui.OptionsWithTooltip
|
||||
ui.SingleStatBaseOptions
|
||||
pieType: PieChartType
|
||||
displayLabels: [...PieChartLabels]
|
||||
legend: PieChartLegendOptions
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: ui.HideableFieldConfig @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
|
||||
import * as ui from '@grafana/schema';
|
||||
|
||||
export const PanelModelVersion = Object.freeze([0, 0]);
|
||||
export const PanelCfgModelVersion = Object.freeze([0, 0]);
|
||||
|
||||
/**
|
||||
* Select the pie chart display style.
|
||||
|
@ -15,25 +15,27 @@
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "stat"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
ui.SingleStatBaseOptions
|
||||
graphMode: ui.BigValueGraphMode | *"area"
|
||||
colorMode: ui.BigValueColorMode | *"value"
|
||||
justifyMode: ui.BigValueJustifyMode | *"auto"
|
||||
textMode: ui.BigValueTextMode | *"auto"
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
composableKinds: PanelCfg: {
|
||||
maturity: "experimental"
|
||||
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
ui.SingleStatBaseOptions
|
||||
graphMode: ui.BigValueGraphMode | *"area"
|
||||
colorMode: ui.BigValueColorMode | *"value"
|
||||
justifyMode: ui.BigValueJustifyMode | *"auto"
|
||||
textMode: ui.BigValueTextMode | *"auto"
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
|
||||
import * as ui from '@grafana/schema';
|
||||
|
||||
export const PanelModelVersion = Object.freeze([0, 0]);
|
||||
export const PanelCfgModelVersion = Object.freeze([0, 0]);
|
||||
|
||||
export interface PanelOptions extends ui.SingleStatBaseOptions {
|
||||
colorMode: ui.BigValueColorMode;
|
||||
|
@ -0,0 +1,51 @@
|
||||
// Copyright 2021 Grafana Labs
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
TimelineMode: "changes" | "samples" @cuetsy(kind="enum")
|
||||
TimelineValueAlignment: "center" | "left" | "right" @cuetsy(kind="type")
|
||||
PanelOptions: {
|
||||
// FIXME ts comments indicate this shouldn't be in the saved model, but currently is emitted
|
||||
mode?: TimelineMode
|
||||
ui.OptionsWithLegend
|
||||
ui.OptionsWithTooltip
|
||||
ui.OptionsWithTimezones
|
||||
showValue: ui.VisibilityMode | *"auto"
|
||||
rowHeight: number | *0.9
|
||||
colWidth?: number
|
||||
mergeValues?: bool | *true
|
||||
alignValue?: TimelineValueAlignment | *"left"
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: {
|
||||
ui.HideableFieldConfig
|
||||
lineWidth?: number | *0
|
||||
fillOpacity?: number | *70
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
// Copyright 2021 Grafana Labs
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "state-timeline"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
TimelineMode: "changes" | "samples" @cuetsy(kind="enum")
|
||||
TimelineValueAlignment: "center" | "left" | "right" @cuetsy(kind="type")
|
||||
PanelOptions: {
|
||||
// FIXME ts comments indicate this shouldn't be in the saved model, but currently is emitted
|
||||
mode?: TimelineMode
|
||||
ui.OptionsWithLegend
|
||||
ui.OptionsWithTooltip
|
||||
ui.OptionsWithTimezones
|
||||
showValue: ui.VisibilityMode | *"auto"
|
||||
rowHeight: number | *0.9
|
||||
colWidth?: number
|
||||
mergeValues?: bool | *true
|
||||
alignValue?: TimelineValueAlignment | *"left"
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: {
|
||||
ui.HideableFieldConfig
|
||||
lineWidth?: number | *0
|
||||
fillOpacity?: number | *70
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
// Copyright 2021 Grafana Labs
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
ui.OptionsWithLegend
|
||||
ui.OptionsWithTooltip
|
||||
ui.OptionsWithTimezones
|
||||
showValue: ui.VisibilityMode
|
||||
rowHeight: number
|
||||
colWidth?: number
|
||||
alignValue: "center" | *"left" | "right"
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: {
|
||||
ui.HideableFieldConfig
|
||||
lineWidth?: number | *1
|
||||
fillOpacity?: number | *70
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
// Copyright 2021 Grafana Labs
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "status-history"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
ui.OptionsWithLegend
|
||||
ui.OptionsWithTooltip
|
||||
ui.OptionsWithTimezones
|
||||
showValue: ui.VisibilityMode
|
||||
rowHeight: number
|
||||
colWidth?: number
|
||||
alignValue: "center" | *"left" | "right"
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: {
|
||||
ui.HideableFieldConfig
|
||||
lineWidth?: number | *1
|
||||
fillOpacity?: number | *70
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
@ -15,25 +15,25 @@
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "table"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
frameIndex: number | *0
|
||||
showHeader: bool | *true
|
||||
showTypeIcons: bool | *false
|
||||
sortBy?: [...ui.TableSortByFieldState]
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: ui.TableFieldOptions & {} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
frameIndex: number | *0
|
||||
showHeader: bool | *true
|
||||
showTypeIcons: bool | *false
|
||||
sortBy?: [...ui.TableSortByFieldState]
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: ui.TableFieldOptions & {} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -14,36 +14,37 @@
|
||||
|
||||
package grafanaplugin
|
||||
|
||||
import "github.com/grafana/thema"
|
||||
composableKinds: PanelCfg: {
|
||||
maturity: "experimental"
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "text"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
TextMode: "html" | "markdown" | "code" @cuetsy(kind="enum",memberNames="HTML|Markdown|Code")
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
TextMode: "html" | "markdown" | "code" @cuetsy(kind="enum",memberNames="HTML|Markdown|Code")
|
||||
|
||||
CodeLanguage: "json" | "yaml" | "xml" | "typescript" | "sql" | "go" | "markdown" | "html" | *"plaintext" @cuetsy(kind="enum")
|
||||
CodeLanguage: "json" | "yaml" | "xml" | "typescript" | "sql" | "go" | "markdown" | "html" | *"plaintext" @cuetsy(kind="enum")
|
||||
|
||||
CodeOptions: {
|
||||
// The language passed to monaco code editor
|
||||
language: CodeLanguage
|
||||
showLineNumbers: bool | *false
|
||||
showMiniMap: bool | *false
|
||||
} @cuetsy(kind="interface")
|
||||
CodeOptions: {
|
||||
// The language passed to monaco code editor
|
||||
language: CodeLanguage
|
||||
showLineNumbers: bool | *false
|
||||
showMiniMap: bool | *false
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
PanelOptions: {
|
||||
mode: TextMode | *"markdown"
|
||||
code?: CodeOptions
|
||||
content: string | *"""
|
||||
PanelOptions: {
|
||||
mode: TextMode | *"markdown"
|
||||
code?: CodeOptions
|
||||
content: string | *"""
|
||||
# Title
|
||||
|
||||
For markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)
|
||||
"""
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
//
|
||||
// Run 'make gen-cue' from repository root to regenerate.
|
||||
|
||||
export const PanelModelVersion = Object.freeze([0, 0]);
|
||||
export const PanelCfgModelVersion = Object.freeze([0, 0]);
|
||||
|
||||
export enum TextMode {
|
||||
Code = 'code',
|
||||
|
@ -15,23 +15,23 @@
|
||||
package grafanaplugin
|
||||
|
||||
import (
|
||||
"github.com/grafana/thema"
|
||||
ui "github.com/grafana/grafana/packages/grafana-schema/src/common"
|
||||
)
|
||||
|
||||
Panel: thema.#Lineage & {
|
||||
name: "timeseries"
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
legend: ui.VizLegendOptions
|
||||
tooltip: ui.VizTooltipOptions
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: ui.GraphFieldConfig & {} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
composableKinds: PanelCfg: {
|
||||
lineage: {
|
||||
seqs: [
|
||||
{
|
||||
schemas: [
|
||||
{
|
||||
PanelOptions: {
|
||||
legend: ui.VizLegendOptions
|
||||
tooltip: ui.VizTooltipOptions
|
||||
} @cuetsy(kind="interface")
|
||||
PanelFieldConfig: ui.GraphFieldConfig & {} @cuetsy(kind="interface")
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user