diff --git a/devenv/dev-dashboards/dashboards_test.go b/devenv/dev-dashboards/dashboards_test.go index 66f29040f84..e330b53bb0f 100644 --- a/devenv/dev-dashboards/dashboards_test.go +++ b/devenv/dev-dashboards/dashboards_test.go @@ -68,7 +68,7 @@ func themaTestableDashboards() (map[string][]byte, error) { jtree := make(map[string]interface{}) json.Unmarshal(b, &jtree) - if oldschemav, has := jtree["schemaVersion"]; !has || !(oldschemav.(float64) > 32) { + if oldschemav, has := jtree["schemaVersion"]; !has || !(oldschemav.(float64) > dashboard.HandoffSchemaVersion-1) { return nil } diff --git a/devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json b/devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json index 29bda45b50f..410540966f0 100644 --- a/devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json +++ b/devenv/dev-dashboards/panel-graph/graph-ng-stacking2.json @@ -71,8 +71,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -178,8 +177,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -266,8 +264,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -354,8 +351,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -485,8 +481,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -573,8 +568,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -661,8 +655,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -792,8 +785,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -893,8 +885,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -982,8 +973,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1069,8 +1059,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1155,8 +1144,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1253,8 +1241,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1354,8 +1341,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1455,8 +1441,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1555,8 +1540,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1692,8 +1676,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1800,8 +1783,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1909,8 +1891,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2040,8 +2021,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2128,8 +2108,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2240,8 +2219,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2356,8 +2334,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2460,8 +2437,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2564,8 +2540,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2701,8 +2676,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2838,8 +2812,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2934,7 +2907,7 @@ } ], "refresh": false, - "schemaVersion": 32, + "schemaVersion": 36, "style": "dark", "tags": [ "gdev", @@ -2954,4 +2927,4 @@ "uid": "1KxMUdE7k", "version": 1, "weekStart": "" -} \ No newline at end of file +} diff --git a/packages/grafana-schema/src/scuemata/dashboard/dashboard.cue b/packages/grafana-schema/src/scuemata/dashboard/dashboard.cue index cb7e767ca01..9040f0a28c3 100644 --- a/packages/grafana-schema/src/scuemata/dashboard/dashboard.cue +++ b/packages/grafana-schema/src/scuemata/dashboard/dashboard.cue @@ -1,434 +1,441 @@ package dashboard import ( - "list" + "list" - "github.com/grafana/grafana/cue/scuemata" + "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 + {// 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 | 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)] + 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 + #FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(kind="enum") - // TODO docs - #FieldColorSeriesByMode: "min" | "max" | "last" @cuetsy(kind="type") + // 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 + #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") + // 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") + #ThresholdsMode: "absolute" | "percentage" @cuetsy(kind="enum") - #ThresholdsConfig: { - mode: #ThresholdsMode + #ThresholdsConfig: { + mode: #ThresholdsMode - // Must be sorted by 'value', first value is always -Infinity - steps: [...#Threshold] - } @cuetsy(kind="interface") + // 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: {...} - } + // 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: {...} + // 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: !="" + // 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 + // TODO docs + id?: number - // FIXME this almost certainly has to be changed in favor of scuemata versions - pluginVersion?: string + // FIXME this almost certainly has to be changed in favor of scuemata versions + pluginVersion?: string - // TODO docs - tags?: [...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] + // 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] + // 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?: [] + // 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" + // 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 + maxDataPoints?: number - // TODO docs - thresholds?: [...] + // TODO docs + thresholds?: [...] - // TODO docs - timeRegions?: [...] + // TODO docs + timeRegions?: [...] - transformations: [...#Transformation] + transformations: [...#Transformation] - // TODO docs - // TODO tighter constraint - interval?: string + // TODO docs + // TODO tighter constraint + interval?: string - // TODO docs - // TODO tighter constraint - timeFrom?: string + // TODO docs + // TODO tighter constraint + timeFrom?: string - // TODO docs - // TODO tighter constraint - timeShift?: string + // TODO docs + // TODO tighter constraint + timeShift?: string - // options is specified by the PanelOptions field in panel - // plugin schemas. - options: {} + // 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 + 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 + // 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 + // 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 + // 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 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 + // True if data source field supports ad-hoc filters + filterable?: bool - // Numeric Options - unit?: string + // Numeric Options + unit?: string - // Significant digits (for display) - decimals?: number + // Significant digits (for display) + decimals?: number - min?: number - max?: 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?: [...{...}] + // 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 numeric values to states + thresholds?: #ThresholdsConfig - // // Map values to a display color - color?: #FieldColor + // // Map values to a display color + color?: #FieldColor - // // Used when reducing field values - // nullValueMode?: NullValueMode + // // Used when reducing field values + // nullValueMode?: NullValueMode - // // The behavior when clicking on a result - links?: [...] + // // The behavior when clicking on a result + links?: [...] - // Alternative to empty string - noValue?: string + // 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 - } + // 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: {...} - ... - } - } + // 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 + // Row panel + #RowPanel: { + type: "row" + collapsed: bool | *false + title?: string - // Name of default datasource. - datasource?: 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" - } - } - } - ] + 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 + 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 - } - } - } - } + // _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 + } + } + } + } } diff --git a/pkg/coremodel/dashboard/lineage.cue b/pkg/coremodel/dashboard/lineage.cue index 1910539d5a6..bd1c1db4f2e 100644 --- a/pkg/coremodel/dashboard/lineage.cue +++ b/pkg/coremodel/dashboard/lineage.cue @@ -7,338 +7,345 @@ name: "dashboard" seqs: [ { schemas: [ - { // 0.0 - // Unique numeric identifier for the dashboard. - // TODO must isolate or remove identifiers local to a Grafana instance...? - id?: int64 - // Unique dashboard identifier that can be generated by anyone. string (8-40) - uid?: string - // Title of dashboard. - title?: string - // Description of dashboard. - description?: string + {// 0.0 + // Unique numeric identifier for the dashboard. + // TODO must isolate or remove identifiers local to a Grafana instance...? + id?: int64 + // 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: uint8 & >=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: uint8 | *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: uint8 | *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 Thema's themaVersion - schemaVersion: uint16 | *33 - // Version of the dashboard, incremented each time the dashboard is updated. - version?: uint32 - panels?: [...(#Panel | #GraphPanel | #HeatmapPanel | #RowPanel)] + 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: uint8 & >=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: uint8 | *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: uint8 | *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 Thema's themaVersion + schemaVersion: uint16 | *33 + // Version of the dashboard, incremented each time the dashboard is updated. + version?: uint32 + panels?: [...(#Panel | #GraphPanel | #HeatmapPanel | #RowPanel)] - // TODO docs - #FieldColorModeId: "thresholds" | "palette-classic" | "palette-saturated" | "continuous-GrYlRd" | "fixed" @cuetsy(kind="enum") + // 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 + #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 + #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") + // 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") + #ThresholdsMode: "absolute" | "percentage" @cuetsy(kind="enum") - #ThresholdsConfig: { - mode: #ThresholdsMode + #ThresholdsConfig: { + mode: #ThresholdsMode - // Must be sorted by 'value', first value is always -Infinity - steps: [...#Threshold] - } @cuetsy(kind="interface") + // 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: {...} - } + // 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: {...} + // 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: !="" + // 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?: uint32 + // TODO docs + id?: uint32 - // FIXME this almost certainly has to be changed in favor of scuemata versions - pluginVersion?: string + // FIXME this almost certainly has to be changed in favor of scuemata versions + pluginVersion?: string - // TODO docs - tags?: [...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] + // 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] + // 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: uint32 & >0 | *9 - // Panel - w: uint32 & >0 & <=24 | *12 - // Panel x - x: uint32 & >=0 & <24 | *0 - // Panel y - y: uint32 & >=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?: [] + // 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: uint32 & >0 | *9 + // Panel + w: uint32 & >0 & <=24 | *12 + // Panel x + x: uint32 & >=0 & <24 | *0 + // Panel y + y: uint32 & >=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" + // 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 + maxDataPoints?: number - // TODO docs - thresholds?: [...] + // TODO docs + thresholds?: [...] - // TODO docs - timeRegions?: [...] + // TODO docs + timeRegions?: [...] - transformations: [...#Transformation] + transformations: [...#Transformation] - // TODO docs - // TODO tighter constraint - interval?: string + // TODO docs + // TODO tighter constraint + interval?: string - // TODO docs - // TODO tighter constraint - timeFrom?: string + // TODO docs + // TODO tighter constraint + timeFrom?: string - // TODO docs - // TODO tighter constraint - timeShift?: string + // TODO docs + // TODO tighter constraint + timeShift?: string - // options is specified by the PanelOptions field in panel - // plugin schemas. - options: {...} + // 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 + 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 + // 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 + // 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 + // 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 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 + // True if data source field supports ad-hoc filters + filterable?: bool - // Numeric Options - unit?: string + // Numeric Options + unit?: string - // Significant digits (for display) - decimals?: number + // Significant digits (for display) + decimals?: number - min?: number - max?: 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?: [...{...}] + // 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 numeric values to states + thresholds?: #ThresholdsConfig - // // Map values to a display color - color?: #FieldColor + // // Map values to a display color + color?: #FieldColor - // // Used when reducing field values - // nullValueMode?: NullValueMode + // // Used when reducing field values + // nullValueMode?: NullValueMode - // // The behavior when clicking on a result - links?: [...] + // // The behavior when clicking on a result + links?: [...] - // Alternative to empty string - noValue?: string + // 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?: _ - }] - }] - } - } + // custom is specified by the PanelFieldConfig field + // in panel plugin schemas. + custom?: {...} + } + overrides: [...{ + matcher: { + id: string | *"" + options?: _ + } + properties: [...{ + id: string | *"" + value?: _ + }] + }] + } + } - // Row panel - #RowPanel: { - type: "row" - collapsed: bool | *false - title?: string + // Row panel + #RowPanel: { + type: "row" + collapsed: bool | *false + title?: string - // Name of default datasource. - datasource?: string + // Name of default datasource. + datasource?: { + type?: string + uid?: string + } - gridPos?: { - // Panel - h: uint32 & >0 | *9 - // Panel - w: uint32 & >0 & <=24 | *12 - // Panel x - x: uint32 & >=0 & <24 | *0 - // Panel y - y: uint32 & >=0 | *0 - // true if fixed - static?: bool - } - id: uint32 - panels: [...(#Panel | #GraphPanel | #HeatmapPanel)] - // Name of template variable to repeat for. - repeat?: string - } - // Support for legacy graph and heatmap panels. - #GraphPanel: { - ... - type: "graph" - } - #HeatmapPanel: { - ... - type: "heatmap" - } - } - ] - } + gridPos?: { + // Panel + h: uint32 & >0 | *9 + // Panel + w: uint32 & >0 & <=24 | *12 + // Panel x + x: uint32 & >=0 & <24 | *0 + // Panel y + y: uint32 & >=0 | *0 + // true if fixed + static?: bool + } + id: uint32 + panels: [...(#Panel | #GraphPanel | #HeatmapPanel)] + // Name of template variable to repeat for. + repeat?: string + } + + // Support for legacy graph and heatmap panels. + #GraphPanel: { + type: "graph" + ... + } + #HeatmapPanel: { + type: "heatmap" + ... + } + }, + ] + }, ] diff --git a/pkg/coremodel/dashboard/schema.go b/pkg/coremodel/dashboard/schema.go index 96c235a1712..c7b7abc084a 100644 --- a/pkg/coremodel/dashboard/schema.go +++ b/pkg/coremodel/dashboard/schema.go @@ -17,6 +17,15 @@ var ( currentVersion = thema.SV(0, 0) ) +// HandoffSchemaVersion is the minimum schemaVersion for dashboards at which the +// Thema-based dashboard schema is known to be valid. +// +// schemaVersion is the original version numbering system for dashboards. If a +// dashboard is below this schemaVersion, it is necessary for the frontend +// typescript dashboard migration logic to first run and get it past this +// number, after which Thema can take over. +const HandoffSchemaVersion = 36 + // Lineage returns the Thema lineage representing Grafana dashboards. The // lineage is the canonical specification of the current datasource schema, all // prior schema versions, and the mappings that allow migration between schema @@ -80,7 +89,7 @@ type model struct { SchemaVersion int `json:"schemaVersion"` Panels []interface{} `json:"panels"` - //// + // // Uid string `json:"uid"` // OrgId int64 `json:"orgId"` diff --git a/pkg/schema/load/load_test.go b/pkg/schema/load/load_test.go index 65d7655bac5..d9f96849cc0 100644 --- a/pkg/schema/load/load_test.go +++ b/pkg/schema/load/load_test.go @@ -19,6 +19,7 @@ import ( "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" @@ -48,7 +49,6 @@ var doTestAgainstDevenv = func(sch schema.VersionedCueSchema, validdir string, f b, err := os.Open(path) require.NoError(t, err, "failed to open dashboard file") - // Only try to validate dashboards with schemaVersion >= 30 jtree := make(map[string]interface{}) byt, err := io.ReadAll(b) if err != nil { @@ -59,9 +59,9 @@ var doTestAgainstDevenv = func(sch schema.VersionedCueSchema, validdir string, f t.Logf("no schemaVersion in %s", path) return nil } else { - if !(oldschemav.(float64) > 32) { + if !(oldschemav.(float64) > dashboard.HandoffSchemaVersion-1) { if testing.Verbose() { - t.Logf("schemaVersion is %v, older than 33, skipping %s", oldschemav, path) + t.Logf("schemaVersion is %v, older than %v, skipping %s", oldschemav, dashboard.HandoffSchemaVersion-1, path) } return nil } diff --git a/scripts/stripnulls.sh b/scripts/stripnulls.sh index 90a873ed50b..d64ff2fb8c9 100755 --- a/scripts/stripnulls.sh +++ b/scripts/stripnulls.sh @@ -7,7 +7,7 @@ SED=$(command -v gsed) SED=${SED:-"sed"} -FILES=$(grep -rl '"schemaVersion": 3[34]' devenv) +FILES=$(grep -rl '"schemaVersion": 3[3456]' devenv) set -e set -x for DASH in ${FILES}; do echo "${DASH}"; grep -v 'null,$' "${DASH}" > "${DASH}-nulless"; mv "${DASH}-nulless" "${DASH}"; done