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:
sam boyer 2023-01-20 04:41:35 -05:00 committed by GitHub
parent 6a7cbeae6c
commit 3b3059c9ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
91 changed files with 2192 additions and 1870 deletions

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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;

View File

@ -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
*/

View 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")

View File

@ -246,3 +246,5 @@ VizTooltipOptions: {
mode: TooltipDisplayMode
sort: SortOrder
} @cuetsy(kind="interface")

View File

@ -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

View File

@ -3,5 +3,5 @@
*
* @packageDocumentation
*/
export * from './common/common.gen';
export * from './veneer/common.types';
export * from './index.gen';

View 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';

View File

@ -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;

View File

@ -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()),
})

View File

@ -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 }}
}
}

View File

@ -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
View 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
}

View 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")

View File

@ -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()
}

View File

@ -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
}

View File

@ -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

View File

@ -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() {}

View File

@ -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.

View File

@ -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

View File

@ -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: {}

View File

@ -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")

View File

@ -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 }}
}
}

View File

@ -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",
}

View 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")
},
]
},
]
}
}

View File

@ -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")
},
]
},
]
}

View File

@ -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"
}
}
}

View File

@ -1,9 +1,6 @@
package grafanaplugin
import "github.com/grafana/thema"
Query: thema.#Lineage & {
name: "missing_kind_datasource"
composableKinds: DataQuery: lineage: {
seqs: [
{
schemas: [

View File

@ -1,8 +1,6 @@
package grafanaplugin
import "github.com/grafana/thema"
Panel: thema.#Lineage & {
composableKinds: PanelCfg: lineage: {
name: "doesnamatch"
seqs: [
{

View 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
},
]
},
]
}
}

View File

@ -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
},
]
},
]
}

View 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
},
]
},
]
}
}

View File

@ -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
},
]
},
]
}

View File

@ -0,0 +1,13 @@
package grafanaplugin
composableKinds: DataQuery: lineage: {
seqs: [
{
schemas: [
{
foo: string
},
]
},
]
}

View File

@ -0,0 +1,18 @@
package grafanaplugin
composableKinds: DataSourceCfg: lineage: {
seqs: [
{
schemas: [
{
Options: {
foo: string
}
SecureOptions: {
bar: string
}
},
]
},
]
}

View File

@ -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
}
},
]
},
]
}

View File

@ -1,9 +1,6 @@
package grafanaplugin
import "github.com/grafana/thema"
Panel: thema.#Lineage & {
name: "mismatch"
composableKinds: PanelCfg: lineage: {
seqs: [
{
schemas: [

View File

@ -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")
},
]
},
]
}

View File

@ -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")
},
]
},
]
}

View File

@ -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"
}
}
}

View File

@ -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
}

View File

@ -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),
}
}

View File

@ -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,
})
}

View File

@ -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

View File

@ -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")

View 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
}
...
}

View File

@ -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
}

View File

@ -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
View 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]
// }

View 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)
}
}

View 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))
}
}

View File

@ -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"

View File

@ -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])
}

View File

@ -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"`

View File

@ -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{})),
)

View File

@ -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")
},
]
},
]
}
}

View File

@ -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;

View 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")
},
]
},
]
}
}

View File

@ -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")
},
]
},
]
}

View File

@ -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;
/**

View File

@ -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")
},
]
},
]
}
}

View File

@ -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;

View File

@ -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")
},
]
},
]
}
}

View File

@ -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")
},
]
},
]
}
}

View File

@ -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")
},
]
},
]
}
}

View File

@ -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',

View File

@ -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")
},
]
},
]
}
}

View File

@ -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;

View File

@ -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")
},
]
},
]
}
}

View 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")
},
]
},
]
}
}

View File

@ -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")
},
]
},
]
}

View File

@ -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 {
/**

View File

@ -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")
},
]
},
]
}
}

View File

@ -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 {
/**

View 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")
},
]
},
]
}
}

View File

@ -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")
},
]
},
]
}

View File

@ -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.

View File

@ -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")
},
]
},
]
}
}

View File

@ -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;

View File

@ -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")
},
]
},
]
}
}

View File

@ -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")
},
]
},
]
}

View File

@ -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")
},
]
},
]
}
}

View File

@ -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")
},
]
},
]
}

View File

@ -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")
},
]
},
]
}
}

View File

@ -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")
},
]
},
]
}
}

View File

@ -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',

View File

@ -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")
},
]
},
]
}
}