schema: Migrate from scuemata to thema (#49805)

* Remove crufty scuemata bits

Buhbye to: cue/ dir with old definitions, CI steps for checking unnecessary
things, and the original dashboard scuemata file.

* Remove grafana-cli cue subcommand

* Remove old testdata

* Don't swallow errors from codegen

* Small nits and tweaks to cuectx package

* WIP - refactor pluggen to use Thema

Also consolidate the embed.FS in the repo root.

* Finish halfway rename

* Convert all panel models.cue to thema

* Rewrite pluggen to use Thema

* Remove pkg/schema, and trim command

* Remove schemaloader service and usages

Will be replaced by coremodel-centric hydrate/dehydrate system Soon™.

* Remove schemaloader from wire

* Remove hangover field on histogram models.cue

* Fix lint errors, some vestiges of trim service

* Remove unused cuetsify cli command
This commit is contained in:
sam boyer 2022-06-06 20:52:44 -04:00 committed by GitHub
parent e7d6a58037
commit 8876d56495
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 538 additions and 32087 deletions

View File

@ -245,25 +245,6 @@ steps:
environment: null
image: grafana/build-container:1.5.5
name: build-plugins
- commands:
- ./bin/linux-amd64/grafana-cli cue validate-schema --grafana-root .
depends_on:
- build-backend
image: grafana/build-container:1.5.5
name: validate-scuemata
- commands:
- '# It is required that the generated Typescript be in sync with the input CUE
files.'
- '# To enforce this, the following command will attempt to generate Typescript
from all'
- '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file
the generated'
- '# code would have been written to. It exits 1 if any diffs are found.'
- ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff
depends_on:
- validate-scuemata
image: grafana/build-container:1.5.5
name: ensure-cuetsified
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
with its inputs.'
@ -940,25 +921,6 @@ steps:
from_secret: grafana_api_key
image: grafana/build-container:1.5.5
name: build-plugins
- commands:
- ./bin/linux-amd64/grafana-cli cue validate-schema --grafana-root .
depends_on:
- build-backend
image: grafana/build-container:1.5.5
name: validate-scuemata
- commands:
- '# It is required that the generated Typescript be in sync with the input CUE
files.'
- '# To enforce this, the following command will attempt to generate Typescript
from all'
- '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file
the generated'
- '# code would have been written to. It exits 1 if any diffs are found.'
- ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff
depends_on:
- validate-scuemata
image: grafana/build-container:1.5.5
name: ensure-cuetsified
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
with its inputs.'
@ -1588,25 +1550,6 @@ steps:
from_secret: grafana_api_key
image: grafana/build-container:1.5.5
name: build-plugins
- commands:
- ./bin/linux-amd64/grafana-cli cue validate-schema --grafana-root .
depends_on:
- build-backend
image: grafana/build-container:1.5.5
name: validate-scuemata
- commands:
- '# It is required that the generated Typescript be in sync with the input CUE
files.'
- '# To enforce this, the following command will attempt to generate Typescript
from all'
- '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file
the generated'
- '# code would have been written to. It exits 1 if any diffs are found.'
- ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff
depends_on:
- validate-scuemata
image: grafana/build-container:1.5.5
name: ensure-cuetsified
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
with its inputs.'
@ -2193,25 +2136,6 @@ steps:
from_secret: grafana_api_key
image: grafana/build-container:1.5.5
name: build-plugins
- commands:
- ./bin/linux-amd64/grafana-cli cue validate-schema --grafana-root .
depends_on:
- build-backend
image: grafana/build-container:1.5.5
name: validate-scuemata
- commands:
- '# It is required that the generated Typescript be in sync with the input CUE
files.'
- '# To enforce this, the following command will attempt to generate Typescript
from all'
- '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file
the generated'
- '# code would have been written to. It exits 1 if any diffs are found.'
- ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff
depends_on:
- validate-scuemata
image: grafana/build-container:1.5.5
name: ensure-cuetsified
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
with its inputs.'
@ -3414,25 +3338,6 @@ steps:
from_secret: grafana_api_key
image: grafana/build-container:1.5.5
name: build-plugins
- commands:
- ./bin/linux-amd64/grafana-cli cue validate-schema --grafana-root .
depends_on:
- build-backend
image: grafana/build-container:1.5.5
name: validate-scuemata
- commands:
- '# It is required that the generated Typescript be in sync with the input CUE
files.'
- '# To enforce this, the following command will attempt to generate Typescript
from all'
- '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file
the generated'
- '# code would have been written to. It exits 1 if any diffs are found.'
- ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff
depends_on:
- validate-scuemata
image: grafana/build-container:1.5.5
name: ensure-cuetsified
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
with its inputs.'
@ -3964,25 +3869,6 @@ steps:
from_secret: grafana_api_key
image: grafana/build-container:1.5.5
name: build-plugins
- commands:
- ./bin/linux-amd64/grafana-cli cue validate-schema --grafana-root .
depends_on:
- build-backend
image: grafana/build-container:1.5.5
name: validate-scuemata
- commands:
- '# It is required that the generated Typescript be in sync with the input CUE
files.'
- '# To enforce this, the following command will attempt to generate Typescript
from all'
- '# appropriate .cue files, then compare with the corresponding (*.gen.ts) file
the generated'
- '# code would have been written to. It exits 1 if any diffs are found.'
- ./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff
depends_on:
- validate-scuemata
image: grafana/build-container:1.5.5
name: ensure-cuetsified
- commands:
- '# It is required that code generated from Thema/CUE be committed and in sync
with its inputs.'
@ -4725,6 +4611,6 @@ kind: secret
name: gcp_upload_artifacts_key
---
kind: signature
hmac: ee50d41dc69f9e424ac31a7a3e282c913fbe182903d10aebe2df2acf8da6c9d8
hmac: d2e97d6683c33ccb2dba773e3c103105227a74448fa732124e4c38d67eb464d8
...

View File

@ -1,27 +0,0 @@
package scuemata
// Definition of the shape of a panel plugin's schema declarations in its
// schema.cue file.
//
// Note that these keys do not appear directly in any real JSON artifact;
// rather, they are composed into panel structures as they are defined within
// the larger Dashboard schema.
#PanelSchema: {
// Defines plugin specific options for a panel
PanelOptions: {...} @cuetsy(kind="interface")
// Define the custom properties that exist within standard field config
PanelFieldConfig?: {...} @cuetsy(kind="interface")
// Panels may define their own types
...
}
// A lineage of panel schema
#PanelLineage: [#PanelSchema, ...#PanelSchema]
// Panel plugin-specific Family
#PanelFamily: {
lineages: [#PanelLineage, ...#PanelLineage]
migrations: [...#Migration]
}

View File

@ -1,61 +0,0 @@
package scuemata
// A family is a collection of schemas that specify a single kind of object,
// allowing evolution of the canonical schema for that kind of object over time.
//
// The schemas are organized into a list of Lineages, which are themselves ordered
// lists of schemas where each schema with its predecessor in the lineage.
//
// If it is desired to define a schema with a breaking schema relative to its
// predecessors, a new Lineage must be created, as well as a Migration that defines
// a mapping to the new schema from the latest schema in prior Lineage.
//
// The version number of a schema is not controlled by the schema itself, but by
// its position in the list of lineages - e.g., 0.0 corresponds to the first
// schema in the first lineage.
#Family: {
compose?: {...}
lineages: [#Lineage, ...#Lineage]
migrations: [...#Migration]
let lseq = lineages[len(lineages)-1]
latest: #LastSchema & {_p: lseq}
}
// A Lineage is a non-empty list containing an ordered series of schemas that
// all describe a single kind of object, where each schema is backwards
// compatible with its predecessor.
#Lineage: [{...}, ...{...}]
#LastSchema: {
_p: #Lineage
_p[len(_p)-1]
}
// A Migration defines a relation between two schemas, "_from" and "_to". The
// relation expresses any complex mappings that must be performed to
// transform an input artifact valid with respect to the _from schema, into
// an artifact valid with respect to the _to schema. This is accomplished
// in two stages:
// 1. A Migration is initially defined by passing in schemas for _from and _to,
// and mappings that translate _from to _to are defined in _rel.
// 2. A concrete object may then be unified with _to, resulting in its values
// being mapped onto "result" by way of _rel.
//
// This is the absolute simplest possible definition of a Migration. It's
// incumbent on the implementor to manually ensure the correctness and
// completeness of the mapping. The primary value in defining such a generic
// structure is to allow comparably generic logic for migrating concrete
// artifacts through schema changes.
//
// If _to isn't backwards compatible (accretion-only) with _from, then _rel must
// explicitly enumerate every field in _from and map it to a field in _to, even
// if they're identical. This is laborious for anything outside trivially tiny
// schema. We'll want to eventually add helpers for whitelisting or blacklisting
// of paths in _from, so that migrations of larger schema can focus narrowly on
// the points of actual change.
#Migration: {
from: {...}
to: {...}
rel: {...}
result: to & rel
}

View File

@ -4,13 +4,7 @@ import (
"embed"
)
// CoreSchema embeds all core CUE files, which live in packages/grafana-schema/src
// CueSchemaFS embeds all schema-related CUE files in the Grafana project.
//
//go:embed cue.mod cue packages/grafana-schema/src/schema/*.cue packages/grafana-schema/src/scuemata/*/*.cue packages/grafana-schema/src/scuemata/*/*/*.cue
var CoreSchema embed.FS
// PluginSchema embeds all expected plugin CUE files and plugin metadata from
// within the public/app/plugins subdirectory.
//
//go:embed public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json
var PluginSchema embed.FS
//go:embed cue.mod packages/grafana-schema/src/schema/*.cue public/app/plugins/*/*/*.cue public/app/plugins/*/*/plugin.json
var CueSchemaFS embed.FS

View File

@ -1,441 +0,0 @@
package dashboard
import (
"list"
"github.com/grafana/grafana/cue/scuemata"
)
Family: scuemata.#Family & {
lineages: [
[
{// 0.0
// Unique numeric identifier for the dashboard.
// TODO must isolate or remove identifiers local to a Grafana instance...?
id?: number
// Unique dashboard identifier that can be generated by anyone. string (8-40)
uid?: string
// Title of dashboard.
title?: string
// Description of dashboard.
description?: string
gnetId?: string
// Tags associated with dashboard.
tags?: [...string]
// Theme of dashboard.
style: *"light" | "dark"
// Timezone of dashboard,
timezone?: *"browser" | "utc" | ""
// Whether a dashboard is editable or not.
editable: bool | *true
// 0 for no shared crosshair or tooltip (default).
// 1 for shared crosshair.
// 2 for shared crosshair AND shared tooltip.
graphTooltip: >=0 & <=2 | *0
// Time range for dashboard, e.g. last 6 hours, last 7 days, etc
time?: {
from: string | *"now-6h"
to: string | *"now"
}
// Timepicker metadata.
timepicker?: {
// Whether timepicker is collapsed or not.
collapse: bool | *false
// Whether timepicker is enabled or not.
enable: bool | *true
// Whether timepicker is visible or not.
hidden: bool | *false
// Selectable intervals for auto-refresh.
refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
}
// Templating.
templating?: list: [...{...}]
// Annotations.
annotations?: list: [...{
builtIn: number | *0
// Datasource to use for annotation.
datasource: {
type?: string
uid?: string
}
// Whether annotation is enabled.
enable: bool | *true
// Whether to hide annotation.
hide?: bool | *false
// Annotation icon color.
iconColor?: string
// Name of annotation.
name?: string
type: string | *"dashboard"
// Query for annotation data.
rawQuery?: string
showIn: number | *0
}]
// Auto-refresh interval.
refresh?: string | false
// Version of the JSON schema, incremented each time a Grafana update brings
// changes to said schema.
// FIXME this is the old schema numbering system, and will be replaced by scuemata
schemaVersion: number | *30
// Version of the dashboard, incremented each time the dashboard is updated.
version?: number
panels?: [...(#Panel | #GraphPanel | #RowPanel)]
// TODO docs
#FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(kind="enum")
// TODO docs
#FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type")
// TODO docs
#FieldColor: {
// The main color scheme mode
mode: #FieldColorModeId | string
// Stores the fixed color value if mode is fixed
fixedColor?: string
// Some visualizations need to know how to assign a series color from by value color schemes
seriesBy?: #FieldColorSeriesByMode
} @cuetsy(kind="interface")
// TODO docs
#Threshold: {
// TODO docs
// FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON
value?: number
// TODO docs
color: string
// TODO docs
// TODO are the values here enumerable into a disjunction?
// Some seem to be listed in typescript comment
state?: string
} @cuetsy(kind="interface")
#ThresholdsMode: "absolute" | "percentage" @cuetsy(kind="enum")
#ThresholdsConfig: {
mode: #ThresholdsMode
// Must be sorted by 'value', first value is always -Infinity
steps: [...#Threshold]
} @cuetsy(kind="interface")
// TODO docs
// FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it
#Transformation: {
id: string
options: {...}
}
// Schema for panel targets is specified by datasource
// plugins. We use a placeholder definition, which the Go
// schema loader either left open/as-is with the Base
// variant of the Dashboard and Panel families, or filled
// with types derived from plugins in the Instance variant.
// When working directly from CUE, importers can extend this
// type directly to achieve the same effect.
#Target: {...}
// Dashboard panels. Panels are canonically defined inline
// because they share a version timeline with the dashboard
// schema; they do not evolve independently.
#Panel: {
// The panel plugin type id.
type: !=""
// TODO docs
id?: number
// FIXME this almost certainly has to be changed in favor of scuemata versions
pluginVersion?: string
// TODO docs
tags?: [...string]
// Internal - the exact major and minor versions of the panel plugin
// schema. Hidden and therefore not a part of the data model, but
// expected to be filled with panel plugin schema versions so that it's
// possible to figure out which schema version matched on a successful
// unification.
// _pv: { maj: int, min: int }
// The major and minor versions of the panel plugin for this schema.
// TODO 2-tuple list instead of struct?
// panelSchema?: { maj: number, min: number }
panelSchema?: [number, number]
// TODO docs
targets?: [...#Target]
// Panel title.
title?: string
// Description.
description?: string
// Whether to display the panel without a background.
transparent: bool | *false
// The datasource used in all targets.
datasource?: {
type?: string
uid?: string
}
// Grid position.
gridPos?: {
// Panel
h: number & >0 | *9
// Panel
w: number & >0 & <=24 | *12
// Panel x
x: number & >=0 & <24 | *0
// Panel y
y: number & >=0 | *0
// true if fixed
static?: bool
}
// Panel links.
// FIXME this is temporarily specified as a closed list so
// that validation will pass when no links are present, but
// to force a failure as soon as it's checked against there
// being anything in the list so it can be fixed in
// accordance with that object
links?: []
// Name of template variable to repeat for.
repeat?: string
// Direction to repeat in if 'repeat' is set.
// "h" for horizontal, "v" for vertical.
repeatDirection: *"h" | "v"
// TODO docs
maxDataPoints?: number
// TODO docs
thresholds?: [...]
// TODO docs
timeRegions?: [...]
transformations: [...#Transformation]
// TODO docs
// TODO tighter constraint
interval?: string
// TODO docs
// TODO tighter constraint
timeFrom?: string
// TODO docs
// TODO tighter constraint
timeShift?: string
// options is specified by the PanelOptions field in panel
// plugin schemas.
options: {}
fieldConfig: {
defaults: {
// The display value for this field. This supports template variables blank is auto
displayName?: string
// This can be used by data sources that return and explicit naming structure for values and labels
// When this property is configured, this value is used rather than the default naming strategy.
displayNameFromDS?: string
// Human readable field metadata
description?: string
// An explict path to the field in the datasource. When the frame meta includes a path,
// This will default to `${frame.meta.path}/${field.name}
//
// When defined, this value can be used as an identifier within the datasource scope, and
// may be used to update the results
path?: string
// True if data source can write a value to the path. Auth/authz are supported separately
writeable?: bool
// True if data source field supports ad-hoc filters
filterable?: bool
// Numeric Options
unit?: string
// Significant digits (for display)
decimals?: number
min?: number
max?: number
// Convert input values into a display string
//
// TODO this one corresponds to a complex type with
// generics on the typescript side. Ouch. Will
// either need special care, or we'll just need to
// accept a very loosely specified schema. It's very
// unlikely we'll be able to translate cue to
// typescript generics in the general case, though
// this particular one *may* be able to work.
mappings?: [...{...}]
// Map numeric values to states
thresholds?: #ThresholdsConfig
// // Map values to a display color
color?: #FieldColor
// // Used when reducing field values
// nullValueMode?: NullValueMode
// // The behavior when clicking on a result
links?: [...]
// Alternative to empty string
noValue?: string
// custom is specified by the PanelFieldConfig field
// in panel plugin schemas.
custom?: {}
}
overrides: [...{
matcher: {
id: string | *""
options?: _
}
properties: [...{
id: string | *""
value?: _
}]
}]
}
// Embed the disjunction of all injected panel schema, if any were injected.
if len(compose._panelSchemas) > 0 {
or(compose._panelSchemas)// TODO try to stick graph in here
}
// Make the plugin-composed subtrees open if the panel is
// of unknown types. This is important in every possible case:
// - Base (this file only): no real dashboard json
// containing any panels would ever validate
// - Dist (this file + core plugin schema): dashboard json containing
// panels with any third-party panel plugins would fail to validate,
// as well as any core plugins lacking a models.cue. The latter case
// is not normally expected, but this is not the appropriate place
// to enforce the invariant, anyway.
// - Instance (this file + core + third-party plugin schema): dashboard
// json containing panels with a third-party plugin that exists but
// is not currently installed would fail to validate.
if !list.Contains(compose._panelTypes, type) {
options: {...}
fieldConfig: defaults: custom: {...}
...
}
}
// Row panel
#RowPanel: {
type: "row"
collapsed: bool | *false
title?: string
// Name of default datasource.
datasource: {
type?: string
uid?: string
}
gridPos?: {
// Panel
h: number & >0 | *9
// Panel
w: number & >0 & <=24 | *12
// Panel x
x: number & >=0 & <24 | *0
// Panel y
y: number & >=0 | *0
// true if fixed
static?: bool
}
id: number
panels: [...(#Panel | #GraphPanel)]
// Name of template variable to repeat for.
repeat?: string
}
// Support for legacy graph panels.
#GraphPanel: {
...
type: "graph"
thresholds: [...{...}]
timeRegions?: [...{...}]
seriesOverrides: [...{...}]
aliasColors?: [string]: string
bars: bool | *false
dashes: bool | *false
dashLength: number | *10
fill?: number
fillGradient?: number
hiddenSeries: bool | *false
legend: {...}
lines: bool | *false
linewidth?: number
nullPointMode: *"null" | "connected" | "null as zero"
percentage: bool | *false
points: bool | *false
pointradius?: number
renderer: string
spaceLength: number | *10
stack: bool | *false
steppedLine: bool | *false
tooltip?: {
shared?: bool
sort: number | *0
value_type: *"individual" | "cumulative"
}
}
},
],
]
compose: {
// Scuemata families for all panel types that should be composed into the
// dashboard schema.
Panel: [string]: scuemata.#PanelFamily
// _panelTypes: [for typ, _ in Panel {typ}]
_panelTypes: [ for typ, _ in Panel {typ}, "graph", "row"]
_panelSchemas: [ for typ, scue in Panel {
for lv, lin in scue.lineages {
for sv, sch in lin {
(_mapPanel & {arg: {
type: typ
v: [lv, sv] // TODO add optionality for exact, at least, at most, any
model: sch // TODO Does this need to be close()d?
}}).out
}
}
}, {type: string}]
_mapPanel: {
arg: {
type: string & !=""
v: [number, number]
model: {...}
}
// Until CUE introduces the must() constraint, we have to enforce
// that the input model is as expected by checking for unification
// in this hidden property (see https://github.com/cue-lang/cue/issues/575).
// If we unified arg.model with the scuemata.#PanelSchema
// meta-schema directly, the struct openness (PanelOptions: {...})
// would be applied to the actual schema instance in the arg. Here,
// where we're actually putting those in the dashboard schema, want
// those to be closed, or at least preserve closed-ness.
_checkSchema: scuemata.#PanelSchema & arg.model
out: {
type: arg.type
panelSchema: arg.v // TODO add optionality for exact, at least, at most, any
options: arg.model.PanelOptions
fieldConfig: defaults: custom: {}
if arg.model.PanelFieldConfig != _|_ {
fieldConfig: defaults: custom: arg.model.PanelFieldConfig
}
}
}
}
}

View File

@ -1,48 +0,0 @@
package dist
import (
"github.com/grafana/grafana/packages/grafana-schema/src/scuemata/dashboard"
pannolist "github.com/grafana/grafana/public/app/plugins/panel/annolist:grafanaschema"
pbarchart "github.com/grafana/grafana/public/app/plugins/panel/barchart:grafanaschema"
pbargauge "github.com/grafana/grafana/public/app/plugins/panel/bargauge:grafanaschema"
pcanvas "github.com/grafana/grafana/public/app/plugins/panel/canvas:grafanaschema"
pdashlist "github.com/grafana/grafana/public/app/plugins/panel/dashlist:grafanaschema"
pgauge "github.com/grafana/grafana/public/app/plugins/panel/gauge:grafanaschema"
phistogram "github.com/grafana/grafana/public/app/plugins/panel/histogram:grafanaschema"
pcandlestick "github.com/grafana/grafana/public/app/plugins/panel/candlestick:grafanaschema"
pnews "github.com/grafana/grafana/public/app/plugins/panel/news:grafanaschema"
pstat "github.com/grafana/grafana/public/app/plugins/panel/stat:grafanaschema"
st "github.com/grafana/grafana/public/app/plugins/panel/state-timeline:grafanaschema"
sh "github.com/grafana/grafana/public/app/plugins/panel/status-history:grafanaschema"
ptable "github.com/grafana/grafana/public/app/plugins/panel/table:grafanaschema"
ptext "github.com/grafana/grafana/public/app/plugins/panel/text:grafanaschema"
ptimeseries "github.com/grafana/grafana/public/app/plugins/panel/timeseries:grafanaschema"
pheatmap_new "github.com/grafana/grafana/public/app/plugins/panel/heatmap-new:grafanaschema"
)
// Family composes the base dashboard scuemata family with all Grafana core plugins -
// the plugins that are dist[ributed] with Grafana. The resulting composed scuemata is
// exactly equivalent to what's produced by the DistDashboardFamily() Go function.
//
// CUE programs should default to importing this dist variant over the base variant.
Family: dashboard.Family & {
compose: Panel: {
// TODO do this with a loop once we include the panel type/plugin id in the model
annolist: pannolist.Panel
barchart: pbarchart.Panel
bargauge: pbargauge.Panel
canvas: pcanvas.Panel
dashlist: pdashlist.Panel
gauge: pgauge.Panel
histogram: phistogram.Panel
candlestick: pcandlestick.Panel
news: pnews.Panel
stat: pstat.Panel
"state-timeline": st.Panel
"status-history": sh.Panel
text: ptext.Panel
table: ptable.Panel
timeseries: ptimeseries.Panel
"heatmap-new": pheatmap_new.Panel
}
}

View File

@ -58,20 +58,12 @@ func (hs *HTTPServer) TrimDashboard(c *models.ReqContext) response.Response {
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
var err error
dash := cmd.Dashboard
meta := cmd.Meta
trimedResult := *dash
if !hs.LoadSchemaService.IsDisabled() {
trimedResult, err = hs.LoadSchemaService.DashboardTrimDefaults(*dash)
if err != nil {
return response.Error(500, "Error while exporting with default values removed", err)
}
}
// TODO temporarily just return the input as a no-op while we convert to thema calls
dto := dtos.TrimDashboardFullWithMeta{
Dashboard: &trimedResult,
Dashboard: dash,
Meta: meta,
}

View File

@ -64,7 +64,6 @@ import (
"github.com/grafana/grafana/pkg/services/queryhistory"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/schemaloader"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/searchusers"
"github.com/grafana/grafana/pkg/services/secrets"
@ -122,7 +121,6 @@ type HTTPServer struct {
ContextHandler *contexthandler.ContextHandler
SQLStore sqlstore.Store
AlertEngine *alerting.AlertEngine
LoadSchemaService *schemaloader.SchemaLoaderService
AlertNG *ngalert.AlertNG
LibraryPanelService librarypanels.Service
LibraryElementService libraryelements.Service
@ -180,8 +178,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
dataSourceProxy *datasourceproxy.DataSourceProxyService, searchService *search.SearchService,
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
schemaService *schemaloader.SchemaLoaderService, alertNG *ngalert.AlertNG,
libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
alertNG *ngalert.AlertNG, libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer, exportService export.ExportService,
encryptionService encryption.Internal, grafanaUpdateChecker *updatechecker.GrafanaService,
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
@ -237,7 +234,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
LivePushGateway: livePushGateway,
PluginContextProvider: plugCtxProvider,
ContextHandler: contextHandler,
LoadSchemaService: schemaService,
AlertNG: alertNG,
LibraryPanelService: libraryPanelService,
LibraryElementService: libraryElementService,

View File

@ -100,12 +100,6 @@ func runPluginCommand(command func(commandLine utils.CommandLine) error) func(co
}
}
func runCueCommand(command func(commandLine utils.CommandLine) error) func(context *cli.Context) error {
return func(context *cli.Context) error {
return command(&utils.ContextCommandLine{Context: context})
}
}
// Command contains command state.
type Command struct {
Client utils.ApiClient
@ -197,74 +191,6 @@ var adminCommands = []*cli.Command{
},
}
var cueCommands = []*cli.Command{
{
Name: "validate-schema",
Usage: "validate known *.cue files in the Grafana project",
Action: runCueCommand(cmd.validateScuemata),
Description: `validate-schema checks that all CUE schema files are valid with respect
to basic standards - valid CUE, valid scuemata, etc. Note that this
command checks only paths that existed when grafana-cli was compiled,
so must be recompiled to validate newly-added CUE files.`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "grafana-root",
Usage: "path to the root of a Grafana repository to validate",
},
},
},
{
Name: "validate-resource",
Usage: "validate resource files (e.g. dashboard JSON) against schema",
Action: runCueCommand(cmd.validateResources),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "dashboard",
Usage: "dashboard JSON file to validate",
},
&cli.BoolFlag{
Name: "base-only",
Usage: "validate using only base schema, not dist (includes plugin schema)",
Value: false,
},
},
},
{
Name: "trim-resource",
Usage: "trim schema-specified defaults from a resource",
Action: runCueCommand(cmd.trimResource),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "dashboard",
Usage: "path to file containing (valid) dashboard JSON",
},
&cli.BoolFlag{
Name: "apply",
Usage: "invert the operation: apply defaults instead of trimming them",
Value: false,
},
},
},
{
Name: "gen-ts",
Usage: "generate TypeScript from all known CUE file types",
Description: `gen-ts generates TypeScript from all CUE files at
expected positions in the filesystem tree of a Grafana repository.`,
Action: runCueCommand(cmd.generateTypescript),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "grafana-root",
Usage: "path to the root of a Grafana repository in which to generate TypeScript from CUE files",
},
&cli.BoolFlag{
Name: "diff",
Usage: "diff results of codegen against files already on disk. Exits 1 if diff is non-empty",
Value: false,
},
},
},
}
var Commands = []*cli.Command{
{
Name: "plugins",
@ -276,9 +202,4 @@ var Commands = []*cli.Command{
Usage: "Grafana admin commands",
Subcommands: adminCommands,
},
{
Name: "cue",
Usage: "Cue validation commands",
Subcommands: cueCommands,
},
}

View File

@ -1,30 +0,0 @@
package commands
import (
gerrors "errors"
"cuelang.org/go/cue/cuecontext"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/codegen"
)
var ctx = cuecontext.New()
// TODO remove this whole thing
func (cmd Command) generateTypescript(c utils.CommandLine) error {
root := c.String("grafana-root")
if root == "" {
return gerrors.New("must provide path to the root of a Grafana repository checkout")
}
wd, err := codegen.CuetsifyPlugins(ctx, root)
if err != nil {
return err
}
if c.Bool("diff") {
return wd.Verify()
}
return wd.Write()
}

View File

@ -1,126 +0,0 @@
package commands
import (
gerrors "errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"testing/fstest"
"cuelang.org/go/cue/errors"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/schema"
"github.com/grafana/grafana/pkg/schema/load"
)
var paths = load.GetDefaultLoadPaths()
func (cmd Command) validateScuemata(c utils.CommandLine) error {
root := c.String("grafana-root")
if root == "" {
return gerrors.New("must provide path to the root of a Grafana repository checkout")
}
// Construct MapFS with the same set of files as those embedded in
// /embed.go, but sourced straight through from disk instead of relying on
// what's compiled. Not the greatest, because we're duplicating
// filesystem-loading logic with what's in /embed.go.
var fspaths load.BaseLoadPaths
var err error
fspaths.BaseCueFS, err = populateMapFSFromRoot(paths.BaseCueFS, root, "")
if err != nil {
return err
}
fspaths.DistPluginCueFS, err = populateMapFSFromRoot(paths.DistPluginCueFS, root, "")
if err != nil {
return err
}
if err := validateScuemata(fspaths, load.DistDashboardFamily); err != nil {
return schema.WrapCUEError(err)
}
return nil
}
// Helper function that populates an fs.FS by walking over a virtual filesystem,
// and reading files from disk corresponding to each file encountered.
func populateMapFSFromRoot(in fs.FS, root, join string) (fs.FS, error) {
out := make(fstest.MapFS)
err := fs.WalkDir(in, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
// Ignore gosec warning G304. The input set here is necessarily
// constrained to files specified in embed.go
// nolint:gosec
b, err := os.Open(filepath.Join(root, join, path))
if err != nil {
return err
}
byt, err := io.ReadAll(b)
if err != nil {
return err
}
out[path] = &fstest.MapFile{Data: byt}
return nil
})
return out, err
}
func (cmd Command) validateResources(c utils.CommandLine) error {
filename := c.String("dashboard")
baseonly := c.Bool("base-only")
if filename == "" {
return gerrors.New("must specify dashboard to validate with --dashboard")
}
b, err := os.Open(filepath.Clean(filename))
res := schema.Resource{Value: b, Name: filename}
if err != nil {
return err
}
var sch schema.VersionedCueSchema
if baseonly {
sch, err = load.BaseDashboardFamily(paths)
} else {
sch, err = load.DistDashboardFamily(paths)
}
if err != nil {
return fmt.Errorf("error while loading dashboard scuemata, err: %w", err)
}
err = sch.Validate(res)
if err != nil {
return gerrors.New(errors.Details(err, nil))
}
return nil
}
func validateScuemata(p load.BaseLoadPaths, loader func(p load.BaseLoadPaths) (schema.VersionedCueSchema, error)) error {
dash, err := loader(p)
if err != nil {
return fmt.Errorf("error while loading dashboard scuemata, err: %w", err)
}
// Check that a CUE value exists.
cueValue := dash.CUE()
if !cueValue.Exists() {
return fmt.Errorf("cue value for schema does not exist")
}
// Check CUE validity.
if err := cueValue.Validate(); err != nil {
return fmt.Errorf("all schema should be valid with respect to basic CUE rules, %w", err)
}
return nil
}

View File

@ -1,70 +0,0 @@
package commands
import (
"os"
"testing"
"testing/fstest"
"github.com/grafana/grafana/pkg/schema/load"
"github.com/laher/mergefs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var defaultBaseLoadPaths = load.GetDefaultLoadPaths()
func TestValidateScuemataBasics(t *testing.T) {
t.Run("Testing scuemata validity with valid cue schemas", func(t *testing.T) {
var baseLoadPaths = load.BaseLoadPaths{
BaseCueFS: defaultBaseLoadPaths.BaseCueFS,
DistPluginCueFS: defaultBaseLoadPaths.DistPluginCueFS,
}
err := validateScuemata(baseLoadPaths, load.BaseDashboardFamily)
require.NoError(t, err, "error while loading base dashboard scuemata")
err = validateScuemata(baseLoadPaths, load.DistDashboardFamily)
require.NoError(t, err, "error while loading dist dashboard scuemata")
})
t.Run("Testing scuemata validity with invalid cue schemas - family missing", func(t *testing.T) {
t.Skip() // TODO debug, re-enable and move
genCue, err := os.ReadFile("testdata/missing_family.cue")
require.NoError(t, err)
filesystem := fstest.MapFS{
"packages/grafana-schema/src/scuemata/dashboard/dashboard.cue": &fstest.MapFile{Data: genCue},
}
mergedFS := mergefs.Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
var baseLoadPaths = load.BaseLoadPaths{
BaseCueFS: mergedFS,
DistPluginCueFS: defaultBaseLoadPaths.DistPluginCueFS,
}
err = validateScuemata(baseLoadPaths, load.BaseDashboardFamily)
assert.EqualError(t, err, "error while loading dashboard scuemata, err: dashboard schema family did not exist at expected path in expected file")
})
t.Run("Testing scuemata validity with invalid cue schemas - panel missing ", func(t *testing.T) {
t.Skip() // TODO debug, re-enable and move
genCue, err := os.ReadFile("testdata/missing_panel.cue")
require.NoError(t, err)
filesystem := fstest.MapFS{
"packages/grafana-schema/src/scuemata/dashboard/dashboard.cue": &fstest.MapFile{Data: genCue},
}
mergedFS := mergefs.Merge(filesystem, defaultBaseLoadPaths.BaseCueFS)
var baseLoadPaths = load.BaseLoadPaths{
BaseCueFS: mergedFS,
DistPluginCueFS: defaultBaseLoadPaths.DistPluginCueFS,
}
err = validateScuemata(baseLoadPaths, load.BaseDashboardFamily)
require.NoError(t, err, "error while loading base dashboard scuemata")
err = validateScuemata(baseLoadPaths, load.DistDashboardFamily)
assert.EqualError(t, err, "all schema should be valid with respect to basic CUE rules, Family.lineages.0.0: field #Panel not allowed")
})
}

View File

@ -1,78 +0,0 @@
package dashboard
import "github.com/grafana/grafana/cue/scuemata"
Dummy: scuemata.#Family & {
lineages: [
[
{ // 0.0
// Unique numeric identifier for the dashboard.
// TODO must isolate or remove identifiers local to a Grafana instance...?
id?: number
// Unique dashboard identifier that can be generated by anyone. string (8-40)
uid: string
// Title of dashboard.
title?: string
// Description of dashboard.
description?: string
gnetId?: string
// Tags associated with dashboard.
tags?: [...string]
// Theme of dashboard.
style: *"light" | "dark"
// Timezone of dashboard,
timezone?: *"browser" | "utc"
// Whether a dashboard is editable or not.
editable: bool | *true
// 0 for no shared crosshair or tooltip (default).
// 1 for shared crosshair.
// 2 for shared crosshair AND shared tooltip.
graphTooltip: >=0 & <=2 | *0
// Time range for dashboard, e.g. last 6 hours, last 7 days, etc
time?: {
from: string | *"now-6h"
to: string | *"now"
}
// Timepicker metadata.
timepicker?: {
// Whether timepicker is collapsed or not.
collapse: bool | *false
// Whether timepicker is enabled or not.
enable: bool | *true
// Whether timepicker is visible or not.
hidden: bool | *false
// Selectable intervals for auto-refresh.
refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
}
// Templating.
templating?: list: [...{...}]
// Annotations.
annotations?: list: [...{
builtIn: number | *0
// Datasource to use for annotation.
datasource: string
// Whether annotation is enabled.
enable?: bool | *true
// Whether to hide annotation.
hide?: bool | *false
// Annotation icon color.
iconColor?: string
// Name of annotation.
name?: string
type: string | *"dashboard"
// Query for annotation data.
rawQuery?: string
showIn: number | *0
}]
// Auto-refresh interval.
refresh?: string
// Version of the JSON schema, incremented each time a Grafana update brings
// changes to said schema.
schemaVersion: number | *25
// Version of the dashboard, incremented each time the dashboard is updated.
version?: number
}
]
]
}

View File

@ -1,78 +0,0 @@
package dashboard
import "github.com/grafana/grafana/cue/scuemata"
Family: scuemata.#Family & {
lineages: [
[
{ // 0.0
// Unique numeric identifier for the dashboard.
// TODO must isolate or remove identifiers local to a Grafana instance...?
id?: number
// Unique dashboard identifier that can be generated by anyone. string (8-40)
uid: string
// Title of dashboard.
title?: string
// Description of dashboard.
description?: string
gnetId?: string
// Tags associated with dashboard.
tags?: [...string]
// Theme of dashboard.
style: *"light" | "dark"
// Timezone of dashboard,
timezone?: *"browser" | "utc"
// Whether a dashboard is editable or not.
editable: bool | *true
// 0 for no shared crosshair or tooltip (default).
// 1 for shared crosshair.
// 2 for shared crosshair AND shared tooltip.
graphTooltip: >=0 & <=2 | *0
// Time range for dashboard, e.g. last 6 hours, last 7 days, etc
time?: {
from: string | *"now-6h"
to: string | *"now"
}
// Timepicker metadata.
timepicker?: {
// Whether timepicker is collapsed or not.
collapse: bool | *false
// Whether timepicker is enabled or not.
enable: bool | *true
// Whether timepicker is visible or not.
hidden: bool | *false
// Selectable intervals for auto-refresh.
refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
}
// Templating.
templating?: list: [...{...}]
// Annotations.
annotations?: list: [...{
builtIn: number | *0
// Datasource to use for annotation.
datasource: string
// Whether annotation is enabled.
enable?: bool | *true
// Whether to hide annotation.
hide?: bool | *false
// Annotation icon color.
iconColor?: string
// Name of annotation.
name?: string
type: string | *"dashboard"
// Query for annotation data.
rawQuery?: string
showIn: number | *0
}]
// Auto-refresh interval.
refresh?: string
// Version of the JSON schema, incremented each time a Grafana update brings
// changes to said schema.
schemaVersion: number | *25
// Version of the dashboard, incremented each time the dashboard is updated.
version?: number
}
]
]
}

View File

@ -1,18 +0,0 @@
{
"panels": [
{
"datasource": "${DS_GDEV-TESTDATA}",
"fieldConfig": {
"defaults": {
"custom": {
"align": "right",
"filterable": false
},
"decimals": 3,
"mappings": [],
"unit": "watt"
}
}
}
]
}

View File

@ -1,152 +0,0 @@
{
"__inputs": [
{
"name": "DS_GDEV-TESTDATA",
"label": "gdev-testdata",
"description": "",
"type": "datasource",
"pluginId": "testdata",
"pluginName": "TestData DB"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "7.5.0-pre"
},
{
"type": "panel",
"id": "table",
"name": "Table",
"version": ""
},
{
"type": "datasource",
"id": "testdata",
"name": "TestData DB",
"version": "1.0.0"
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"rawQuery": "wtf",
"showIn": 0,
"type": "dashboard"
}
]
},
"editable": true,
"graphTooltip": 0,
"id": 42,
"links": [],
"panels": [
{
"datasource": "${DS_GDEV-TESTDATA}",
"fieldConfig": {
"defaults": {
"custom": {
"align": "right",
"filterable": false
},
"decimals": 3,
"unit": "watt"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Max"
},
"properties": [
{
"id": "custom.displayMode",
"value": "lcd-gauge"
}
]
},
{
"matcher": {
"id": "byName",
"options": "A"
},
"properties": [
{
"id": "custom.width",
"value": 200
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"showHeader": true,
"sortBy": []
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1"
},
"lines": 10,
"points": [],
"pulseWave": {
"offCount": 3,
"offValue": 1,
"onCount": 3,
"onValue": 2,
"timeStep": 60
},
"refId": "A",
"scenarioId": "random_walk_table",
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": ""
}
],
"title": "Panel Title",
"type": "table",
"panelSchema": {
"maj": 0,
"min": 0
}
}
],
"schemaVersion": 27,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timezone": "browser",
"title": "with table",
"uid": "emal8gQMz",
"version": 2
}

View File

@ -1,59 +0,0 @@
package commands
import (
"bytes"
"encoding/json"
gerrors "errors"
"fmt"
"io"
"os"
"path/filepath"
"cuelang.org/go/cue/errors"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/utils"
"github.com/grafana/grafana/pkg/schema"
"github.com/grafana/grafana/pkg/schema/load"
)
func (cmd Command) trimResource(c utils.CommandLine) error {
filename := c.String("dashboard")
if filename == "" {
return gerrors.New("must specify dashboard file path with --dashboard")
}
apply := c.Bool("apply")
f, err := os.Open(filepath.Clean(filename))
if err != nil {
return err
}
b, err := io.ReadAll(f)
if err != nil {
return err
}
res := schema.Resource{Value: string(b), Name: filename}
sch, err := load.DistDashboardFamily(paths)
if err != nil {
return fmt.Errorf("error while loading dashboard scuemata, err: %w", err)
}
var out schema.Resource
if apply {
out, err = schema.ApplyDefaults(res, sch.CUE())
} else {
out, err = schema.TrimDefaults(res, sch.CUE())
}
if err != nil {
return gerrors.New(errors.Details(err, nil))
}
b = []byte(out.Value.(string))
var buf bytes.Buffer
err = json.Indent(&buf, b, "", " ")
if err != nil {
return err
}
fmt.Println(buf.String())
return nil
}

View File

@ -6,26 +6,32 @@ import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"testing/fstest"
"text/template"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/build"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/errors"
cload "cuelang.org/go/cue/load"
"cuelang.org/go/cue/load"
"cuelang.org/go/cue/parser"
"github.com/grafana/cuetsy"
"github.com/grafana/grafana/pkg/schema/load"
"github.com/grafana/grafana"
"github.com/grafana/thema"
tload "github.com/grafana/thema/load"
)
// The only import statement we currently allow in any models.cue file
const allowedImport = "github.com/grafana/grafana/packages/grafana-schema/src/schema"
const schemasPath = "github.com/grafana/grafana/packages/grafana-schema/src/schema"
// CUE import paths, mapped to corresponding TS import paths. An empty value
// 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{
allowedImport: "@grafana/schema",
"github.com/grafana/thema": "",
schemasPath: "@grafana/schema",
}
// Hard-coded list of paths to skip. Remove a particular file as we're ready
@ -44,55 +50,33 @@ var skipPaths = []string{
const prefix = "/"
var paths = load.GetDefaultLoadPaths()
// CuetsifyPlugins runs cuetsy against plugins' models.cue files.
func CuetsifyPlugins(ctx *cue.Context, root string) (WriteDiffer, error) {
lib := thema.NewLibrary(ctx)
// TODO this whole func has a lot of old, crufty behavior from the scuemata era; needs TLC
var fspaths load.BaseLoadPaths
var err error
fspaths.BaseCueFS, err = populateMapFSFromRoot(paths.BaseCueFS, root, "")
overlay := make(map[string]load.Source)
err := toOverlay(prefix, grafana.CueSchemaFS, overlay)
// err := tload.ToOverlay(prefix, grafana.CueSchemaFS, overlay)
if err != nil {
return nil, err
}
fspaths.DistPluginCueFS, err = populateMapFSFromRoot(paths.DistPluginCueFS, root, "")
if err != nil {
return nil, err
}
overlay, err := defaultOverlay(fspaths)
if err != nil {
return nil, err
}
// Prep the cue load config
clcfg := &cload.Config{
Overlay: overlay,
// FIXME these module paths won't work for things not under our cue.mod - AKA third-party plugins
ModuleRoot: prefix,
Module: "github.com/grafana/grafana",
}
// FIXME hardcoding paths to exclude is not the way to handle this
excl := map[string]bool{
"cue.mod": true,
"cue/scuemata": true,
"packages/grafana-schema/src/scuemata/dashboard": true,
"packages/grafana-schema/src/scuemata/dashboard/dist": true,
}
exclude := func(path string) bool {
dir := filepath.Dir(path)
if excl[dir] {
return true
}
for _, p := range skipPaths {
if path == p {
return true
}
}
return false
return filepath.Dir(path) == "cue.mod"
}
// Prep the cue load config
clcfg := &load.Config{
Overlay: overlay,
// FIXME these module paths won't work for things not under our cue.mod - AKA third-party plugins
ModuleRoot: prefix,
Module: "github.com/grafana/grafana",
}
outfiles := NewWriteDiffer()
@ -110,88 +94,74 @@ func CuetsifyPlugins(ctx *cue.Context, root string) (WriteDiffer, error) {
}
seen[dir] = true
clcfg.Dir = filepath.Join(root, dir)
// FIXME Horrible hack to figure out the identifier used for
// imported packages - intercept the parser called by the loader to
// look at the ast.Files on their way in to building.
// Much better if we could work backwards from the cue.Value,
// maybe even directly in cuetsy itself, and figure out when a
// referenced object is "out of bounds".
// var imports sync.Map
var imports []*ast.ImportSpec
clcfg.ParseFile = func(name string, src interface{}) (*ast.File, error) {
f, err := parser.ParseFile(name, src, parser.ParseComments)
if err != nil {
return nil, err
}
imports = append(imports, f.Imports...)
return f, nil
}
if strings.Contains(path, "public/app/plugins") {
clcfg.Package = "grafanaschema"
} else {
clcfg.Package = ""
}
// FIXME loading in this way causes all files in a dir to be loaded
// as a single cue.Instance or cue.Value, which makes it quite
// difficult to map them _back_ onto the original file and generate
// discrete .gen.ts files for each .cue input. However, going one
// .cue file at a time and passing it as the first arg to
// load.Instances() means that the other files are ignored
// completely, causing references between these files to be
// unresolved, and thus encounter a different kind of error.
insts := cload.Instances(nil, clcfg)
if len(insts) > 1 {
panic("extra instances")
}
bi := insts[0]
v := ctx.BuildInstance(bi)
if v.Err() != nil {
return v.Err()
}
var b []byte
f := &tsFile{}
seen := make(map[string]bool)
// FIXME explicitly mapping path patterns to conversion patterns
// is exactly what we want to avoid
switch {
// panel plugin models.cue files
case strings.Contains(path, "public/app/plugins"):
for _, im := range imports {
default:
insts := load.Instances(nil, clcfg)
if len(insts) > 1 {
return fmt.Errorf("%s: resulted in more than one instance", path)
}
v := ctx.BuildInstance(insts[0])
b, err = cuetsy.Generate(v, cuetsy.Config{})
if err != nil {
return err
}
case strings.Contains(path, "public/app/plugins"): // panel plugin models.cue files
// The simple - and preferable - thing would be to have plugins use the same
// package name for their models.cue as their containing dir. That's not
// possible, though, because we allow dashes in plugin names, but CUE does not
// allow them in package names. Yuck.
inst, err := loadInstancesWithThema(in, dir, "grafanaschema")
if err != nil {
return fmt.Errorf("could not load CUE instance for %s: %w", dir, err)
}
// Also parse file directly to extract imports.
// NOTE this will need refactoring to support working with more than one file at a time
of, _ := in.Open(path)
pf, _ := parser.ParseFile(filepath.Base(path), of, parser.ParseComments)
iseen := make(map[string]bool)
for _, im := range pf.Imports {
ip := strings.Trim(im.Path.Value, "\"")
if ip != allowedImport {
mappath, has := importMap[ip]
if !has {
// TODO make a specific error type for this
return errors.Newf(im.Pos(), "import %q not allowed, panel plugins may only import from %q", ip, allowedImport)
var all []string
for im := range importMap {
all = append(all, fmt.Sprintf("\t%s", im))
}
return errors.Newf(im.Pos(), "%s: import %q not allowed, panel plugins may only import from:\n%s\n", path, ip, strings.Join(all, "\n"))
}
// TODO this approach will silently swallow the unfixable
// error case where multiple files in the same dir import
// the same package to a different ident
if !seen[ip] {
seen[ip] = true
if mappath != "" && !iseen[ip] {
iseen[ip] = true
f.Imports = append(f.Imports, convertImport(im))
}
}
// Extract the latest schema and its version number. (All of this goes away with Thema, whew)
f.V = &tsModver{}
lins := v.LookupPath(cue.ParsePath("Panel.lineages"))
f.V.Lin, _ = lins.Len().Int64()
f.V.Lin = f.V.Lin - 1
schs := lins.LookupPath(cue.MakePath(cue.Index(int(f.V.Lin))))
f.V.Sch, _ = schs.Len().Int64()
f.V.Sch = f.V.Sch - 1
latest := schs.LookupPath(cue.MakePath(cue.Index(int(f.V.Sch))))
v := ctx.BuildInstance(inst)
b, err = cuetsy.Generate(latest, cuetsy.Config{})
default:
b, err = cuetsy.Generate(v, cuetsy.Config{})
lin, err := thema.BindLineage(v.LookupPath(cue.ParsePath("Panel")), lib)
if err != nil {
return fmt.Errorf("%s: failed to bind lineage: %w", path, err)
}
f.V = thema.LatestVersion(lin)
f.WriteModelVersion = true
b, err = cuetsy.Generate(thema.SchemaP(lin, f.V).UnwrapCUE(), cuetsy.Config{})
if err != nil {
return err
}
}
if err != nil {
return err
}
f.Body = string(b)
var buf bytes.Buffer
@ -201,11 +171,7 @@ func CuetsifyPlugins(ctx *cue.Context, root string) (WriteDiffer, error) {
})
}
err = cuetsify(fspaths.BaseCueFS)
if err != nil {
return nil, gerrors.New(errors.Details(err, nil))
}
err = cuetsify(fspaths.DistPluginCueFS)
err = cuetsify(grafana.CueSchemaFS)
if err != nil {
return nil, gerrors.New(errors.Details(err, nil))
}
@ -215,7 +181,7 @@ func CuetsifyPlugins(ctx *cue.Context, root string) (WriteDiffer, error) {
func convertImport(im *ast.ImportSpec) *tsImport {
tsim := &tsImport{
Pkg: importMap[allowedImport],
Pkg: importMap[schemasPath],
}
if im.Name != nil && im.Name.String() != "" {
tsim.Ident = im.Name.String()
@ -231,21 +197,101 @@ func convertImport(im *ast.ImportSpec) *tsImport {
return tsim
}
func defaultOverlay(p load.BaseLoadPaths) (map[string]cload.Source, error) {
overlay := make(map[string]cload.Source)
var themamodpath string = filepath.Join("cue.mod", "pkg", "github.com", "grafana", "thema")
if err := toOverlay(prefix, p.BaseCueFS, overlay); err != nil {
// all copied and hacked up from Thema's LoadInstancesWithThema, simply to allow setting the
// package name
func loadInstancesWithThema(modFS fs.FS, dir string, pkgname string) (*build.Instance, error) {
var modname string
err := fs.WalkDir(modFS, "cue.mod", func(path string, d fs.DirEntry, err error) error {
// fs.FS implementations tend to not use path separators as expected. Use a
// normalized one for comparisons, but retain the original for calls back into modFS.
normpath := filepath.FromSlash(path)
if err != nil {
return err
}
if d.IsDir() {
switch normpath {
case filepath.Join("cue.mod", "gen"), filepath.Join("cue.mod", "usr"):
return fs.SkipDir
case themamodpath:
return fmt.Errorf("path %q already exists in modFS passed to InstancesWithThema, must be absent for dynamic dependency injection", themamodpath)
}
return nil
} else if normpath == filepath.Join("cue.mod", "module.cue") {
modf, err := modFS.Open(path)
if err != nil {
return err
}
defer modf.Close() // nolint: errcheck
b, err := io.ReadAll(modf)
if err != nil {
return err
}
modname, err = cuecontext.New().CompileBytes(b).LookupPath(cue.MakePath(cue.Str("module"))).String()
if err != nil {
return err
}
if modname == "" {
return fmt.Errorf("InstancesWithThema requires non-empty module name in modFS' cue.mod/module.cue")
}
}
return nil
})
if err != nil {
return nil, err
}
if err := toOverlay(prefix, p.DistPluginCueFS, overlay); err != nil {
if modname == "" {
return nil, errors.New("cue.mod/module.cue did not exist")
}
modroot := filepath.FromSlash(filepath.Join("/", modname))
overlay := make(map[string]load.Source)
if err := tload.ToOverlay(modroot, modFS, overlay); err != nil {
return nil, err
}
return overlay, nil
// Special case for when we're calling this loader with paths inside the thema module
if modname == "github.com/grafana/thema" {
if err := tload.ToOverlay(modroot, thema.CueJointFS, overlay); err != nil {
return nil, err
}
} else {
if err := tload.ToOverlay(filepath.Join(modroot, themamodpath), thema.CueFS, overlay); err != nil {
return nil, err
}
}
if dir == "" {
dir = "."
}
cfg := &load.Config{
Overlay: overlay,
ModuleRoot: modroot,
Module: modname,
Dir: filepath.Join(modroot, dir),
Package: pkgname,
}
if dir == "." {
cfg.Package = filepath.Base(modroot)
cfg.Dir = modroot
}
inst := load.Instances(nil, cfg)[0]
if inst.Err != nil {
return nil, inst.Err
}
return inst, nil
}
func toOverlay(prefix string, vfs fs.FS, overlay map[string]cload.Source) error {
func toOverlay(prefix string, vfs fs.FS, overlay map[string]load.Source) error {
if !filepath.IsAbs(prefix) {
return fmt.Errorf("must provide absolute path prefix when generating cue overlay, got %q", prefix)
}
@ -274,7 +320,7 @@ func toOverlay(prefix string, vfs fs.FS, overlay map[string]cload.Source) error
return err
}
overlay[filepath.Join(prefix, path)] = cload.FromBytes(b)
overlay[filepath.Join(prefix, path)] = load.FromBytes(b)
return nil
})
@ -285,44 +331,11 @@ func toOverlay(prefix string, vfs fs.FS, overlay map[string]cload.Source) error
return nil
}
// Helper function that populates an fs.FS by walking over a virtual filesystem,
// and reading files from disk corresponding to each file encountered.
func populateMapFSFromRoot(in fs.FS, root, join string) (fs.FS, error) {
out := make(fstest.MapFS)
err := fs.WalkDir(in, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
// Ignore gosec warning G304. The input set here is necessarily
// constrained to files specified in embed.go
// nolint:gosec
b, err := os.Open(filepath.Join(root, join, path))
if err != nil {
return err
}
byt, err := io.ReadAll(b)
if err != nil {
return err
}
out[path] = &fstest.MapFile{Data: byt}
return nil
})
return out, err
}
type tsFile struct {
V *tsModver
Imports []*tsImport
Body string
}
type tsModver struct {
Lin, Sch int64
V thema.SyntacticVersion
WriteModelVersion bool
Imports []*tsImport
Body string
}
type tsImport struct {
@ -337,7 +350,7 @@ var tsTemplate = template.Must(template.New("cuetsygen").Parse(`//~~~~~~~~~~~~~~
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
{{range .Imports}}
import * as {{.Ident}} from '{{.Pkg}}';{{end}}
{{if .V}}
export const modelVersion = Object.freeze([{{ .V.Lin }}, {{ .V.Sch }}]);
{{if .WriteModelVersion }}
export const modelVersion = Object.freeze([{{index .V 0}}, {{index .V 1}}]);
{{end}}
{{.Body}}`))

View File

@ -17,8 +17,8 @@ import (
"github.com/grafana/thema/load"
)
var ctx *cue.Context = cuecontext.New()
var lib thema.Library = thema.NewLibrary(ctx)
var ctx = cuecontext.New()
var lib = thema.NewLibrary(ctx)
// ProvideCUEContext is a wire service provider of a central cue.Context.
func ProvideCUEContext() *cue.Context {
@ -82,6 +82,12 @@ func LoadGrafanaInstancesWithThema(
return lin, nil
}
// prefixWithGrafanaCUE constructs an fs.FS that merges the provided fs.FS with one
// containing grafana's cue.mod at the root. The provided prefix should be the
//
// The returned fs.FS is suitable for passing to a CUE loader, such as
// cuelang.org/cue/load.Instances or
// github.com/grafana/thema/load.InstancesWithThema.
func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) {
m := fstest.MapFS{
// fstest can recognize only forward slashes.
@ -89,7 +95,7 @@ func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) {
}
prefix = filepath.FromSlash(prefix)
err := fs.WalkDir(inputfs, ".", (func(path string, d fs.DirEntry, err error) error {
err := fs.WalkDir(inputfs, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
@ -111,7 +117,7 @@ func prefixWithGrafanaCUE(prefix string, inputfs fs.FS) (fs.FS, error) {
// fstest can recognize only forward slashes.
m[filepath.ToSlash(filepath.Join(prefix, path))] = &fstest.MapFile{Data: b}
return nil
}))
})
return m, err
}

View File

@ -1,94 +0,0 @@
package load
import (
"fmt"
"io"
"io/fs"
"path/filepath"
"cuelang.org/go/cue/cuecontext"
"cuelang.org/go/cue/load"
"github.com/grafana/grafana"
)
var ctx = cuecontext.New()
// Families can have variants, where more typing information narrows the
// possible values for certain keys in schemas. These are a meta-property
// of the schema, effectively encoded in these loaders.
//
// We can generally define three variants:
// - "Base": strictly core schema files, no plugins. (go:embed-able)
// - "Dist": "Base" + plugins that ship with vanilla Grafana (go:embed-able)
// - "Instance": "Dist" + the non-core plugins available in an actual, running Grafana
// BaseLoadPaths contains the configuration for loading a DistDashboard
type BaseLoadPaths struct {
// BaseCueFS should be rooted at a directory containing the filesystem layout
// expected to exist at github.com/grafana/grafana/cue.
BaseCueFS fs.FS
// DistPluginCueFS should point to some fs path (TBD) under which all core
// plugins live.
DistPluginCueFS fs.FS
// InstanceCueFS should point to a root dir in which non-core plugins live.
// Normal case will be that this only happens when an actual Grafana
// instance is making the call, and has a plugin dir to offer - though
// external tools could always create their own dirs shaped like a Grafana
// plugin dir, and point to those.
InstanceCueFS fs.FS
}
func GetDefaultLoadPaths() BaseLoadPaths {
return BaseLoadPaths{
BaseCueFS: grafana.CoreSchema,
DistPluginCueFS: grafana.PluginSchema,
}
}
// toOverlay converts all .cue files in the fs.FS into Source entries in an
// overlay map, as expected by load.Config.
//
// Each entry is placed in the map with the provided prefix - which must be an
// absolute path - ahead of the actual path of the added file within the fs.FS.
//
// The function writes into the provided overlay map, to facilitate the
// construction of a single overlay map from multiple fs.FS.
//
// All files reachable by walking the provided fs.FS are added to the overlay
// map, on the premise that control over the FS is sufficient to allow any
// desired filtering.
func toOverlay(prefix string, vfs fs.FS, overlay map[string]load.Source) error {
if !filepath.IsAbs(prefix) {
return fmt.Errorf("must provide absolute path prefix when generating cue overlay, got %q", prefix)
}
err := fs.WalkDir(vfs, ".", (func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
f, err := vfs.Open(path)
if err != nil {
return err
}
b, err := io.ReadAll(f)
if err != nil {
return err
}
overlay[filepath.Join(prefix, path)] = load.FromBytes(b)
return nil
}))
if err != nil {
return err
}
return nil
}

View File

@ -1,6 +0,0 @@
//go:build !windows
// +build !windows
package load
const prefix = "/"

View File

@ -1,6 +0,0 @@
//go:build windows
// +build windows
package load
const prefix = "C:\\"

View File

@ -1,214 +0,0 @@
package load
import (
"errors"
"fmt"
"path/filepath"
"cuelang.org/go/cue"
"cuelang.org/go/cue/load"
"github.com/grafana/grafana/pkg/schema"
)
var panelSubpath = cue.MakePath(cue.Def("#Panel"))
var dashboardDir = filepath.Join("packages", "grafana-schema", "src", "scuemata", "dashboard")
func defaultOverlay(p BaseLoadPaths) (map[string]load.Source, error) {
overlay := make(map[string]load.Source)
if err := toOverlay(prefix, p.BaseCueFS, overlay); err != nil {
return nil, err
}
if err := toOverlay(prefix, p.DistPluginCueFS, overlay); err != nil {
return nil, err
}
return overlay, nil
}
// BaseDashboardFamily loads the family of schema representing the "Base" variant of
// a Grafana dashboard: the core-defined dashboard schema that applies universally to
// all dashboards, independent of any plugins.
//
// The returned VersionedCueSchema will always be the oldest schema in the
// family: the 0.0 schema. schema.Find() provides easy traversal to newer schema
// versions.
func BaseDashboardFamily(p BaseLoadPaths) (schema.VersionedCueSchema, error) {
v, err := baseDashboardFamily(p)
if err != nil {
return nil, err
}
return buildGenericScuemata(v)
}
// Helper that gets the entire scuemata family, for reuse by Dist/Instance callers.
func baseDashboardFamily(p BaseLoadPaths) (cue.Value, error) {
overlay, err := defaultOverlay(p)
if err != nil {
return cue.Value{}, err
}
cfg := &load.Config{
Overlay: overlay,
ModuleRoot: prefix,
Module: "github.com/grafana/grafana",
Dir: filepath.Join(prefix, dashboardDir),
}
inst := ctx.BuildInstance(load.Instances(nil, cfg)[0])
if inst.Err() != nil {
cueError := schema.WrapCUEError(inst.Err())
if inst.Err() != nil {
return cue.Value{}, cueError
}
}
famval := inst.LookupPath(cue.MakePath(cue.Str("Family")))
if !famval.Exists() {
return cue.Value{}, errors.New("dashboard schema family did not exist at expected path in expected file")
}
return famval, nil
}
// DistDashboardFamily loads the family of schema representing the "Dist"
// variant of a Grafana dashboard: the "Base" variant (see
// BaseDashboardFamily()), but constrained such that all substructures (e.g.
// panels) must be valid with respect to the schemas provided by the core
// plugins that ship with Grafana.
//
// The returned VersionedCueSchema will always be the oldest schema in the
// family: the 0.0 schema. schema.Find() provides easy traversal to newer schema
// versions.
func DistDashboardFamily(p BaseLoadPaths) (schema.VersionedCueSchema, error) {
famval, err := baseDashboardFamily(p)
if err != nil {
return nil, err
}
scuemap, err := loadPanelScuemata(p)
if err != nil {
return nil, err
}
// TODO see if unifying into the expected form in a loop, then unifying that
// consolidated form improves performance
for typ, fam := range scuemap {
famval = famval.FillPath(cue.MakePath(cue.Str("compose"), cue.Str("Panel"), cue.Str(typ)), fam)
}
head, err := buildGenericScuemata(famval)
if err != nil {
return nil, err
}
// TODO sloppy duplicate logic of what's in readPanelModels(), for now
all := make(map[string]schema.VersionedCueSchema)
for id, val := range scuemap {
fam, err := buildGenericScuemata(val)
if err != nil {
return nil, err
}
all[id] = fam
}
var first, prev *compositeDashboardSchema
for head != nil {
cds := &compositeDashboardSchema{
base: head,
actual: head.CUE(),
panelFams: all,
// TODO migrations
migration: terminalMigrationFunc,
}
if prev == nil {
first = cds
} else {
prev.next = cds
}
prev = cds
head = head.Successor()
}
return first, nil
}
type compositeDashboardSchema struct {
// The base/root dashboard schema
base schema.VersionedCueSchema
actual cue.Value
next *compositeDashboardSchema
migration migrationFunc
panelFams map[string]schema.VersionedCueSchema
}
// Validate checks that the resource is correct with respect to the schema.
func (cds *compositeDashboardSchema) Validate(r schema.Resource) error {
name := r.Name
if name == "" {
name = "resource"
}
rv := ctx.CompileString(r.Value.(string), cue.Filename(name))
if rv.Err() != nil {
return rv.Err()
}
return cds.actual.Unify(rv).Validate(cue.Concrete(true))
}
// CUE returns the cue.Value representing the actual schema.
func (cds *compositeDashboardSchema) CUE() cue.Value {
return cds.actual
}
// Version reports the major and minor versions of the schema.
func (cds *compositeDashboardSchema) Version() (major int, minor int) {
return cds.base.Version()
}
// Returns the next VersionedCueSchema
func (cds *compositeDashboardSchema) Successor() schema.VersionedCueSchema {
if cds.next == nil {
// Untyped nil, allows `<sch> == nil` checks to work as people expect
return nil
}
return cds.next
}
func (cds *compositeDashboardSchema) Migrate(x schema.Resource) (schema.Resource, schema.VersionedCueSchema, error) { // TODO restrict input/return type to concrete
r, sch, err := cds.migration(x.Value)
if err != nil || sch == nil {
// TODO fix sloppy types
r = x.Value.(cue.Value)
}
return schema.Resource{Value: r}, sch, nil
}
func (cds *compositeDashboardSchema) LatestPanelSchemaFor(id string) (schema.VersionedCueSchema, error) {
// So much slop rn, but it's OK because i FINALLY know where this is going!
psch, has := cds.panelFams[id]
if !has {
// TODO typed errors
return nil, fmt.Errorf("unknown panel plugin type %q", id)
}
latest := schema.Find(psch, schema.Latest())
// FIXME this relies on old sloppiness
sch := &genericVersionedSchema{
actual: cds.base.CUE().LookupPath(panelSubpath).Unify(mapPanelModel(id, latest)),
}
sch.major, sch.minor = latest.Version()
return sch, nil
}
// One-off special interface for dashboard composite schema, until the composite
// dashboard schema pattern is fully generalized.
//
// NOTE: THIS IS A TEMPORARY TYPE. IT WILL BE REPLACED WITH A GENERIC INTERFACE
// TO REPRESENT COMPOSITIONAL SCHEMA FAMILY PRIOR TO GRAFANA 8. UPDATING WILL
// SHOULD BE TRIVIAL, BUT IT WILL CAUSE BREAKAGES.
type CompositeDashboardSchema interface {
schema.VersionedCueSchema
LatestPanelSchemaFor(id string) (schema.VersionedCueSchema, error)
}

View File

@ -1,178 +0,0 @@
package load
import (
"path/filepath"
"cuelang.org/go/cue"
"cuelang.org/go/cue/load"
"github.com/grafana/grafana/pkg/schema"
)
// getBaseScuemata attempts to load the base scuemata family and schema
// definitions on which all Grafana scuemata rely.
//
// TODO probably cache this or something
func getBaseScuemata(p BaseLoadPaths) (cue.Value, error) {
overlay := make(map[string]load.Source)
if err := toOverlay(filepath.Join(prefix, "grafana"), p.BaseCueFS, overlay); err != nil {
return cue.Value{}, err
}
cfg := &load.Config{
Overlay: overlay,
Package: "scuemata",
// TODO Semantics of loading instances is quite confusing. This 'Dir'
// field is a case in point. It must be set to "/" in order for the
// overlay to be searched and have all files loaded in the cue/scuemata
// directory. (This isn't necessary when loading individual .cue files.)
// But anchoring a search at root seems like we're begging for
// vulnerabilities where Grafana can read and print out anything on the
// filesystem, which can be a disclosure problem, unless we're
// absolutely sure the search is within a virtual filesystem. Which i'm
// not.
//
// And no, changing the toOverlay() to have a subpath and the
// load.Instances to mirror that subpath does not allow us to get rid of
// this "/".
Dir: prefix,
}
res := ctx.BuildInstance(load.Instances([]string{
filepath.Join(prefix, "grafana", "cue", "scuemata", "scuemata.cue"),
filepath.Join(prefix, "grafana", "cue", "scuemata", "panel-plugin.cue"),
}, cfg)[0])
return res, res.Err()
}
func buildGenericScuemata(famval cue.Value) (schema.VersionedCueSchema, error) {
// TODO verify subsumption by #Family; renders many
// error checks below unnecessary
majiter, err := famval.LookupPath(cue.MakePath(cue.Str("lineages"))).List()
if err != nil {
return nil, err
}
var major int
var first, lastgvs *genericVersionedSchema
for majiter.Next() {
var minor int
miniter, _ := majiter.Value().List()
for miniter.Next() {
gvs := &genericVersionedSchema{
actual: miniter.Value(),
major: major,
minor: minor,
// This gets overwritten on all but the very final schema
migration: terminalMigrationFunc,
}
if minor != 0 {
// TODO Verify that this schema is backwards compat with prior.
// Create an implicit migration operation on the prior schema.
lastgvs.migration = implicitMigration(gvs.actual, gvs)
lastgvs.next = gvs
} else if major != 0 {
lastgvs.next = gvs
// x.0. There should exist an explicit migration definition;
// load it up and ready it for use, and place it on the final
// schema in the prior sequence.
//
// Also...should at least try to make sure it's pointing at the
// expected schema, to maintain our invariants?
// TODO impl
} else {
first = gvs
}
lastgvs = gvs
minor++
}
major++
}
return first, nil
}
type genericVersionedSchema struct {
actual cue.Value
major int
minor int
next *genericVersionedSchema
migration migrationFunc
}
// Validate checks that the resource is correct with respect to the schema.
func (gvs *genericVersionedSchema) Validate(r schema.Resource) error {
name := r.Name
if name == "" {
name = "resource"
}
rv := ctx.CompileString(r.Value.(string), cue.Filename(name))
if rv.Err() != nil {
return rv.Err()
}
return gvs.actual.Unify(rv).Validate(cue.Concrete(true))
}
// CUE returns the cue.Value representing the actual schema.
func (gvs *genericVersionedSchema) CUE() cue.Value {
return gvs.actual
}
// Version reports the major and minor versions of the schema.
func (gvs *genericVersionedSchema) Version() (major int, minor int) {
return gvs.major, gvs.minor
}
// Returns the next VersionedCueSchema
func (gvs *genericVersionedSchema) Successor() schema.VersionedCueSchema {
if gvs.next == nil {
// Untyped nil, allows `<sch> == nil` checks to work as people expect
return nil
}
return gvs.next
}
// Migrate transforms a resource into a new Resource that is correct with
// respect to its Successor schema.
func (gvs *genericVersionedSchema) Migrate(x schema.Resource) (schema.Resource, schema.VersionedCueSchema, error) { // TODO restrict input/return type to concrete
r, sch, err := gvs.migration(x.Value)
if err != nil || sch == nil {
r = x.Value.(cue.Value)
}
return schema.Resource{Value: r}, sch, nil
}
type migrationFunc func(x interface{}) (cue.Value, schema.VersionedCueSchema, error)
var terminalMigrationFunc = func(x interface{}) (cue.Value, schema.VersionedCueSchema, error) {
// TODO send back the input
return cue.Value{}, nil, nil
}
// panic if called
// var panicMigrationFunc = func(x interface{}) (cue.Value, schema.VersionedCueSchema, error) {
// panic("migrations are not yet implemented")
// }
// Creates a func to perform a "migration" that simply unifies the input
// artifact (which is expected to have already have been validated against an
// earlier schema) with a later schema.
func implicitMigration(v cue.Value, next schema.VersionedCueSchema) migrationFunc {
return func(x interface{}) (cue.Value, schema.VersionedCueSchema, error) {
w := v.FillPath(cue.Path{}, x)
// TODO is it possible that migration would be successful, but there
// still exists some error here? Need to better understand internal CUE
// erroring rules? seems like incomplete cue.Value may always an Err()?
//
// TODO should check concreteness here? Or can we guarantee a priori it
// can be made concrete simply by looking at the schema, before
// implicitMigration() is called to create this function?
if w.Err() != nil {
return w, nil, w.Err()
}
return w, next, w.Err()
}
}

View File

@ -1,281 +0,0 @@
package load
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"testing/fstest"
"cuelang.org/go/cue"
"cuelang.org/go/cue/errors"
"cuelang.org/go/cue/load"
cuejson "cuelang.org/go/pkg/encoding/json"
"github.com/grafana/grafana/pkg/coremodel/dashboard"
"github.com/grafana/grafana/pkg/schema"
"github.com/laher/mergefs"
"github.com/stretchr/testify/require"
)
var (
p = GetDefaultLoadPaths()
update = flag.Bool("update", false, "update golden files")
)
type testfunc func(*testing.T, schema.VersionedCueSchema, []byte, fs.FileInfo, string)
// for now we keep the validdir as input parameter since for trim apply default we can't use devenv directory yet,
// otherwise we can hardcoded validdir and just pass the testtype is more than enough.
// TODO: remove validdir once we can test directly with devenv folder
var doTestAgainstDevenv = func(sch schema.VersionedCueSchema, validdir string, fn testfunc) func(t *testing.T) {
return func(t *testing.T) {
require.NoError(t, filepath.Walk(validdir, func(path string, d fs.FileInfo, err error) error {
require.NoError(t, err)
if d.IsDir() || filepath.Ext(d.Name()) != ".json" {
return nil
}
// Ignore gosec warning G304 since it's a test
// nolint:gosec
b, err := os.Open(path)
require.NoError(t, err, "failed to open dashboard file")
jtree := make(map[string]interface{})
byt, err := io.ReadAll(b)
if err != nil {
t.Fatal(err)
}
require.NoError(t, json.Unmarshal(byt, &jtree))
if oldschemav, has := jtree["schemaVersion"]; !has {
t.Logf("no schemaVersion in %s", path)
return nil
} else {
if !(oldschemav.(float64) > dashboard.HandoffSchemaVersion-1) {
if testing.Verbose() {
t.Logf("schemaVersion is %v, older than %v, skipping %s", oldschemav, dashboard.HandoffSchemaVersion-1, path)
}
return nil
}
}
t.Run(filepath.Base(path), func(t *testing.T) {
fn(t, sch, byt, d, path)
})
return nil
}))
}
}
// Basic well-formedness tests on core scuemata.
func TestScuemataBasics(t *testing.T) {
all := make(map[string]schema.VersionedCueSchema)
dash, err := BaseDashboardFamily(p)
require.NoError(t, err, "error while loading base dashboard scuemata")
all["basedash"] = dash
ddash, err := DistDashboardFamily(p)
require.NoError(t, err, "error while loading dist dashboard scuemata")
all["distdash"] = ddash
for set, sch := range all {
t.Run(set, func(t *testing.T) {
require.NotNil(t, sch, "scuemata for %q linked to empty chain", set)
maj, min := sch.Version()
t.Run(fmt.Sprintf("%v.%v", maj, min), func(t *testing.T) {
cv := sch.CUE()
t.Run("Exists", func(t *testing.T) {
require.True(t, cv.Exists(), "cue value for schema does not exist")
})
t.Run("Validate", func(t *testing.T) {
require.NoError(t, cv.Validate(), "all schema should be valid with respect to basic CUE rules")
})
})
})
}
}
func TestDevenvDashboardValidity(t *testing.T) {
// TODO will need to expand this appropriately when the scuemata contain
// more than one schema
var validdir = filepath.Join("..", "..", "..", "devenv", "dev-dashboards")
dash, err := BaseDashboardFamily(p)
require.NoError(t, err, "error while loading base dashboard scuemata")
dashboardValidity := func(t *testing.T, sch schema.VersionedCueSchema, byt []byte, d fs.FileInfo, path string) {
err := sch.Validate(schema.Resource{Value: string(byt), Name: path})
if err != nil {
// Testify trims errors to short length. We want the full text
errstr := errors.Details(err, nil)
t.Log(errstr)
if strings.Contains(errstr, "null") {
t.Log("validation failure appears to involve nulls - see if scripts/stripnulls.sh has any effect?")
}
t.FailNow()
}
}
t.Run("base", doTestAgainstDevenv(dash, validdir, dashboardValidity))
ddash, err := DistDashboardFamily(p)
require.NoError(t, err, "error while loading dist dashboard scuemata")
t.Run("dist", doTestAgainstDevenv(ddash, validdir, dashboardValidity))
}
// TO update the golden file located in pkg/schema/testdata/devenvgoldenfiles
// run go test -v ./pkg/schema/load/... -update
func TestUpdateDevenvDashboardGoldenFiles(t *testing.T) {
flag.Parse()
if *update {
ddash, err := DistDashboardFamily(p)
require.NoError(t, err, "error while loading dist dashboard scuemata")
var validdir = filepath.Join("..", "..", "..", "devenv", "dev-dashboards")
goldenFileUpdate := func(t *testing.T, sch schema.VersionedCueSchema, byt []byte, d fs.FileInfo, _ string) {
dsSchema, err := schema.SearchAndValidate(sch, string(byt))
require.NoError(t, err)
origin, err := schema.ApplyDefaults(schema.Resource{Value: string(byt)}, dsSchema.CUE())
require.NoError(t, err)
var prettyJSON bytes.Buffer
err = json.Indent(&prettyJSON, []byte(origin.Value.(string)), "", "\t")
require.NoError(t, err)
err = ioutil.WriteFile(filepath.Join("..", "testdata", "devenvgoldenfiles", d.Name()), prettyJSON.Bytes(), 0644)
require.NoError(t, err)
}
t.Run("updategoldenfile", doTestAgainstDevenv(ddash, validdir, goldenFileUpdate))
}
}
func TestDevenvDashboardTrimApplyDefaults(t *testing.T) {
ddash, err := DistDashboardFamily(p)
require.NoError(t, err, "error while loading dist dashboard scuemata")
// TODO will need to expand this appropriately when the scuemata contain
// more than one schema
validdir := filepath.Join("..", "testdata", "devenvgoldenfiles")
trimApplyDefaults := func(t *testing.T, sch schema.VersionedCueSchema, byt []byte, d fs.FileInfo, path string) {
dsSchema, err := schema.SearchAndValidate(sch, string(byt))
require.NoError(t, err)
// Trimmed default json file
trimmed, err := schema.TrimDefaults(schema.Resource{Value: string(byt)}, dsSchema.CUE())
require.NoError(t, err)
// store the trimmed result into testdata for easy debug
out, err := schema.ApplyDefaults(trimmed, dsSchema.CUE())
require.NoError(t, err)
require.JSONEq(t, string(byt), out.Value.(string))
}
t.Run("defaults", doTestAgainstDevenv(ddash, validdir, trimApplyDefaults))
}
func TestPanelValidity(t *testing.T) {
t.Skip()
validdir := os.DirFS(filepath.Join("testdata", "artifacts", "panels"))
ddash, err := DistDashboardFamily(p)
require.NoError(t, err, "error while loading dist dashboard scuemata")
// TODO hmm, it's awkward for this test's structure to have to pick just one
// type of panel plugin, but we can change the test structure. However, is
// there any other situation where we want the panel subschema with all
// possible disjunctions? If so, maybe the interface needs work. Or maybe
// just defer that until the proper generic composite scuemata impl.
dpan, err := ddash.(CompositeDashboardSchema).LatestPanelSchemaFor("table")
require.NoError(t, err, "error while loading panel subschema")
require.NoError(t, fs.WalkDir(validdir, ".", func(path string, d fs.DirEntry, err error) error {
require.NoError(t, err)
if d.IsDir() || filepath.Ext(d.Name()) != ".json" {
return nil
}
t.Run(path, func(t *testing.T) {
// TODO FIXME stop skipping once we actually have the schema filled in
// enough that the tests pass, lol
b, err := validdir.Open(path)
require.NoError(t, err, "failed to open panel file")
err = dpan.Validate(schema.Resource{Value: b})
require.NoError(t, err, "panel failed validation")
})
return nil
}))
}
func TestCueErrorWrapper(t *testing.T) {
a := fstest.MapFS{
filepath.Join(dashboardDir, "dashboard.cue"): &fstest.MapFile{Data: []byte("package dashboard\n{;;;;;;;;}")},
}
filesystem := mergefs.Merge(a, GetDefaultLoadPaths().BaseCueFS)
var baseLoadPaths = BaseLoadPaths{
BaseCueFS: filesystem,
DistPluginCueFS: GetDefaultLoadPaths().DistPluginCueFS,
}
_, err := BaseDashboardFamily(baseLoadPaths)
require.Error(t, err)
require.Contains(t, err.Error(), "in file")
require.Contains(t, err.Error(), "line: ")
_, err = DistDashboardFamily(baseLoadPaths)
require.Error(t, err)
require.Contains(t, err.Error(), "in file")
require.Contains(t, err.Error(), "line: ")
}
func TestAllPluginsInDist(t *testing.T) {
overlay, err := defaultOverlay(p)
require.NoError(t, err)
cfg := &load.Config{
Overlay: overlay,
ModuleRoot: prefix,
Module: "github.com/grafana/grafana",
Dir: filepath.Join(prefix, dashboardDir, "dist"),
Package: "dist",
}
inst := ctx.BuildInstance(load.Instances(nil, cfg)[0])
require.NoError(t, inst.Err())
dinst := ctx.CompileString(`
Family: compose: Panel: {}
typs: [for typ, _ in Family.compose.Panel {typ}]
`, cue.Filename("str"))
require.NoError(t, dinst.Err())
typs := dinst.Unify(inst).LookupPath(cue.MakePath(cue.Str("typs")))
j, err := cuejson.Marshal(typs)
require.NoError(t, err)
var importedPanelTypes, loadedPanelTypes []string
require.NoError(t, json.Unmarshal([]byte(j), &importedPanelTypes))
// TODO a more canonical way of getting all the dist plugin types with
// models.cue would be nice.
m, err := loadPanelScuemata(p)
require.NoError(t, err)
for typ := range m {
loadedPanelTypes = append(loadedPanelTypes, typ)
}
sort.Strings(importedPanelTypes)
sort.Strings(loadedPanelTypes)
require.Equal(t, loadedPanelTypes, importedPanelTypes, "%s/family.cue needs updating, it must compose the same set of panel plugin models that are found by the plugin loader", cfg.Dir)
}

View File

@ -1,137 +0,0 @@
package load
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"io/ioutil"
"path/filepath"
"cuelang.org/go/cue"
"cuelang.org/go/cue/load"
"github.com/grafana/grafana/pkg/schema"
)
// mapPanelModel maps a schema from the #PanelModel form in which it's declared
// in a plugin's model.cue to the structure in which it actually appears in the
// dashboard schema.
// TODO remove, this is old sloppy hacks
func mapPanelModel(id string, vcs schema.VersionedCueSchema) cue.Value {
maj, min := vcs.Version()
// Ignore err return, this can't fail to compile
inter := ctx.CompileString(fmt.Sprintf(`
in: {
type: %q
v: {
maj: %d
min: %d
}
model: {...}
}
result: {
type: in.type,
panelSchema: maj: in.v.maj
panelSchema: min: in.v.min
options: in.model.PanelOptions
fieldConfig: defaults: custom: {}
if in.model.PanelFieldConfig != _|_ {
fieldConfig: defaults: custom: in.model.PanelFieldConfig
}
}
`, id, maj, min), cue.Filename(fmt.Sprintf("%s-glue-panelComposition", id)))
// TODO validate, especially with #PanelModel
return inter.FillPath(cue.MakePath(cue.Str("in"), cue.Str("model")), vcs.CUE()).LookupPath(cue.MakePath(cue.Str(("result"))))
}
func loadPanelScuemata(p BaseLoadPaths) (map[string]cue.Value, error) {
overlay := make(map[string]load.Source)
if err := toOverlay(prefix, p.BaseCueFS, overlay); err != nil {
return nil, err
}
if err := toOverlay(prefix, p.DistPluginCueFS, overlay); err != nil {
return nil, err
}
base, err := getBaseScuemata(p)
if err != nil {
return nil, err
}
pmf := base.LookupPath(cue.MakePath(cue.Def("#PanelFamily")))
if !pmf.Exists() {
return nil, errors.New("could not locate #PanelFamily definition")
}
all := make(map[string]cue.Value)
err = fs.WalkDir(p.DistPluginCueFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || d.Name() != "plugin.json" {
return nil
}
dpath := filepath.Dir(path)
// For now, skip plugins without a models.cue
_, err = p.DistPluginCueFS.Open(filepath.Join(dpath, "models.cue"))
if err != nil {
return nil
}
fi, err := p.DistPluginCueFS.Open(path)
if err != nil {
return err
}
b, err := ioutil.ReadAll(fi)
if err != nil {
return err
}
jmap := make(map[string]interface{})
err = json.Unmarshal(b, &jmap)
if err != nil {
return err
}
iid, has := jmap["id"]
if !has || jmap["type"] != "panel" {
return errors.New("no type field in plugin.json or not a panel type plugin")
}
id := iid.(string)
cfg := &load.Config{
Package: "grafanaschema",
Overlay: overlay,
}
li := load.Instances([]string{filepath.Join("/", dpath, "models.cue")}, cfg)
imod := ctx.BuildInstance(li[0])
if imod.Err() != nil {
return imod.Err()
}
// Get the Family declaration in the models.cue file...
pmod := imod.LookupPath(cue.MakePath(cue.Str("Panel")))
if !pmod.Exists() {
return fmt.Errorf("%s does not contain a declaration of its models at path 'Family'", path)
}
// Ensure the declared value is subsumed by/correct wrt #PanelFamily
// TODO not actually sure that Final is what we want here.
if err := pmf.Subsume(pmod, cue.Final()); err != nil {
return err
}
all[id] = pmod
return nil
})
if err != nil {
return nil, err
}
return all, nil
}

View File

@ -1,3 +0,0 @@
All artifact JSON contained in these subdirectories should be valid. Invalid
JSON is handled elsewhere, as it must be coupled with expected error messages
for testing purposes.

View File

@ -1,152 +0,0 @@
{
"__inputs": [
{
"name": "DS_GDEV-TESTDATA",
"label": "gdev-testdata",
"description": "",
"type": "datasource",
"pluginId": "testdata",
"pluginName": "TestData DB"
}
],
"__requires": [
{
"type": "grafana",
"id": "grafana",
"name": "Grafana",
"version": "7.5.0-pre"
},
{
"type": "panel",
"id": "table",
"name": "Table",
"version": ""
},
{
"type": "datasource",
"id": "testdata",
"name": "TestData DB",
"version": "1.0.0"
}
],
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"rawQuery": "wtf",
"showIn": 0,
"type": "dashboard"
}
]
},
"editable": true,
"graphTooltip": 0,
"id": 42,
"links": [],
"panels": [
{
"datasource": "${DS_GDEV-TESTDATA}",
"fieldConfig": {
"defaults": {
"custom": {
"align": "right",
"filterable": false
},
"decimals": 3,
"unit": "watt"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Max"
},
"properties": [
{
"id": "custom.displayMode",
"value": "lcd-gauge"
}
]
},
{
"matcher": {
"id": "byName",
"options": "A"
},
"properties": [
{
"id": "custom.width",
"value": 200
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"showHeader": true,
"sortBy": []
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1"
},
"lines": 10,
"points": [],
"pulseWave": {
"offCount": 3,
"offValue": 1,
"onCount": 3,
"onValue": 2,
"timeStep": 60
},
"refId": "A",
"scenarioId": "random_walk_table",
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": ""
}
],
"title": "Panel Title",
"type": "table",
"panelSchema": {
"maj": 0,
"min": 0
}
}
],
"schemaVersion": 27,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timezone": "browser",
"title": "with table",
"uid": "emal8gQMz",
"version": 2
}

View File

@ -1,84 +0,0 @@
{
"datasource": "${DS_GDEV-TESTDATA}",
"fieldConfig": {
"defaults": {
"custom": {
"align": "right",
"filterable": false
},
"decimals": 3,
"unit": "watt"
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Max"
},
"properties": [
{
"id": "custom.displayMode",
"value": "lcd-gauge"
}
]
},
{
"matcher": {
"id": "byName",
"options": "A"
},
"properties": [
{
"id": "custom.width",
"value": 200
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"options": {
"showHeader": true,
"sortBy": []
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1"
},
"lines": 10,
"points": [],
"pulseWave": {
"offCount": 3,
"offValue": 1,
"onCount": 3,
"onValue": 2,
"timeStep": 60
},
"refId": "A",
"scenarioId": "random_walk_table",
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": ""
}
],
"title": "Panel Title",
"type": "table",
"panelSchema": {
"maj": 0,
"min": 0
}
}

View File

@ -1 +0,0 @@
foo

View File

@ -1,104 +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 grafanaschema
import (
ui "github.com/grafana/grafana/cue/ui:grafanaschema"
)
// TODO should we remove Family, and make lineages and migrations top-level values?
// It's easy to do, and arguably increases clarity of this crucial file by
// reducing one layer of nesting. But it sorta requires understanding that CUE
// also thinks of an entire file (aka, an "instance") as a struct in order for
// it to make sense that the file itself is schematized by #PanelFamily. What's
// the best DX here?
// "Family" must be an instance of the #PanelFamily type, defined in
// cue/scuemata/panel-plugin.cue. This ensures some key invariants:
//
// - lineages is an array of arrays. Outer array is major version, inner is minor.
// (This IS NOT semver, though.)
// - Within a single seq, each successive schema is backwards compatible with
// the prior schema. (See, it's not semver. No special rules for v0.)
// - For each seq/major version after the first, there exists a migration
// that allows us to transform a resource compliant with the old version of
// the schema into one compliant with the new one.
//
// That's right, we've schematized our schema declarations. Not all above
// invariants are enforced right now, but they must be before launch.
//
// Grafana won't need to rely on multiple versions of schema until after this
// system is released with Grafana 8. But it needs to be in place at the moment
// Grafana 8 is released - especially for plugins, which have their own release
// cycle, and could need to make breaking changes very shortly after v8's release.
Family: {
lineages: [
[
{ // v0.0. The actual schema is the contents of this struct.
PanelOptions: {
frameIndex: number | *0
showHeader: bool | *true
sortBy?: [...ui.TableSortByFieldState]
}
PanelFieldConfig: {
width?: int
align?: *null | string
displayMode?: string | *"auto" // TODO? TableCellDisplayMode
filterable?: bool
}
},
{ // v0.1
lineages[0][0]
PanelOptions: foo: string | *"foo"
}
],
[
{ // v1.0 - breaking changes vs. v0.1 in this struct.
PanelOptions: {
frameIndex: number | *0
includeHeader: bool | *true
sortBy?: [...ui.TableSortByFieldState]
}
PanelFieldConfig: {
width?: int
align?: string
displayMode?: string
}
}
],
]
migrations: [
{ // maps from v0.1 to v1.0
// TODO it's not good that the user has to specify these. Should be
// implicit, since we don't want to allow any actual choice here.
// But NOT having it also means CUE can't actually tell if the
// _rel definition makes any sense at all. UGHHH. Would it be
// better to put these directly on the lineages?
from: lineages[0][1]
to: lineages[1][0]
rel: {
PanelOptions: {
frameIndex: from.PanelOptions.frameIndex
includeHeader: from.PanelOptions.showHeader
if from.PanelOptions.sortBy != _|_ {
sortBy: from.PanelOptions.sortBy | *null
}
}
PanelFieldConfig: from.PanelFieldConfig
}
result: rel & to
}
]
}

View File

@ -1,9 +0,0 @@
{
"type": "panel",
"name": "Sample plugin with lineage",
"id": "with-lineage",
"info": {
"description": "Show how complex history may work"
}
}

View File

@ -1,573 +0,0 @@
package schema
import (
"bytes"
"errors"
"fmt"
"math/bits"
"cuelang.org/go/cue"
errs "cuelang.org/go/cue/errors"
cuejson "cuelang.org/go/pkg/encoding/json"
)
// CueError wraps Errors caused by malformed cue files.
type CueError struct {
ErrorMap map[int]string
}
// Error func needed to implement standard golang error
func (cErr *CueError) Error() string {
var errorString string
if cErr.ErrorMap != nil {
for k, v := range cErr.ErrorMap {
errorString = errorString + fmt.Sprintf("line: %d, %s \n", k, v)
}
}
return errorString
}
// CueSchema represents a single, complete CUE-based schema that can perform
// operations on Resources.
//
// All CueSchema MUST EITHER:
// - Be a VersionedCueSchema, and be the latest version in the latest lineage in a Family
// - Return non-nil from Successor(), and a procedure to Migrate() a Resource to that successor schema
//
// By definition, VersionedCueSchema are within a lineage. As long as lineage
// backwards compatibility invariants hold, migration to a VersionedCueSchema to
// a successor schema in their lineage is trivial: simply unify the Resource
// with the successor schema.
type CueSchema interface {
// Validate checks that the resource is correct with respect to the schema.
Validate(Resource) error
// Migrate transforms a Resource into a new Resource that is correct with
// respect to its Successor schema. It returns the transformed resource,
// the schema to which the resource now conforms, and any errors that
// may have occurred during the migration.
//
// No migration occurs and the input Resource is returned in two cases:
//
// - The migration encountered an error; the third return is non-nil.
// - There exists no schema to migrate to; the second and third return are nil.
//
// Note that the returned schema is always a VersionedCueSchema. This
// reflects a key design invariant of the system: all migrations, whether
// they begin from a schema inside or outside of the Family, must land
// somewhere on a Family's sequence of schemata.
Migrate(Resource) (Resource, VersionedCueSchema, error)
// Successor returns the VersionedCueSchema to which this CueSchema can migrate a
// Resource.
Successor() VersionedCueSchema
// CUE returns the cue.Value representing the actual schema.
CUE() cue.Value
}
// VersionedCueSchema are CueSchema that are part of a backwards-compatible
// versioned lineage.
type VersionedCueSchema interface {
CueSchema
// Version reports the major and minor versions of the schema.
Version() (major, minor int)
}
// SearchAndValidate traverses the family of schemas reachable from the provided
// VersionedCueSchema. For each schema, it attempts to validate the provided
// value, which may be a byte slice representing valid JSON (TODO YAML), a Go
// struct, or cue.Value. If providing a cue.Value that is not fully concrete,
// the result is undefined.
//
// Traversal is performed from the newest schema to the oldest. However, because
// newer VersionedCueSchema have no way of directly accessing their predecessors
// (they form a singly-linked list), the oldest possible schema should always be
// provided - typically, the one returned from the family loader function.
//
// Failure to validate against any schema in the family is indicated by a
// non-nil error return. Success is indicated by a non-nil VersionedCueSchema.
// If successful, the returned VersionedCueSchema will be the first one against
// which the provided resource passed validation.
func SearchAndValidate(s VersionedCueSchema, v interface{}) (VersionedCueSchema, error) {
arr := AsArray(s)
// Work from latest to earliest
var err error
for o := len(arr) - 1; o >= 0; o-- {
for i := len(arr[o]) - 1; i >= 0; i-- {
if err = arr[o][i].Validate(Resource{Value: v}); err == nil {
return arr[o][i], nil
}
}
}
// TODO sloppy, return more than last error. Need our own error type that
// collates all the individual errors, relates them to the schema that
// produced them, and ideally deduplicates repeated errors across each
// schema.
cueErrors := WrapCUEError(err)
if err != nil {
return nil, cueErrors
}
return nil, err
}
// AsArray collates all VersionedCueSchema in a Family into a two-dimensional
// array. The outer array index corresponds to major version number and inner
// array index to minor version number.
func AsArray(sch VersionedCueSchema) [][]VersionedCueSchema {
var ret [][]VersionedCueSchema
var flat []VersionedCueSchema
// two loops. lazy day, today
for sch != nil {
flat = append(flat, sch)
sch = sch.Successor()
}
for _, sch := range flat {
maj, _ := sch.Version()
if len(ret) == maj {
ret = append(ret, []VersionedCueSchema{})
}
ret[maj] = append(ret[maj], sch)
}
return ret
}
// Find traverses the chain of VersionedCueSchema until the criteria in the
// SearchOption is met.
//
// If no schema is found that fulfills the criteria, nil is returned. Latest()
// and LatestInCurrentMajor() will always succeed, unless the input schema is
// nil.
func Find(s VersionedCueSchema, opt SearchOption) VersionedCueSchema {
if s == nil {
return nil
}
p := &ssopt{}
opt(p)
if err := p.validate(); err != nil {
panic(fmt.Sprint("unreachable:", err))
}
switch {
case p.latest:
for ; s.Successor() != nil; s = s.Successor() {
}
return s
case p.latestInCurrentMajor:
p.latestInMajor, _ = s.Version()
fallthrough
case p.hasLatestInMajor:
imaj, _ := s.Version()
if imaj > p.latestInMajor {
return nil
}
var last VersionedCueSchema
for imaj <= p.latestInMajor {
last, s = s, s.Successor()
if s == nil {
if imaj == p.latestInMajor {
return last
}
return nil
}
imaj, _ = s.Version()
}
return last
default: // exact
for s != nil {
maj, min := s.Version()
if p.exact == [2]int{maj, min} {
return s
}
s = s.Successor()
}
return nil
}
}
// SearchOption indicates how far along a chain of schemas an operation should
// proceed.
type SearchOption sso
type sso func(p *ssopt)
type ssopt struct {
latest bool
latestInMajor int
hasLatestInMajor bool
latestInCurrentMajor bool
exact [2]int
}
func (p *ssopt) validate() error {
var which uint16
if p.latest {
which = which + 1<<1
}
if p.exact != [2]int{0, 0} {
which = which + 1<<2
}
if p.hasLatestInMajor {
if p.latestInMajor != -1 {
which = which + 1<<3
}
} else if p.latestInMajor != 0 {
// Disambiguate real zero from default zero
return fmt.Errorf("latestInMajor should never be non-zero if hasLatestInMajor is false, got %v", p.latestInMajor)
}
if p.latestInCurrentMajor {
which = which + 1<<4
}
if bits.OnesCount16(which) != 1 {
return errors.New("may only pass one SchemaSearchOption")
}
return nil
}
// Latest indicates that traversal will continue to the newest schema in the
// newest lineage.
func Latest() SearchOption {
return func(p *ssopt) {
p.latest = true
}
}
// LatestInMajor will find the latest schema within the provided major version
// lineage. If no lineage exists corresponding to the provided number, traversal
// will terminate with an error.
func LatestInMajor(maj int) SearchOption {
return func(p *ssopt) {
p.latestInMajor = maj
}
}
// LatestInCurrentMajor will find the newest schema having the same major
// version as the schema from which the search begins.
func LatestInCurrentMajor() SearchOption {
return func(p *ssopt) {
p.latestInCurrentMajor = true
}
}
// Exact will find the schema with the exact major and minor version number
// provided.
func Exact(maj, min int) SearchOption {
return func(p *ssopt) {
p.exact = [2]int{maj, min}
}
}
// ApplyDefaults returns a new, concrete copy of the Resource with all paths
// that are 1) missing in the Resource AND 2) specified by the schema,
// filled with default values specified by the schema.
func ApplyDefaults(r Resource, scue cue.Value) (Resource, error) {
name := r.Name
if name == "" {
name = "resource"
}
rv := scue.Context().CompileString(r.Value.(string), cue.Filename(name))
if rv.Err() != nil {
return r, rv.Err()
}
rvUnified, err := applyDefaultHelper(rv, scue)
if err != nil {
return r, err
}
re, err := convertCUEValueToString(rvUnified)
if err != nil {
return r, err
}
return Resource{Value: re}, nil
}
func applyDefaultHelper(input cue.Value, scue cue.Value) (cue.Value, error) {
switch scue.IncompleteKind() {
case cue.ListKind:
// if list element exist
ele := scue.LookupPath(cue.MakePath(cue.AnyIndex))
// if input is not a concrete list, we must have list elements exist to be used to trim defaults
if ele.Exists() {
if ele.IncompleteKind() == cue.BottomKind {
return input, errors.New("can't get the element of list")
}
iter, err := input.List()
if err != nil {
return input, errors.New("can't apply defaults for list")
}
var iterlist []cue.Value
for iter.Next() {
ref, err := getBranch(ele, iter.Value())
if err != nil {
return input, err
}
re, err := applyDefaultHelper(iter.Value(), ref)
if err == nil {
iterlist = append(iterlist, re)
}
}
liInstance := scue.Context().NewList(iterlist...)
if liInstance.Err() != nil {
return input, liInstance.Err()
}
return liInstance, nil
} else {
return input.Unify(scue), nil
}
case cue.StructKind:
iter, err := scue.Fields(cue.Optional(true))
if err != nil {
return input, err
}
for iter.Next() {
lable, _ := iter.Value().Label()
lv := input.LookupPath(cue.MakePath(cue.Str(lable)))
if err != nil {
continue
}
if lv.Exists() {
res, err := applyDefaultHelper(lv, iter.Value())
if err != nil {
continue
}
input = input.FillPath(cue.MakePath(cue.Str(lable)), res)
} else if !iter.IsOptional() {
input = input.FillPath(cue.MakePath(cue.Str(lable)), iter.Value().Eval())
}
}
return input, nil
default:
input = input.Unify(scue)
}
return input, nil
}
func convertCUEValueToString(inputCUE cue.Value) (string, error) {
re, err := cuejson.Marshal(inputCUE)
if err != nil {
return re, err
}
result := []byte(re)
result = bytes.Replace(result, []byte("\\u003c"), []byte("<"), -1)
result = bytes.Replace(result, []byte("\\u003e"), []byte(">"), -1)
result = bytes.Replace(result, []byte("\\u0026"), []byte("&"), -1)
return string(result), nil
}
// TrimDefaults returns a new, concrete copy of the Resource where all paths
// in the where the values at those paths are the same as the default value
// given in the schema.
func TrimDefaults(r Resource, scue cue.Value) (Resource, error) {
name := r.Name
if name == "" {
name = "resource"
}
rvInstance := scue.Context().CompileString(r.Value.(string), cue.Filename(name))
if rvInstance.Err() != nil {
return r, rvInstance.Err()
}
rv, _, err := removeDefaultHelper(scue, rvInstance)
if err != nil {
return r, err
}
re, err := convertCUEValueToString(rv)
if err != nil {
return r, err
}
return Resource{Value: re}, nil
}
func getDefault(icue cue.Value) (cue.Value, bool) {
d, exist := icue.Default()
if exist && d.Kind() == cue.ListKind {
len, err := d.Len().Int64()
if err != nil {
return d, false
}
var defaultExist bool
if len <= 0 {
op, vals := icue.Expr()
if op == cue.OrOp {
for _, val := range vals {
vallen, _ := val.Len().Int64()
if val.Kind() == cue.ListKind && vallen <= 0 {
defaultExist = true
break
}
}
if !defaultExist {
exist = false
}
} else {
exist = false
}
}
}
return d, exist
}
func isCueValueEqual(inputdef cue.Value, input cue.Value) bool {
d, exist := getDefault(inputdef)
if exist {
return input.Subsume(d) == nil && d.Subsume(input) == nil
}
return false
}
func removeDefaultHelper(inputdef cue.Value, input cue.Value) (cue.Value, bool, error) {
// To include all optional fields, we need to use inputdef for iteration,
// since the lookuppath with optional field doesn't work very well
rv := inputdef.Context().CompileString("", cue.Filename("helper"))
if rv.Err() != nil {
return input, false, rv.Err()
}
switch inputdef.IncompleteKind() {
case cue.StructKind:
// Get all fields including optional fields
iter, err := inputdef.Fields(cue.Optional(true))
if err != nil {
return rv, false, err
}
keySet := make(map[string]bool)
for iter.Next() {
lable, _ := iter.Value().Label()
keySet[lable] = true
lv := input.LookupPath(cue.MakePath(cue.Str(lable)))
if err != nil {
return rv, false, err
}
if lv.Exists() {
re, isEqual, err := removeDefaultHelper(iter.Value(), lv)
if err == nil && !isEqual {
rv = rv.FillPath(cue.MakePath(cue.Str(lable)), re)
}
}
}
// Get all the fields that are not defined in schema yet for panel
iter, err = input.Fields()
if err != nil {
return rv, false, err
}
for iter.Next() {
lable, _ := iter.Value().Label()
if exists := keySet[lable]; !exists {
rv = rv.FillPath(cue.MakePath(cue.Str(lable)), iter.Value())
}
}
return rv, false, nil
case cue.ListKind:
if isCueValueEqual(inputdef, input) {
return rv, true, nil
}
// take every element of the list
ele := inputdef.LookupPath(cue.MakePath(cue.AnyIndex))
// if input is not a concrete list, we must have list elements exist to be used to trim defaults
if ele.Exists() {
if ele.IncompleteKind() == cue.BottomKind {
return rv, true, nil
}
iter, err := input.List()
if err != nil {
return rv, true, nil
}
var iterlist []cue.Value
for iter.Next() {
ref, err := getBranch(ele, iter.Value())
if err != nil {
iterlist = append(iterlist, iter.Value())
continue
}
re, isEqual, err := removeDefaultHelper(ref, iter.Value())
if err == nil && !isEqual {
iterlist = append(iterlist, re)
} else {
iterlist = append(iterlist, iter.Value())
}
}
liInstance := inputdef.Context().NewList(iterlist...)
return liInstance, false, liInstance.Err()
}
// now when ele is empty, we don't trim anything
return input, false, nil
default:
if isCueValueEqual(inputdef, input) {
return input, true, nil
}
return input, false, nil
}
}
func getBranch(schemaObj cue.Value, concretObj cue.Value) (cue.Value, error) {
op, defs := schemaObj.Expr()
if op == cue.OrOp {
for _, def := range defs {
err := def.Unify(concretObj).Validate(cue.Concrete(true))
if err == nil {
return def, nil
}
}
// no matching branches? wtf
return schemaObj, errors.New("no branch is found for list")
}
return schemaObj, nil
}
// A Resource represents a concrete data object - e.g., JSON
// representing a dashboard.
//
// This type mostly exists to improve readability for users. Having a type that
// differentiates cue.Value that represent a schema from cue.Value that
// represent a concrete object is quite helpful. It also gives us a working type
// for a resource that can be reused across multiple calls, so that re-parsing
// isn't necessary.
//
// TODO this is a terrible way to do this, refactor
type Resource struct {
Value interface{}
Name string
}
// WrapCUEError is a wrapper for cueErrors that occur and are not self explanatory.
// If an error is of type cueErr, then iterate through the error array, export line number
// and filename, otherwise return usual error.
func WrapCUEError(err error) error {
var cErr errs.Error
m := make(map[int]string)
if ok := errors.As(err, &cErr); ok {
for _, e := range errs.Errors(cErr) {
if e.Position().File() != nil {
line := e.Position().Line()
m[line] = fmt.Sprintf("%q: in file %s", err, e.Position().File().Name())
}
}
}
if len(m) != 0 {
return &CueError{m}
}
return err
}
// TODO add migrator with SearchOption for stopping criteria

View File

@ -1,129 +0,0 @@
package schema
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"testing"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"github.com/google/go-cmp/cmp"
"golang.org/x/tools/txtar"
)
var CasesDir = filepath.Join("testdata", "trimapplydefaults")
type Case struct {
Name string
CUE string
Full string
Trimmed string
}
func TestGenerate(t *testing.T) {
cases, err := loadCases(CasesDir)
if err != nil {
t.Fatal(err)
}
for _, c := range cases {
t.Run(c.Name+" apply defaults", func(t *testing.T) {
ctx := cuecontext.New()
scmInstance := ctx.CompileString(c.CUE, cue.Filename(c.Name+".cue"))
if scmInstance.Err() != nil {
t.Fatal(scmInstance.Err())
}
inputResource := Resource{Value: c.Trimmed}
out, err := ApplyDefaults(inputResource, scmInstance)
if err != nil {
t.Fatal(err)
}
b := []byte(out.Value.(string))
b, _ = JsonRemarshal(b)
if s := cmp.Diff(c.Full, string(b)); s != "" {
t.Fatal(s)
}
})
}
for _, c := range cases {
t.Run(c.Name+" trim defaults", func(t *testing.T) {
ctx := cuecontext.New()
scmInstance := ctx.CompileString(c.CUE, cue.Filename(c.Name+".cue"))
if scmInstance.Err() != nil {
t.Fatal(scmInstance.Err())
}
inputResource := Resource{Value: c.Full}
out, err := TrimDefaults(inputResource, scmInstance)
if err != nil {
t.Fatal(err)
}
b := []byte(out.Value.(string))
b, _ = JsonRemarshal(b)
if s := cmp.Diff(c.Trimmed, string(b)); s != "" {
t.Fatal(s)
}
})
}
}
func JsonRemarshal(bytes []byte) ([]byte, error) {
var ifce interface{}
err := json.Unmarshal(bytes, &ifce)
if err != nil {
return []byte{}, err
}
output, err := json.Marshal(ifce)
outputstring := string(output)
if err != nil {
return []byte{}, err
}
outputstring = strings.Replace(outputstring, "\\u003c", "<", -1)
outputstring = strings.Replace(outputstring, "\\u003e", ">", -1)
outputstring = strings.Replace(outputstring, "\\u0026", "&", -1)
return []byte(outputstring), nil
}
func loadCases(dir string) ([]Case, error) {
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
var cases []Case
for _, fi := range files {
file := filepath.Join(dir, fi.Name())
a, err := txtar.ParseFile(file)
if err != nil {
return nil, err
}
if len(a.Files) != 3 {
return nil, fmt.Errorf("Malformed test case '%s': Must contain exactly three files (CUE, Full and Trimed), but has %d", file, len(a.Files))
}
fullBuffer := new(bytes.Buffer)
fullJson := a.Files[1].Data
if err := json.Compact(fullBuffer, fullJson); err != nil {
return nil, err
}
trimBuffer := new(bytes.Buffer)
trimedJson := a.Files[2].Data
if err := json.Compact(trimBuffer, trimedJson); err != nil {
return nil, err
}
cases = append(cases, Case{
Name: strings.TrimSuffix(fi.Name(), filepath.Ext(fi.Name())),
CUE: string(a.Files[0].Data),
Full: fullBuffer.String(),
Trimmed: trimBuffer.String(),
})
}
return cases, nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,631 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"showIn": 0,
"type": "dashboard"
}
]
},
"editable": true,
"graphTooltip": 0,
"links": [],
"panels": [
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 0
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 10,
"w": 12,
"x": 0,
"y": 0
},
"id": 9,
"options": {
"barWidth": 1,
"groupWidth": 0.82,
"legend": {
"calcs": [
"max"
],
"displayMode": "list",
"placement": "right",
"asTable": false,
"isVisible": false
},
"orientation": "auto",
"showValue": "auto",
"stacking": "none",
"text": {},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "Time,Name,Stat1,Stat2\n2020-01-01T00:00:00Z,Stockholm, 10, 15\n2020-01-01T00:00:00Z,New York, 19, 5\n2020-01-01T00:00:00Z,London, 10, 1\n2020-01-01T00:00:00Z,Negative, 15, -5\n2020-01-01T00:00:00Z,Long value, 15,10",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Auto sizing & auto show values",
"type": "barchart",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"description": "Should be smaller given the longer value",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 0
},
"decimals": 2,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 10,
"w": 12,
"x": 12,
"y": 0
},
"id": 15,
"options": {
"barWidth": 1,
"groupWidth": 0.82,
"legend": {
"calcs": [
"max"
],
"displayMode": "list",
"placement": "right",
"asTable": false,
"isVisible": false
},
"orientation": "auto",
"showValue": "auto",
"stacking": "none",
"text": {},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, 5\nLondon, 10, 1\nNegative, 15, -5\nLong value, 15,10",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Auto sizing & auto show values",
"type": "barchart",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 0
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 8,
"x": 0,
"y": 10
},
"id": 16,
"options": {
"barWidth": 1,
"groupWidth": 0.89,
"legend": {
"calcs": [
"max"
],
"displayMode": "list",
"placement": "right",
"asTable": false,
"isVisible": false
},
"orientation": "auto",
"showValue": "auto",
"stacking": "none",
"text": {},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "auto show values & No room for value",
"type": "barchart",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 0
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 8,
"x": 8,
"y": 10
},
"id": 17,
"options": {
"barWidth": 1,
"groupWidth": 0.89,
"legend": {
"calcs": [
"max"
],
"displayMode": "list",
"placement": "right",
"asTable": false,
"isVisible": false
},
"orientation": "auto",
"showValue": "always",
"stacking": "none",
"text": {},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "auto show values & Always show value",
"type": "barchart",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 0
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 8,
"x": 16,
"y": 10
},
"id": 10,
"options": {
"barWidth": 1,
"groupWidth": 1,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"orientation": "auto",
"showValue": "auto",
"stacking": "none",
"text": {
"titleSize": 10,
"valueSize": 25
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"panelId": 9,
"refId": "A"
}
],
"title": "Fixed value sizing",
"type": "barchart",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 0
},
"decimals": 7,
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 12,
"x": 0,
"y": 21
},
"id": 18,
"options": {
"barWidth": 1,
"groupWidth": 0.82,
"legend": {
"calcs": [
"max"
],
"displayMode": "list",
"placement": "right",
"asTable": false,
"isVisible": false
},
"orientation": "horizontal",
"showValue": "auto",
"stacking": "none",
"text": {},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "Name,Stat1,Stat2\nStockholm, 10, 15\nNew York, 19, -5\nLondon, 10, 1\nLong value, 15,10",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Auto sizing & auto show values",
"type": "barchart",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 0
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 12,
"x": 12,
"y": 21
},
"id": 19,
"options": {
"barWidth": 1,
"groupWidth": 0.89,
"legend": {
"calcs": [
"max"
],
"displayMode": "list",
"placement": "right",
"asTable": false,
"isVisible": false
},
"orientation": "horizontal",
"showValue": "auto",
"stacking": "none",
"text": {},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"csvContent": "Name,Stat1,Stat2,Stat3,Stat4,Stat5,Stat6,Stat7,Stat8,Stat9,Stat10\nA, 10, 15,8,3,4,12,14,1,5,10\nB, 19, 5,8,3,4,12,14,6,7,2\nC, 15, 5,8,3,4,10,4,6,7,2\n",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "auto show values & little room",
"type": "barchart",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
}
],
"refresh": "",
"schemaVersion": 33,
"style": "dark",
"tags": [
"gdev",
"panel-tests",
"barchart"
],
"templating": {
"list": []
},
"time": {
"from": "now-5m",
"to": "now"
},
"timepicker": {
"collapse": false,
"enable": true,
"hidden": false,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "BarChart - Panel Tests - Value sizing",
"uid": "WFlOM-jM1",
"version": 9
}

View File

@ -1,619 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"showIn": 0,
"type": "dashboard"
}
]
},
"description": "",
"editable": true,
"graphTooltip": 0,
"links": [],
"panels": [
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"thresholdsStyle": {
"mode": "line"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"hide": false,
"max": 100,
"min": 1,
"refId": "A",
"scenarioId": "random_walk",
"startValue": 50
},
{
"alias": "",
"csvContent": "min,max,threshold1\n1000,1000,8000\n0,100,80\n\n",
"refId": "config",
"scenarioId": "csv_content"
}
],
"title": "Min, max, threshold from separate query",
"transformations": [
{
"id": "configFromData",
"options": {
"configRefId": "config",
"mappings": []
}
}
],
"type": "timeseries",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "left",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "SensorA"
},
"properties": [
{
"id": "custom.displayMode",
"value": "color-text"
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 0
},
"id": 5,
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"csvContent": "Name, Value, SensorA, MyUnit, MyColor\nGoogle, 10, 50, km/h, blue\nGoogle, 100, 100,km/h, orange\n",
"hide": false,
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Custom mappings and apply to self",
"transformations": [
{
"id": "configFromData",
"options": {
"applyTo": {
"id": "byName",
"options": "SensorA"
},
"applyToConfig": true,
"configRefId": "A",
"mappings": [
{
"configProperty": "unit",
"fieldName": "MyUnit",
"handlerKey": "unit"
},
{
"fieldName": "MyColor",
"handlerKey": "color"
}
]
}
}
],
"type": "table",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "center",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Value"
},
"properties": [
{
"id": "custom.displayMode",
"value": "color-background-solid"
}
]
}
]
},
"gridPos": {
"h": 5,
"w": 12,
"x": 0,
"y": 9
},
"id": 7,
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"csvContent": "ID, DisplayText\n21412312312, Homer\n12421412413, Simpsons \n12321312313, Bart",
"hide": false,
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Mapping data",
"transformations": [
{
"id": "configFromData",
"options": {
"applyToConfig": true,
"configRefId": "A",
"mappings": [
{
"fieldName": "Color",
"handlerKey": "mappings.color"
},
{
"fieldName": "Value",
"handlerKey": "mappings.value"
}
]
}
}
],
"type": "table",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "center",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Value"
},
"properties": [
{
"id": "custom.displayMode",
"value": "color-background-solid"
}
]
}
]
},
"gridPos": {
"h": 10,
"w": 12,
"x": 12,
"y": 9
},
"id": 6,
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"csvContent": "Value, Color\nOK, blue\nPretty bad, red\nYay it's green, green\nSomething is off, orange\nNo idea, #88AA00\nAm I purple?, purple",
"hide": false,
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Value mappings from query result applied to itself",
"transformations": [
{
"id": "configFromData",
"options": {
"applyTo": {
"id": "byName",
"options": "Value"
},
"applyToConfig": true,
"configRefId": "A",
"mappings": [
{
"fieldName": "Color",
"handlerKey": "mappings.color"
},
{
"fieldName": "Value",
"handlerKey": "mappings.value"
}
]
}
}
],
"type": "table",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "center",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 0,
"y": 14
},
"id": 8,
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"csvContent": "ID, Value\n21412312312, 100\n12421412413, 20\n12321312313, 10",
"hide": false,
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Display data",
"transformations": [
{
"id": "configFromData",
"options": {
"applyToConfig": true,
"configRefId": "A",
"mappings": [
{
"fieldName": "Color",
"handlerKey": "mappings.color"
},
{
"fieldName": "Value",
"handlerKey": "mappings.value"
}
]
}
}
],
"type": "table",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"axisSoftMin": 0,
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineWidth": 1
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 10,
"w": 12,
"x": 0,
"y": 19
},
"id": 9,
"options": {
"barWidth": 0.97,
"groupWidth": 0.7,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"orientation": "horizontal",
"stacking": "none",
"showValue": "auto",
"text": {},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"csvContent": "ID, Value\nA21412312312, 100\nA12421412413, 20\nA12321312313, 10\n",
"hide": false,
"refId": "data",
"scenarioId": "csv_content"
},
{
"csvContent": "ID, DisplayText\nA21412312312, Homer\nA12421412413, Marge \nA12321312313, Bart",
"hide": false,
"refId": "mappings",
"scenarioId": "csv_content"
}
],
"title": "Value mapping ID -> DisplayText from separate query",
"transformations": [
{
"id": "configFromData",
"options": {
"applyTo": {
"id": "byName",
"options": "ID"
},
"applyToConfig": false,
"configRefId": "mappings",
"mappings": [
{
"fieldName": "ID",
"handlerKey": "mappings.value"
},
{
"fieldName": "DisplayText",
"handlerKey": "mappings.text"
}
]
}
}
],
"type": "barchart",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
}
],
"refresh": "",
"schemaVersion": 33,
"style": "dark",
"tags": [
"gdev",
"transform"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"collapse": false,
"enable": true,
"hidden": false,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Transforms - Config from query",
"uid": "Juj4_7ink",
"version": 1
}

File diff suppressed because it is too large Load Diff

View File

@ -1,965 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"showIn": 0,
"type": "dashboard"
}
]
},
"editable": true,
"graphTooltip": 0,
"links": [],
"panels": [
{
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 37,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 3,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "orange",
"value": 15
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 8,
"x": 0,
"y": 0
},
"id": 11,
"maxDataPoints": 45,
"options": {
"legend": {
"calcs": [],
"displayMode": "hidden",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,10,20,30,40,50"
}
],
"title": "15 orange, 30 red",
"type": "timeseries",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 37,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 3,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 20,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "orange",
"value": 15
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 8,
"x": 8,
"y": 0
},
"id": 12,
"maxDataPoints": 45,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,10,20,30,40,50"
}
],
"title": "15 orange, 30 red",
"type": "timeseries",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 37,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 20,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "orange",
"value": 15
},
{
"color": "red",
"value": 50
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 8,
"x": 16,
"y": 0
},
"id": 13,
"maxDataPoints": 45,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,10,20,30,40,50"
}
],
"title": "15 orange, 50 red",
"type": "timeseries",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 5,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 3,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "orange",
"value": 20
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 7
},
"id": 9,
"maxDataPoints": 45,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"panelId": 4,
"refId": "A"
}
],
"title": "Color line by discrete tresholds",
"type": "timeseries",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "bars",
"fillOpacity": 84,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 0,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "orange",
"value": 20
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 7
},
"id": 4,
"interval": "80s",
"maxDataPoints": 42,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"max": 40,
"min": 0,
"noise": 1,
"refId": "A",
"scenarioId": "random_walk",
"spread": 20,
"startValue": 1
}
],
"title": "Color bars by discrete thresholds",
"type": "timeseries",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 3,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "blue"
},
{
"color": "green",
"value": 0
},
{
"color": "orange",
"value": 20
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 14
},
"id": 6,
"maxDataPoints": 50,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"panelId": 4,
"refId": "A"
}
],
"title": "Color line by color scale",
"type": "timeseries",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "bars",
"fillOpacity": 64,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "blue"
},
{
"color": "green",
"value": 0
},
{
"color": "orange",
"value": 20
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 14
},
"id": 10,
"maxDataPoints": 45,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"panelId": 4,
"refId": "A"
}
],
"title": "Color bars by color scale",
"type": "timeseries",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 64,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "blue"
},
{
"color": "green",
"value": 0
},
{
"color": "orange",
"value": 20
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 21
},
"id": 7,
"maxDataPoints": 50,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"panelId": 4,
"refId": "A"
}
],
"title": "Color line by color scale",
"type": "timeseries",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "points",
"fillOpacity": 10,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 3,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "blue"
},
{
"color": "green",
"value": 0
},
{
"color": "orange",
"value": 20
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 21
},
"id": 8,
"maxDataPoints": 250,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"max": 45,
"min": 20,
"noise": 0,
"refId": "A",
"scenarioId": "random_walk",
"spread": 12,
"startValue": 40
},
{
"hide": false,
"max": 20,
"min": 1,
"noise": 0,
"refId": "B",
"scenarioId": "random_walk",
"spread": 10
}
],
"title": "Color line by color scale",
"type": "timeseries",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
}
],
"refresh": false,
"schemaVersion": 33,
"style": "dark",
"tags": [
"gdev",
"panel-tests"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"collapse": false,
"enable": true,
"hidden": false,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Panel Tests - Graph NG - By value color schemes",
"uid": "aBXrJ0R7z",
"version": 11
}

File diff suppressed because it is too large Load Diff

View File

@ -1,500 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"showIn": 0,
"type": "dashboard"
}
]
},
"editable": true,
"graphTooltip": 0,
"id": 632,
"links": [],
"panels": [
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"viz": false,
"legend": false,
"tooltip": false
},
"lineWidth": 1
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"id": 4,
"options": {
"bucketOffset": 0,
"combine": false,
"legend": {
"calcs": [],
"displayMode": "hidden",
"placement": "bottom",
"asTable": false,
"isVisible": false
}
},
"targets": [
{
"refId": "A",
"scenarioId": "random_walk",
"spread": 10
}
],
"title": "Time series + Auto buckets",
"type": "histogram",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"viz": false,
"legend": false,
"tooltip": false
},
"lineWidth": 1
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 0
},
"id": 3,
"options": {
"bucketOffset": 0,
"bucketSize": 3,
"combine": false,
"legend": {
"calcs": [],
"displayMode": "hidden",
"placement": "bottom",
"asTable": false,
"isVisible": false
}
},
"targets": [
{
"panelId": 4,
"refId": "A"
}
],
"title": "Time series + bucket size 3",
"type": "histogram",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"viz": false,
"legend": false,
"tooltip": false
},
"lineWidth": 1
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 8
},
"id": 5,
"options": {
"bucketOffset": 0,
"bucketSize": 1,
"combine": false,
"legend": {
"calcs": [],
"displayMode": "hidden",
"placement": "bottom",
"asTable": false,
"isVisible": false
}
},
"targets": [
{
"csvFileName": "weight_height.csv",
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "People height distribution",
"transformations": [
{
"id": "filterFieldsByName",
"options": {
"include": {
"names": [
"Height"
]
}
}
}
],
"type": "histogram",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 80,
"gradientMode": "none",
"hideFrom": {
"viz": false,
"legend": false,
"tooltip": false
},
"lineWidth": 1
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 8
},
"id": 6,
"options": {
"bucketOffset": 0,
"bucketSize": 5,
"combine": false,
"legend": {
"calcs": [],
"displayMode": "hidden",
"placement": "bottom",
"asTable": false,
"isVisible": false
}
},
"targets": [
{
"csvFileName": "weight_height.csv",
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "People weight distribution",
"transformations": [
{
"id": "filterFieldsByName",
"options": {
"include": {
"names": [
"Weight"
]
}
}
}
],
"type": "histogram",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 16
},
"id": 8,
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"csvFileName": "weight_height.csv",
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "Standalone transform - Height",
"transformations": [
{
"id": "filterFieldsByName",
"options": {
"include": {
"names": [
"Height"
]
}
}
},
{
"id": "histogram",
"options": {
"combine": true,
"fields": {}
}
}
],
"type": "table",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 16
},
"id": 9,
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"csvFileName": "weight_height.csv",
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "Standalone transform - Weight",
"transformations": [
{
"id": "filterFieldsByName",
"options": {
"include": {
"names": [
"Weight"
]
}
}
},
{
"id": "histogram",
"options": {
"combine": true,
"fields": {}
}
}
],
"type": "table",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
}
],
"schemaVersion": 33,
"style": "dark",
"tags": [
"gdev",
"panel-tests"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"collapse": false,
"enable": true,
"hidden": false,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Panel Tests - Histogram",
"uid": "UTv--wqMk",
"version": 4
}

View File

@ -1,313 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"showIn": 0,
"type": "dashboard"
}
]
},
"editable": true,
"graphTooltip": 0,
"links": [],
"panels": [
{
"gridPos": {
"h": 26,
"w": 6,
"x": 0,
"y": 0
},
"id": 7,
"links": [],
"options": {
"maxItems": 100,
"query": "",
"showHeadings": true,
"showRecentlyViewed": true,
"showSearch": false,
"showStarred": true,
"tags": []
},
"pluginVersion": "8.1.0-pre",
"tags": [],
"title": "Starred",
"type": "dashlist",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": [],
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
}
},
{
"gridPos": {
"h": 13,
"w": 6,
"x": 6,
"y": 0
},
"id": 2,
"links": [],
"options": {
"maxItems": 1000,
"query": "",
"showHeadings": false,
"showRecentlyViewed": false,
"showSearch": true,
"showStarred": false,
"tags": [
"panel-tests"
]
},
"pluginVersion": "8.1.0-pre",
"tags": [
"panel-tests"
],
"title": "tag: panel-tests",
"type": "dashlist",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": [],
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
}
},
{
"gridPos": {
"h": 13,
"w": 6,
"x": 12,
"y": 0
},
"id": 3,
"links": [],
"options": {
"maxItems": 1000,
"query": "",
"showHeadings": false,
"showRecentlyViewed": false,
"showSearch": true,
"showStarred": false,
"tags": [
"gdev",
"demo"
]
},
"pluginVersion": "8.1.0-pre",
"tags": [
"gdev",
"demo"
],
"title": "tag: dashboard-demo",
"type": "dashlist",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": [],
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
}
},
{
"gridPos": {
"h": 26,
"w": 6,
"x": 18,
"y": 0
},
"id": 5,
"links": [],
"options": {
"maxItems": 1000,
"query": "",
"showHeadings": false,
"showRecentlyViewed": false,
"showSearch": true,
"showStarred": false,
"tags": [
"gdev",
"datasource-test"
]
},
"pluginVersion": "8.1.0-pre",
"tags": [
"gdev",
"datasource-test"
],
"title": "Data source tests",
"type": "dashlist",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": [],
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
}
},
{
"gridPos": {
"h": 13,
"w": 6,
"x": 6,
"y": 13
},
"id": 4,
"links": [],
"options": {
"maxItems": 1000,
"query": "",
"showHeadings": false,
"showRecentlyViewed": false,
"showSearch": true,
"showStarred": false,
"tags": [
"templating",
"gdev"
]
},
"pluginVersion": "8.1.0-pre",
"tags": [
"templating",
"gdev"
],
"title": "tag: templating ",
"type": "dashlist",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": [],
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
}
},
{
"gridPos": {
"h": 13,
"w": 6,
"x": 12,
"y": 13
},
"id": 8,
"links": [],
"options": {
"maxItems": 1000,
"query": "",
"showHeadings": false,
"showRecentlyViewed": false,
"showSearch": true,
"showStarred": false,
"tags": [
"gdev",
"transform"
]
},
"pluginVersion": "8.1.0-pre",
"tags": [
"gdev",
"demo"
],
"title": "tag: transforms",
"type": "dashlist",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": [],
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
}
}
],
"schemaVersion": 33,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"collapse": false,
"enable": true,
"hidden": false,
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"timezone": "",
"title": "Grafana Dev Overview & Home",
"uid": "j6T00KRZz",
"version": 2
}

File diff suppressed because it is too large Load Diff

View File

@ -1,252 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"showIn": 0,
"type": "dashboard"
}
]
},
"editable": true,
"graphTooltip": 0,
"id": 3151,
"links": [],
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "gdev-opentsdb-v2.3",
"fieldConfig": {
"defaults": {
"links": []
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"hiddenSeries": false,
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.1.0-pre",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"aggregator": "sum",
"alias": "$tag_hostname",
"currentFilterGroupBy": false,
"currentFilterKey": "",
"currentFilterType": "literal_or",
"currentFilterValue": "",
"disableDownsampling": false,
"downsampleAggregator": "avg",
"downsampleFillPolicy": "none",
"explicitTags": false,
"filters": [
{
"filter": "*",
"groupBy": true,
"tagk": "hostname",
"type": "wildcard"
}
],
"metric": "cpu",
"refId": "A",
"shouldComputeRate": false
}
],
"thresholds": [],
"timeRegions": [],
"title": "CPU per host",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"logBase": 1,
"show": true
},
{
"format": "short",
"logBase": 1,
"show": true
}
],
"yaxis": {
"align": false
}
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "gdev-opentsdb-v2.3",
"fieldConfig": {
"defaults": {
"links": []
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 0
},
"hiddenSeries": false,
"id": 4,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "8.1.0-pre",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"aggregator": "sum",
"alias": "$tag_hostname",
"currentFilterGroupBy": false,
"currentFilterKey": "",
"currentFilterType": "literal_or",
"currentFilterValue": "",
"downsampleAggregator": "avg",
"downsampleFillPolicy": "none",
"filters": [
{
"filter": "*",
"groupBy": true,
"tagk": "hostname",
"type": "wildcard"
}
],
"metric": "logins.count",
"refId": "A"
}
],
"thresholds": [],
"timeRegions": [],
"title": "Login Count per host",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"mode": "time",
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"logBase": 1,
"show": true
},
{
"format": "short",
"logBase": 1,
"show": true
}
],
"yaxis": {
"align": false
}
}
],
"schemaVersion": 33,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"collapse": false,
"enable": true,
"hidden": false,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Datasource tests - OpenTSDB v2.3",
"uid": "rZRUGik7k",
"version": 3
}

View File

@ -1,414 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"showIn": 0,
"type": "dashboard"
}
]
},
"editable": true,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"custom": {}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 9,
"x": 0,
"y": 0
},
"id": 62,
"options": {
"basemap": {
"config": {},
"type": "default"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"color": {
"field": "Price",
"fixed": "dark-green"
},
"fillOpacity": 0.4,
"shape": "circle",
"showLegend": true,
"size": {
"field": "Count",
"fixed": 5,
"max": 15,
"min": 2
}
},
"location": {
"gazetteer": "public/gazetteer/usa-states.json",
"lookup": "State",
"mode": "auto"
},
"type": "markers"
}
],
"view": {
"id": "coords",
"lat": 38.297683,
"lon": -99.228359,
"shared": true,
"zoom": 3.98
}
},
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "Size, color mapped to different fields + share view",
"type": "geomap",
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
},
{
"color": "#EAB839",
"value": 90
}
]
},
"custom": {}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 9,
"x": 9,
"y": 0
},
"id": 66,
"options": {
"basemap": {
"config": {},
"type": "default"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"color": {
"field": "Price",
"fixed": "dark-green"
},
"fillOpacity": 0.4,
"shape": "circle",
"showLegend": true,
"size": {
"field": "Count",
"fixed": 5,
"max": 15,
"min": 2
}
},
"location": {
"gazetteer": "public/gazetteer/usa-states.json",
"lookup": "State",
"mode": "auto"
},
"type": "markers"
}
],
"view": {
"id": "coords",
"lat": 38.297683,
"lon": -99.228359,
"shared": true,
"zoom": 3.98
}
},
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "Thresholds legend",
"type": "geomap",
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-BlYlRd"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"custom": {}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 9,
"x": 0,
"y": 11
},
"id": 63,
"options": {
"basemap": {
"config": {},
"type": "default"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"blur": 27,
"radius": 25,
"weight": {
"field": "Count",
"fixed": 1,
"max": 1,
"min": 0
}
},
"location": {
"gazetteer": "public/gazetteer/usa-states.json",
"lookup": "State",
"mode": "auto"
},
"type": "heatmap"
}
],
"view": {
"id": "coords",
"lat": 38.251497,
"lon": -100.932144,
"shared": false,
"zoom": 4.15
}
},
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "Heatmap data layer",
"transformations": [],
"type": "geomap",
"transparent": false,
"repeatDirection": "h"
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"custom": {}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 9,
"x": 9,
"y": 11
},
"id": 65,
"options": {
"basemap": {
"config": {
"server": "world-imagery"
},
"type": "esri-xyz"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"color": {
"fixed": "#ff001e"
},
"fillOpacity": 0.4,
"shape": "star",
"showLegend": true,
"size": {
"field": "Count",
"fixed": 5,
"max": 15,
"min": 2
}
},
"location": {
"gazetteer": "public/gazetteer/usa-states.json",
"lookup": "State",
"mode": "auto"
},
"type": "markers"
}
],
"view": {
"id": "coords",
"lat": 40.159084,
"lon": -96.508021,
"shared": true,
"zoom": 3.83
}
},
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "Base layer ArcGIS wold imagery + star shape + share view",
"type": "geomap",
"transparent": false,
"repeatDirection": "h",
"transformations": []
}
],
"refresh": "",
"schemaVersion": 33,
"style": "dark",
"tags": [
"gdev",
"panel-tests"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"collapse": false,
"enable": true,
"hidden": false
},
"timezone": "",
"title": "Panel Tests - Geomap",
"uid": "2xuwrgV7z",
"version": 5
}

View File

@ -1,700 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"showIn": 0,
"type": "dashboard"
}
]
},
"editable": true,
"graphTooltip": 0,
"links": [],
"panels": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "left",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 0,
"y": 0
},
"id": 8,
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"panelId": 2,
"refId": "A"
}
],
"title": "Raw data",
"type": "table",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "left",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Value"
},
"properties": [
{
"id": "custom.width",
"value": 82
}
]
},
{
"matcher": {
"id": "byName",
"options": "Unit"
},
"properties": [
{
"id": "custom.width",
"value": 108
}
]
}
]
},
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 0
},
"id": 7,
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false,
"sortBy": []
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"panelId": 3,
"refId": "A"
}
],
"title": "Raw data",
"type": "table",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"custom": {}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 5
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"csvContent": "Name,Value,Unit,Color\nTemperature,10,degree,green\nPressure,100,bar,blue\nSpeed,30,km/h,red",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Unit and color from data",
"transformations": [
{
"id": "rowsToFields",
"options": {}
}
],
"type": "stat",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"custom": {}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 5
},
"id": 3,
"options": {
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showThresholdLabels": true,
"showThresholdMarkers": true,
"text": {}
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"csvContent": "Name,Value,Unit,min,max, threshold1\nTemperature,10,degree,0,50,30\nPressure,100,Pa,0,300,200\nSpeed,30,km/h,0,150,110",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Min, Max & Thresholds from data",
"transformations": [
{
"id": "rowsToFields",
"options": {}
}
],
"type": "gauge",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "left",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 12,
"x": 0,
"y": 12
},
"id": 10,
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"panelId": 9,
"refId": "A"
}
],
"title": "Raw data",
"type": "table",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "left",
"displayMode": "auto"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Value"
},
"properties": [
{
"id": "custom.width",
"value": 82
}
]
},
{
"matcher": {
"id": "byName",
"options": "Unit"
},
"properties": [
{
"id": "custom.width",
"value": 108
}
]
}
]
},
"gridPos": {
"h": 5,
"w": 12,
"x": 12,
"y": 12
},
"id": 12,
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false,
"sortBy": []
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"panelId": 11,
"refId": "A"
}
],
"title": "Raw data (Custom mapping)",
"type": "table",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"custom": {}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 17
},
"id": 9,
"options": {
"displayMode": "gradient",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showUnfilled": true,
"text": {}
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"csvContent": "Name,Value,Unit,Min,Max\nTemperature,20,degree,0,50\nPressure,150,Pa,0,300\nSpeed,100,km/h,0,110",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Min max from data",
"transformations": [
{
"id": "rowsToFields",
"options": {}
}
],
"type": "bargauge",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
}
]
},
"custom": {}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 17
},
"id": 11,
"options": {
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showThresholdLabels": true,
"showThresholdMarkers": true,
"text": {}
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"csvContent": "Name,Value,Type,Quota, Warning\nTemperature,25,degree,50,30\nPressure,100,Pa,300,200\nSpeed,30,km/h,150,130",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Custom mapping",
"transformations": [
{
"id": "rowsToFields",
"options": {
"mappings": [
{
"configProperty": "unit",
"fieldName": "Type",
"handlerKey": "unit"
},
{
"configProperty": "max",
"fieldName": "Quota",
"handlerKey": "max"
},
{
"configProperty": "threshold1",
"fieldName": "Warning",
"handlerKey": "threshold1"
}
]
}
}
],
"type": "gauge",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"custom": {}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 24
},
"id": 13,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "horizontal",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"text": {},
"textMode": "auto"
},
"pluginVersion": "8.1.0-pre",
"targets": [
{
"csvContent": "Name, City, Country, Value\nSensorA, Stockholm, Sweden, 20\nSensorB, London, England, 50\nSensorC, New York, USA,100",
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "Extra string fields to labels",
"transformations": [
{
"id": "rowsToFields",
"options": {}
}
],
"type": "stat",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h"
}
],
"refresh": "",
"schemaVersion": 33,
"style": "dark",
"tags": [
"gdev",
"transform"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"collapse": false,
"enable": true,
"hidden": false,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Transforms - Rows to fields",
"uid": "PMtIInink",
"version": 1
}

View File

@ -1,490 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"showIn": 0,
"type": "dashboard"
}
]
},
"editable": true,
"graphTooltip": 0,
"links": [],
"panels": [
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 80,
"lineWidth": 1
},
"mappings": [
{
"options": {
"CRITICAL": {
"color": "red",
"index": 3
},
"HIGH": {
"color": "orange",
"index": 2
},
"LOW": {
"color": "blue",
"index": 0
},
"NORMAL": {
"color": "green",
"index": 1
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 0
},
"id": 9,
"options": {
"alignValue": "center",
"colWidth": 0.9,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"mergeValues": true,
"rowHeight": 0.98,
"showValue": "always",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "SensorA",
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "LOW,HIGH,NORMAL,NORMAL,NORMAL,LOW,LOW,NORMAL,HIGH,CRITICAL"
},
{
"alias": "SensorB",
"hide": false,
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "NORMAL,LOW,LOW,CRITICAL,CRITICAL,LOW,LOW,NORMAL,HIGH,CRITICAL"
},
{
"alias": "SensorA",
"hide": false,
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "NORMAL,NORMAL,NORMAL,NORMAL,CRITICAL,LOW,NORMAL,NORMAL,NORMAL,LOW"
}
],
"title": "State changes strings",
"type": "state-timeline",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"fillOpacity": 70,
"lineWidth": 1
},
"mappings": [
{
"options": {
"match": "true",
"result": {
"color": "semi-dark-green",
"index": 0,
"text": "ON"
}
},
"type": "special"
},
{
"options": {
"match": "false",
"result": {
"color": "red",
"index": 1,
"text": "OFF"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 13,
"x": 0,
"y": 8
},
"id": 13,
"options": {
"alignValue": "center",
"colWidth": 1,
"legend": {
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false,
"calcs": []
},
"mergeValues": true,
"mode": "changes",
"rowHeight": 0.98,
"showValue": "always",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"alias": "",
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "true,false,true,true,true,true,false,false"
},
{
"hide": false,
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "false,true,false,true,true,false,false,false,true,true"
},
{
"hide": false,
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "true,false,true,true"
},
{
"hide": false,
"refId": "D",
"scenarioId": "csv_metric_values",
"stringInput": "false,true,false,true,true"
}
],
"title": "State changes with boolean values",
"type": "state-timeline",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"description": "Should show gaps",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"fillOpacity": 80,
"lineWidth": 1
},
"mappings": [
{
"options": {
"match": "true",
"result": {
"color": "semi-dark-green",
"index": 0,
"text": "ON"
}
},
"type": "special"
},
{
"options": {
"match": "false",
"result": {
"color": "red",
"index": 1,
"text": "OFF"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 11,
"x": 13,
"y": 8
},
"id": 12,
"options": {
"alignValue": "center",
"colWidth": 1,
"legend": {
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false,
"calcs": []
},
"mergeValues": true,
"mode": "changes",
"rowHeight": 0.98,
"showValue": "always",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "true,false,true,true,true,true,false,false"
},
{
"hide": false,
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "false,true,false,true,true,false,false,false,true,true"
},
{
"hide": false,
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "true,false,null,true,true"
},
{
"hide": false,
"refId": "D",
"scenarioId": "csv_metric_values",
"stringInput": "false,null,null,false,true,true"
}
],
"title": "State changes with nulls",
"type": "state-timeline",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"fillOpacity": 96,
"lineWidth": 0
},
"decimals": 0,
"mappings": [],
"max": 30,
"min": -10,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 12,
"w": 24,
"x": 0,
"y": 19
},
"id": 4,
"maxDataPoints": 20,
"options": {
"alignValue": "center",
"colWidth": 0.96,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"rowHeight": 0.98,
"showValue": "always",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1"
},
"lines": 10,
"max": 30,
"min": -10,
"noise": 2,
"points": [],
"pulseWave": {
"offCount": 3,
"offValue": 1,
"onCount": 3,
"onValue": 2,
"timeStep": 60
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 4,
"spread": 15,
"startValue": 5,
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": ""
}
],
"title": "Status map",
"type": "status-history",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
}
],
"refresh": false,
"schemaVersion": 33,
"style": "dark",
"tags": [
"gdev",
"demo"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"collapse": false,
"enable": true,
"hidden": false,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "utc",
"title": "Timeline Demo",
"uid": "mIJjFy8Kz",
"version": 3
}

View File

@ -1,427 +0,0 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"showIn": 0,
"type": "dashboard"
}
]
},
"editable": true,
"graphTooltip": 0,
"id": 329,
"links": [],
"panels": [
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 70,
"lineWidth": 1
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 0
},
"id": 8,
"options": {
"alignValue": "left",
"colWidth": 0.9,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"mergeValues": true,
"rowHeight": 0.9,
"showValue": "always",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1"
},
"lines": 10,
"points": [
[
0,
1616551651000
],
[
1,
1616556554000
],
[
2,
1616559873000
],
[
0,
1616561077000
],
[
3,
1616563090000
]
],
"pulseWave": {
"offCount": 3,
"offValue": 1,
"onCount": 3,
"onValue": 2,
"timeStep": 60
},
"refId": "A",
"scenarioId": "manual_entry",
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": ""
},
{
"alias": "",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1"
},
"hide": false,
"lines": 10,
"points": [
[
4,
1616555060000
],
[
5,
1616560081000
],
[
4,
1616562217000
],
[
5,
1616565458000
]
],
"pulseWave": {
"offCount": 3,
"offValue": 1,
"onCount": 3,
"onValue": 2,
"timeStep": 60
},
"refId": "B",
"scenarioId": "manual_entry",
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": ""
},
{
"points": [
[
4,
1616557148000
],
[
1616558756000
],
[
4,
1616561658000
],
[
1616562446000
],
[
4,
1616564104000
],
[
1616564548000
],
[
4,
1616564871000
]
],
"refId": "C",
"scenarioId": "manual_entry"
}
],
"title": "State timeline",
"type": "state-timeline",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 70,
"lineWidth": 1
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 24,
"x": 0,
"y": 10
},
"id": 9,
"options": {
"alignValue": "left",
"colWidth": 0.9,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"mergeValues": true,
"rowHeight": 0.9,
"showValue": "always",
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "",
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "a,a,b,b,b,b,c,a,a,d,d,d,d,d"
},
{
"alias": "",
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "null,null,e,e,e,null,null,e,null,null,e,null,e,e,e,e"
},
{
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "true,null,false,null,true,false"
}
],
"title": "State timeline (strings & booleans)",
"type": "state-timeline",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
},
{
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"fillOpacity": 70,
"lineWidth": 1
},
"mappings": [],
"thresholds": {
"mode": "percentage",
"steps": [
{
"color": "green"
},
{
"color": "#EAB839",
"value": 60
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 24,
"x": 0,
"y": 21
},
"id": 4,
"maxDataPoints": 20,
"options": {
"colWidth": 0.9,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"asTable": false,
"isVisible": false
},
"rowHeight": 0.9,
"showValue": "always",
"tooltip": {
"mode": "single",
"sort": "none"
},
"alignValue": "left"
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1"
},
"lines": 10,
"points": [],
"pulseWave": {
"offCount": 3,
"offValue": 1,
"onCount": 3,
"onValue": 2,
"timeStep": 60
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 4,
"spread": 14.9,
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": ""
}
],
"title": "Status grid",
"type": "status-history",
"panelSchema": [
0,
0
],
"transparent": false,
"repeatDirection": "h",
"transformations": []
}
],
"refresh": false,
"schemaVersion": 33,
"style": "dark",
"tags": [
"gdev",
"panel-tests"
],
"templating": {
"list": []
},
"time": {
"from": "2021-03-24T03:00:00.000Z",
"to": "2021-03-24T07:00:00.000Z"
},
"timepicker": {
"collapse": false,
"enable": true,
"hidden": false,
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "utc",
"title": "Timeline Modes",
"uid": "mIJjFy8Gz",
"version": 13
}

View File

@ -1,41 +0,0 @@
Verifies common usecases for trimdefault/applydefault functions:
* Basic cases
* Real dashboard
-- CUE --
{
timepicker?: {
collapse: bool | *false
enable: bool | *true
hidden: bool | *false
refresh_intervals: [...string] | *["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
}
}
-- Full --
{
"timepicker": {
"collapse": true,
"enable":true,
"hidden":false,
"refresh_intervals":[
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
}
}
-- Trimmed --
{
"timepicker": {
"collapse": true
}
}

View File

@ -1,35 +0,0 @@
Verifies common usecases for trimdefault/applydefault functions:
* Basic cases
* Real dashboard
-- CUE --
{
id?: number
uid: string
gnetId?: string
style: *"light" | "dark"
timezone?: *"browser" | "utc"
editable: bool | *true
schemaVersion: number | *25
version?: number
graphTooltip: >=0 & <=2 | *0
}
-- Full --
{
"editable": true,
"graphTooltip": 0,
"id": 42,
"schemaVersion": 27,
"style": "light",
"uid": "emal8gQMz",
"version": 2
}
-- Trimmed --
{
"id": 42,
"schemaVersion": 27,
"uid": "emal8gQMz",
"version": 2
}

View File

@ -1,71 +0,0 @@
Verifies common usecases for trimdefault/applydefault functions:
* open structure should be kept when fields not present
-- CUE --
#ListA: {
datasource: "gdev-postgres"
hide: number | *0
includeAll : bool | *true
label: string | *"Datacenter"
}
#ListB: {
datasource: "gdev-mysql"
hide: number | *1
includeAll : bool | *false
label: string | *"Datacenter"
}
#ListC: {
datasource: !=""
hide: number | *2
includeAll : bool | *false
label: string | *"Awesome"
}
{
templating?: list: [...#ListA | #ListB | #ListC]
}
-- Full --
{
"templating": {
"list": [
{
"datasource": "gdev-postgres",
"hide": 0,
"includeAll": false,
"label": "Datacenter"
},
{
"datasource": "gdev-mysql",
"hide": 0,
"includeAll": false,
"label": "Datacenter"
},
{
"datasource": "gdev-random",
"hide": 2,
"includeAll": false,
"label": "Datacenter"
}
]
}
}
-- Trimmed --
{
"templating": {
"list": [
{
"datasource": "gdev-postgres",
"includeAll": false
},
{
"datasource": "gdev-mysql",
"hide": 0
},
{
"datasource": "gdev-random",
"label": "Datacenter"
}
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +0,0 @@
Verifies common usecases for trimdefault/applydefault functions:
* Nested struct
* Simple array
-- CUE --
{
annotations?: list: [...{
builtIn: number | *0
datasource: string
enable?: bool | *true
hide?: bool | *false
iconColor?: string
name?: string
type: string | *"dashboard"
rawQuery?: string
showIn: number | *0
}]
}
-- Full --
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"name": "Annotations & Alerts",
"showIn": 0,
"type": "dashboard"
}
]
}
}
-- Trimmed --
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"name": "Annotations & Alerts"
}
]
}
}

View File

@ -1,73 +0,0 @@
Verifies common usecases for trimdefault/applydefault functions:
* open structure should be kept when fields not present
-- CUE --
{
templating?: list: [...{...}]
}
-- Full --
{
"templating": {
"list": [
{
"allValue": null,
"current": {
"text": "America",
"value": "America"
},
"datasource": "gdev-postgres",
"definition": "",
"hide": 0,
"includeAll": false,
"label": "Datacenter",
"multi": false,
"name": "datacenter",
"options": [],
"query": "SELECT DISTINCT datacenter FROM grafana_metric",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
}
}
-- Trimmed --
{
"templating": {
"list": [
{
"allValue": null,
"current": {
"text": "America",
"value": "America"
},
"datasource": "gdev-postgres",
"definition": "",
"hide": 0,
"includeAll": false,
"label": "Datacenter",
"multi": false,
"name": "datacenter",
"options": [],
"query": "SELECT DISTINCT datacenter FROM grafana_metric",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"tagValuesQuery": "",
"tags": [],
"tagsQuery": "",
"type": "query",
"useTags": false
}
]
}
}

View File

@ -78,7 +78,6 @@ import (
"github.com/grafana/grafana/pkg/services/queryhistory"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/schemaloader"
"github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/services/secrets"
@ -190,7 +189,6 @@ var wireBasicSet = wire.NewSet(
contexthandler.ProvideService,
jwt.ProvideService,
wire.Bind(new(models.JWTService), new(*jwt.AuthService)),
schemaloader.ProvideService,
ngalert.ProvideService,
librarypanels.ProvideService,
wire.Bind(new(librarypanels.Service), new(*librarypanels.LibraryPanelService)),

View File

@ -6,7 +6,6 @@ import (
"github.com/grafana/grafana/pkg/api/apierrors"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
@ -19,17 +18,15 @@ import (
type ImportDashboardAPI struct {
dashboardImportService dashboardimport.Service
quotaService QuotaService
schemaLoaderService SchemaLoaderService
pluginStore plugins.Store
ac accesscontrol.AccessControl
}
func New(dashboardImportService dashboardimport.Service, quotaService QuotaService,
schemaLoaderService SchemaLoaderService, pluginStore plugins.Store, ac accesscontrol.AccessControl) *ImportDashboardAPI {
pluginStore plugins.Store, ac accesscontrol.AccessControl) *ImportDashboardAPI {
return &ImportDashboardAPI{
dashboardImportService: dashboardImportService,
quotaService: quotaService,
schemaLoaderService: schemaLoaderService,
pluginStore: pluginStore,
ac: ac,
}
@ -65,14 +62,6 @@ func (api *ImportDashboardAPI) ImportDashboard(c *models.ReqContext) response.Re
return response.Error(403, "Quota reached", nil)
}
trimDefaults := c.QueryBoolWithDefault("trimdefaults", true)
if trimDefaults && !api.schemaLoaderService.IsDisabled() {
req.Dashboard, err = api.schemaLoaderService.DashboardApplyDefaults(req.Dashboard)
if err != nil {
return response.Error(http.StatusInternalServerError, "Error while applying default value to the dashboard json", err)
}
}
req.User = c.SignedInUser
resp, err := api.dashboardImportService.ImportDashboard(c.Req.Context(), &req)
if err != nil {
@ -91,8 +80,3 @@ type quotaServiceFunc func(c *models.ReqContext, target string) (bool, error)
func (fn quotaServiceFunc) QuotaReached(c *models.ReqContext, target string) (bool, error) {
return fn(c, target)
}
type SchemaLoaderService interface {
IsDisabled() bool
DashboardApplyDefaults(input *simplejson.Json) (*simplejson.Json, error)
}

View File

@ -26,15 +26,7 @@ func TestImportDashboardAPI(t *testing.T) {
},
}
schemaLoaderServiceCalled := false
schemaLoaderService := &schemaLoaderServiceMock{
dashboardApplyDefaultsFunc: func(input *simplejson.Json) (*simplejson.Json, error) {
schemaLoaderServiceCalled = true
return input, nil
},
}
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), schemaLoaderService, nil, acmock.New().WithDisabled())
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, acmock.New().WithDisabled())
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
@ -98,7 +90,6 @@ func TestImportDashboardAPI(t *testing.T) {
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
require.False(t, schemaLoaderServiceCalled)
require.True(t, importDashboardServiceCalled)
})
})
@ -112,16 +103,7 @@ func TestImportDashboardAPI(t *testing.T) {
},
}
schemaLoaderServiceCalled := false
schemaLoaderService := &schemaLoaderServiceMock{
enabled: true,
dashboardApplyDefaultsFunc: func(input *simplejson.Json) (*simplejson.Json, error) {
schemaLoaderServiceCalled = true
return input, nil
},
}
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), schemaLoaderService, nil, acmock.New().WithDisabled())
importDashboardAPI := New(service, quotaServiceFunc(quotaNotReached), nil, acmock.New().WithDisabled())
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
s := webtest.NewServer(t, routeRegister)
@ -140,15 +122,13 @@ func TestImportDashboardAPI(t *testing.T) {
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
require.True(t, schemaLoaderServiceCalled)
require.True(t, importDashboardServiceCalled)
})
})
t.Run("Quota reached", func(t *testing.T) {
service := &serviceMock{}
schemaLoaderService := &schemaLoaderServiceMock{}
importDashboardAPI := New(service, quotaServiceFunc(quotaReached), schemaLoaderService, nil, acmock.New().WithDisabled())
importDashboardAPI := New(service, quotaServiceFunc(quotaReached), nil, acmock.New().WithDisabled())
routeRegister := routing.NewRouteRegister()
importDashboardAPI.RegisterAPIEndpoints(routeRegister)
@ -184,23 +164,6 @@ func (s *serviceMock) ImportDashboard(ctx context.Context, req *dashboardimport.
return nil, nil
}
type schemaLoaderServiceMock struct {
enabled bool
dashboardApplyDefaultsFunc func(input *simplejson.Json) (*simplejson.Json, error)
}
func (s *schemaLoaderServiceMock) IsDisabled() bool {
return !s.enabled
}
func (s *schemaLoaderServiceMock) DashboardApplyDefaults(input *simplejson.Json) (*simplejson.Json, error) {
if s.dashboardApplyDefaultsFunc != nil {
return s.dashboardApplyDefaultsFunc(input)
}
return input, nil
}
func quotaReached(c *models.ReqContext, target string) (bool, error) {
return true, nil
}

View File

@ -14,11 +14,10 @@ import (
"github.com/grafana/grafana/pkg/services/librarypanels"
"github.com/grafana/grafana/pkg/services/plugindashboards"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/schemaloader"
)
func ProvideService(routeRegister routing.RouteRegister,
quotaService *quota.QuotaService, schemaLoaderService *schemaloader.SchemaLoaderService,
quotaService *quota.QuotaService,
pluginDashboardService plugindashboards.Service, pluginStore plugins.Store,
libraryPanelService librarypanels.Service, dashboardService dashboards.DashboardService,
ac accesscontrol.AccessControl,
@ -29,7 +28,7 @@ func ProvideService(routeRegister routing.RouteRegister,
libraryPanelService: libraryPanelService,
}
dashboardImportAPI := api.New(s, quotaService, schemaLoaderService, pluginStore, ac)
dashboardImportAPI := api.New(s, quotaService, pluginStore, ac)
dashboardImportAPI.RegisterAPIEndpoints(routeRegister)
return s

View File

@ -1,144 +0,0 @@
package schemaloader
import (
"encoding/json"
"fmt"
"github.com/grafana/grafana"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/schema"
"github.com/grafana/grafana/pkg/schema/load"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/infra/log"
)
const ServiceName = "SchemaLoader"
var baseLoadPath load.BaseLoadPaths = load.BaseLoadPaths{
BaseCueFS: grafana.CoreSchema,
DistPluginCueFS: grafana.PluginSchema,
}
type RenderUser struct {
OrgID int64
UserID int64
OrgRole string
}
func ProvideService(features featuremgmt.FeatureToggles) (*SchemaLoaderService, error) {
dashFam, err := load.BaseDashboardFamily(baseLoadPath)
if err != nil {
return nil, fmt.Errorf("failed to load dashboard cue schema from path %q: %w", baseLoadPath, err)
}
s := &SchemaLoaderService{
features: features,
DashFamily: dashFam,
log: log.New("schemaloader"),
}
return s, nil
}
type SchemaLoaderService struct {
log log.Logger
DashFamily schema.VersionedCueSchema
features featuremgmt.FeatureToggles
}
func (rs *SchemaLoaderService) IsDisabled() bool {
if rs.features == nil {
return true
}
return !rs.features.IsEnabled(featuremgmt.FlagTrimDefaults)
}
func (rs *SchemaLoaderService) DashboardApplyDefaults(input *simplejson.Json) (*simplejson.Json, error) {
val, _ := input.Map()
val = removeNils(val)
data, _ := json.Marshal(val)
dsSchema := schema.Find(rs.DashFamily, schema.Latest())
result, err := schema.ApplyDefaults(schema.Resource{Value: data}, dsSchema.CUE())
if err != nil {
return input, err
}
output, err := simplejson.NewJson([]byte(result.Value.(string)))
if err != nil {
return input, err
}
return output, nil
}
func (rs *SchemaLoaderService) DashboardTrimDefaults(input simplejson.Json) (simplejson.Json, error) {
val, _ := input.Map()
val = removeNils(val)
data, _ := json.Marshal(val)
dsSchema, err := schema.SearchAndValidate(rs.DashFamily, string(data))
if err != nil {
return input, err
}
result, err := schema.TrimDefaults(schema.Resource{Value: data}, dsSchema.CUE())
if err != nil {
return input, err
}
output, err := simplejson.NewJson([]byte(result.Value.(string)))
if err != nil {
return input, err
}
return *output, nil
}
func removeNils(initialMap map[string]interface{}) map[string]interface{} {
withoutNils := map[string]interface{}{}
for key, value := range initialMap {
_, ok := value.(map[string]interface{})
if ok {
value = removeNils(value.(map[string]interface{}))
withoutNils[key] = value
continue
}
_, ok = value.([]interface{})
if ok {
value = removeNilArray(value.([]interface{}))
withoutNils[key] = value
continue
}
if value != nil {
if val, ok := value.(string); ok {
if val == "" {
continue
}
}
withoutNils[key] = value
}
}
return withoutNils
}
func removeNilArray(initialArray []interface{}) []interface{} {
withoutNils := []interface{}{}
for _, value := range initialArray {
_, ok := value.(map[string]interface{})
if ok {
value = removeNils(value.(map[string]interface{}))
withoutNils = append(withoutNils, value)
continue
}
_, ok = value.([]interface{})
if ok {
value = removeNilArray(value.([]interface{}))
withoutNils = append(withoutNils, value)
continue
}
if value != nil {
if val, ok := value.(string); ok {
if val == "" {
continue
}
}
withoutNils = append(withoutNils, value)
}
}
return withoutNils
}

View File

@ -46,6 +46,10 @@ func main() {
}
wd, err := codegen.CuetsifyPlugins(cuecontext.New(), groot)
if err != nil {
fmt.Fprintf(os.Stderr, "error while generating code:\n%s\n", err)
os.Exit(1)
}
if _, set := os.LookupEnv("CODEGEN_VERIFY"); set {
err = wd.Verify()

View File

@ -14,24 +14,28 @@
package grafanaschema
Panel: {
lineages: [
[
{
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")
}
]
]
migrations: []
import "github.com/grafana/thema"
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")
},
]
},
]
}

View File

@ -15,33 +15,36 @@
package grafanaschema
import (
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
"github.com/grafana/thema"
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
)
Panel: {
lineages: [
[
{
PanelOptions: {
ui.OptionsWithLegend
ui.OptionsWithTooltip
ui.OptionsWithTextFormatting
orientation: ui.VizOrientation
// TODO this default is a guess based on common devenv values
stacking: ui.StackingMode | *"none"
showValue: ui.VisibilityMode
barWidth: number
groupWidth: number
} @cuetsy(kind="interface")
PanelFieldConfig: {
ui.AxisConfig
ui.HideableFieldConfig
lineWidth?: number
fillOpacity?: number
gradientMode?: ui.GraphGradientMode
} @cuetsy(kind="interface")
}
]
]
migrations: []
Panel: thema.#Lineage & {
name: "barchart"
seqs: [
{
schemas: [
{
PanelOptions: {
ui.OptionsWithLegend
ui.OptionsWithTooltip
ui.OptionsWithTextFormatting
orientation: ui.VizOrientation
// TODO this default is a guess based on common devenv values
stacking: ui.StackingMode | *"none"
showValue: ui.VisibilityMode
barWidth: number
groupWidth: number
} @cuetsy(kind="interface")
PanelFieldConfig: {
ui.AxisConfig
ui.HideableFieldConfig
lineWidth?: number
fillOpacity?: number
gradientMode?: ui.GraphGradientMode
} @cuetsy(kind="interface")
},
]
},
]
}

View File

@ -14,21 +14,26 @@
package grafanaschema
import ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
import (
"github.com/grafana/thema"
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
)
Panel: {
lineages: [
[
{
PanelOptions: {
ui.SingleStatBaseOptions
displayMode: ui.BarGaugeDisplayMode | *"gradient"
showUnfilled: bool | *true
minVizWidth: uint32 | *0
minVizHeight: uint32 | *10
} @cuetsy(kind="interface")
}
]
]
migrations: []
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")
},
]
},
]
}

View File

@ -14,20 +14,22 @@
package grafanaschema
Panel: {
lineages: [
[
{
PanelOptions: {
// anything for now
...
} @cuetsy(kind="interface")
PanelFieldConfig: {
// anything for now
...
} @cuetsy(kind="interface")
}
]
]
migrations: []
Panel: thema.#Lineage & {
name: "candlestick"
seqs: [
{
schemas: [
{
PanelOptions: {
// anything for now
...
} @cuetsy(kind="interface")
PanelFieldConfig: {
// anything for now
...
} @cuetsy(kind="interface")
},
]
},
]
}

View File

@ -14,16 +14,20 @@
package grafanaschema
Panel: {
lineages: [
[
{
PanelOptions: {
// anything for now
...
} @cuetsy(kind="interface")
}
]
]
migrations: []
import "github.com/grafana/thema"
Panel: thema.#Lineage & {
name: "canvas"
seqs: [
{
schemas: [
{
PanelOptions: {
// anything for now
...
} @cuetsy(kind="interface")
},
]
},
]
}

View File

@ -14,24 +14,28 @@
package grafanaschema
Panel: {
lineages: [
[
{
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")
},
]
]
migrations: []
import "github.com/grafana/thema"
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")
},
]
},
]
}

View File

@ -14,19 +14,24 @@
package grafanaschema
import ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
import (
"github.com/grafana/thema"
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
)
Panel: {
lineages: [
[
{
PanelOptions: {
ui.SingleStatBaseOptions
showThresholdLabels: bool | *false
showThresholdMarkers: bool | *true
} @cuetsy(kind="interface")
}
]
]
migrations: []
Panel: thema.#Lineage & {
name: "gauge"
seqs: [
{
schemas: [
{
PanelOptions: {
ui.SingleStatBaseOptions
showThresholdLabels: bool | *false
showThresholdMarkers: bool | *true
} @cuetsy(kind="interface")
},
]
},
]
}

View File

@ -14,20 +14,24 @@
package grafanaschema
Panel: {
lineages: [
[
{
PanelOptions: {
// anything for now
...
} @cuetsy(kind="interface")
PanelFieldConfig: {
// anything for now
...
} @cuetsy(kind="interface")
}
]
]
migrations: []
import "github.com/grafana/thema"
Panel: thema.#Lineage & {
name: "heatmap-new"
seqs: [
{
schemas: [
{
PanelOptions: {
// anything for now
...
} @cuetsy(kind="interface")
PanelFieldConfig: {
// anything for now
...
} @cuetsy(kind="interface")
},
]
},
]
}

View File

@ -14,23 +14,28 @@
package grafanaschema
import ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
import (
"github.com/grafana/thema"
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
)
Panel: {
lineages: [
[
{
PanelOptions: {
ui.OptionsWithLegend
ui.OptionsWithTooltip
bucketSize?: int
bucketOffset: int | *0
combine?: bool
} @cuetsy(kind="interface")
Panel: thema.#Lineage & {
name: "histogram"
seqs: [
{
schemas: [
{
PanelOptions: {
ui.OptionsWithLegend
ui.OptionsWithTooltip
bucketSize?: int
bucketOffset: int | *0
combine?: bool
} @cuetsy(kind="interface")
PanelFieldConfig: ui.GraphFieldConfig & {} @cuetsy(kind="interface")
}
]
]
migrations: []
PanelFieldConfig: ui.GraphFieldConfig & {} @cuetsy(kind="interface")
},
]
},
]
}

View File

@ -14,17 +14,21 @@
package grafanaschema
Panel: {
lineages: [
[
{
PanelOptions: {
// empty/missing will default to grafana blog
feedUrl?: string
showImage?: bool | *true
} @cuetsy(kind="interface")
}
]
]
migrations: []
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")
},
]
},
]
}

View File

@ -14,21 +14,26 @@
package grafanaschema
import ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
import (
"github.com/grafana/thema"
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
)
Panel: {
lineages: [
[
{
PanelOptions: {
ui.SingleStatBaseOptions
graphMode: ui.BigValueGraphMode | *"area"
colorMode: ui.BigValueColorMode | *"value"
justifyMode: ui.BigValueJustifyMode | *"auto"
textMode: ui.BigValueTextMode | *"auto"
} @cuetsy(kind="interface")
}
]
]
migrations: []
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")
},
]
},
]
}

View File

@ -15,33 +15,36 @@
package grafanaschema
import (
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
"github.com/grafana/thema"
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
)
Panel: {
lineages: [
[
{
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
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")
}
]
]
migrations: []
Panel: thema.#Lineage & {
name: "state-timeline"
lineages: [
{
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
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

@ -15,28 +15,31 @@
package grafanaschema
import (
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
"github.com/grafana/thema"
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
)
Panel: {
lineages: [
[
{
PanelOptions: {
ui.OptionsWithLegend
ui.OptionsWithTooltip
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")
}
]
]
migrations: []
Panel: thema.#Lineage & {
name: "status-history"
seqs: [
{
schemas: [
{
PanelOptions: {
ui.OptionsWithLegend
ui.OptionsWithTooltip
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,22 +15,25 @@
package grafanaschema
import (
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
"github.com/grafana/thema"
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
)
Panel: {
lineages: [
[
{
PanelOptions: {
frameIndex: number | *0
showHeader: bool | *true
showTypeIcons: bool | *false
sortBy?: [...ui.TableSortByFieldState]
} @cuetsy(kind="interface")
PanelFieldConfig: ui.TableFieldOptions & {} @cuetsy(kind="interface")
},
]
]
migrations: []
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")
},
]
},
]
}

View File

@ -14,21 +14,25 @@
package grafanaschema
Panel: {
lineages: [
[
{
TextMode: "html" | "markdown" @cuetsy(kind="enum",memberNames="HTML|Markdown")
PanelOptions: {
mode: TextMode | *"markdown"
content: string | *"""
import "github.com/grafana/thema"
Panel: thema.#Lineage & {
name: "text"
seqs: [
{
schemas: [
{
TextMode: "html" | "markdown" @cuetsy(kind="enum",memberNames="HTML|Markdown")
PanelOptions: {
mode: TextMode | *"markdown"
content: string | *"""
# Title
For markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)
"""
} @cuetsy(kind="interface")
}
]
]
migrations: []
} @cuetsy(kind="interface")
},
]
},
]
}

View File

@ -15,20 +15,23 @@
package grafanaschema
import (
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
"github.com/grafana/thema"
ui "github.com/grafana/grafana/packages/grafana-schema/src/schema"
)
Panel: {
lineages: [
[
{
PanelOptions: {
legend: ui.VizLegendOptions
tooltip: ui.VizTooltipOptions
} @cuetsy(kind="interface")
PanelFieldConfig: ui.GraphFieldConfig & {} @cuetsy(kind="interface")
}
]
]
migrations: []
Panel: thema.#Lineage & {
name: "timeseries"
seqs: [
{
schemas: [
{
PanelOptions: {
legend: ui.VizLegendOptions
tooltip: ui.VizTooltipOptions
} @cuetsy(kind="interface")
PanelFieldConfig: ui.GraphFieldConfig & {} @cuetsy(kind="interface")
},
]
},
]
}

View File

@ -39,8 +39,6 @@ load(
'upload_packages_step',
'store_packages_step',
'upload_cdn_step',
'validate_scuemata_step',
'ensure_cuetsified_step',
'verify_gen_cue_step',
'test_a11y_frontend_step',
'trigger_oss'
@ -135,8 +133,6 @@ def get_steps(edition):
build_frontend_step(edition=edition, ver_mode=ver_mode),
build_frontend_package_step(edition=edition, ver_mode=ver_mode),
build_plugins_step(edition=edition, sign=True),
validate_scuemata_step(),
ensure_cuetsified_step(),
verify_gen_cue_step(),
]
integration_test_steps = [

View File

@ -29,8 +29,6 @@ load(
'redis_integration_tests_step',
'memcached_integration_tests_step',
'benchmark_ldap_step',
'validate_scuemata_step',
'ensure_cuetsified_step',
'verify_gen_cue_step',
'test_a11y_frontend_step',
'enterprise_downstream_step',
@ -125,8 +123,6 @@ def pr_pipelines(edition):
build_frontend_step(edition=edition, ver_mode=ver_mode),
build_frontend_package_step(edition=edition, ver_mode=ver_mode),
build_plugins_step(edition=edition),
validate_scuemata_step(),
ensure_cuetsified_step(),
verify_gen_cue_step(),
]
integration_test_steps = [

View File

@ -39,8 +39,6 @@ load(
'upload_packages_step',
'store_packages_step',
'upload_cdn_step',
'validate_scuemata_step',
'ensure_cuetsified_step',
'verify_gen_cue_step',
'publish_images_step',
'trigger_oss'
@ -182,8 +180,6 @@ def get_steps(edition, ver_mode):
build_frontend_step(edition=edition, ver_mode=ver_mode),
build_frontend_package_step(edition=edition, ver_mode=ver_mode),
build_plugins_step(edition=edition, sign=True),
validate_scuemata_step(),
ensure_cuetsified_step(),
verify_gen_cue_step(),
]
@ -192,7 +188,6 @@ def get_steps(edition, ver_mode):
mysql_integration_tests_step(edition=edition, ver_mode=ver_mode),
]
if include_enterprise2:
test_steps.extend([
lint_backend_step(edition=edition2),

View File

@ -1149,36 +1149,6 @@ def get_windows_steps(edition, ver_mode):
return steps
def validate_scuemata_step():
return {
'name': 'validate-scuemata',
'image': build_image,
'depends_on': [
'build-backend',
],
'commands': [
'./bin/linux-amd64/grafana-cli cue validate-schema --grafana-root .',
],
}
def ensure_cuetsified_step():
return {
'name': 'ensure-cuetsified',
'image': build_image,
'depends_on': [
'validate-scuemata',
],
'commands': [
'# It is required that the generated Typescript be in sync with the input CUE files.',
'# To enforce this, the following command will attempt to generate Typescript from all',
'# appropriate .cue files, then compare with the corresponding (*.gen.ts) file the generated',
'# code would have been written to. It exits 1 if any diffs are found.',
'./bin/linux-amd64/grafana-cli cue gen-ts --grafana-root . --diff',
],
}
def verify_gen_cue_step():
return {
'name': 'verify-gen-cue',