diff --git a/go.mod b/go.mod index f30d4328d38..92fd45c35d6 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,7 @@ require ( github.com/google/wire v0.5.0 github.com/gorilla/websocket v1.5.0 github.com/gosimple/slug v1.12.0 - github.com/grafana/cuetsy v0.0.4-0.20220714174355-ebd987fdab27 + github.com/grafana/cuetsy v0.1.1 github.com/grafana/grafana-aws-sdk v0.10.8 github.com/grafana/grafana-azure-sdk-go v1.3.0 github.com/grafana/grafana-plugin-sdk-go v0.139.0 @@ -275,6 +275,7 @@ require ( github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/memberlist v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index ddef442eea2..272fd7e2903 100644 --- a/go.sum +++ b/go.sum @@ -1368,8 +1368,8 @@ github.com/gosimple/slug v1.12.0 h1:xzuhj7G7cGtd34NXnW/yF0l+AGNfWqwgh/IXgFy7dnc= github.com/gosimple/slug v1.12.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= -github.com/grafana/cuetsy v0.0.4-0.20220714174355-ebd987fdab27 h1:r0ZSb0gSKEf1hmdYljn7ezOPJyfrC0qcky576U82yEk= -github.com/grafana/cuetsy v0.0.4-0.20220714174355-ebd987fdab27/go.mod h1:7OoEYb42s7PbSYtNUTy1DoCeJ3LAOTafsZbndvikTj8= +github.com/grafana/cuetsy v0.1.1 h1:+1jaDDYCpvKlcOWJgBRbkc5+VZIClCEn5mbI+4PLZqM= +github.com/grafana/cuetsy v0.1.1/go.mod h1:4KWkUOslwvRTpEv7wdQG0jDFTuJmU+0L9x0h4kWxa2A= github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f h1:FvvSVEbnGeM2bUivGmsiXTi8URJyBU7TcFEEoRe5wWI= github.com/grafana/dskit v0.0.0-20211011144203-3a88ec0b675f/go.mod h1:uPG2nyK4CtgNDmWv7qyzYcdI+S90kHHRWvHnBtEMBXM= github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036 h1:GplhUk6Xes5JIhUUrggPcPBhOn+eT8+WsHiebvq7GgA= diff --git a/packages/grafana-schema/src/index.gen.ts b/packages/grafana-schema/src/index.gen.ts new file mode 100644 index 00000000000..a34df96996a --- /dev/null +++ b/packages/grafana-schema/src/index.gen.ts @@ -0,0 +1,76 @@ +// This file is autogenerated. DO NOT EDIT. +// +// Generated by pkg/framework/coremodel/gen.go +// +// Run `make gen-cue` from repository root to regenerate. + +// Raw generated types from dashboard entity type. +export type { + AnnotationQuery, + VariableModel, + DashboardLink, + DashboardLinkType, + VariableType, + FieldColorModeId, + FieldColorSeriesByMode, + FieldColor, + GridPos, + Threshold, + ThresholdsMode, + ThresholdsConfig, + ValueMapping, + MappingType, + ValueMap, + RangeMap, + RegexMap, + SpecialValueMap, + SpecialValueMatch, + ValueMappingResult, + Transformation, + DashboardCursorSync, + MatcherConfig, + RowPanel +} from './raw/dashboard/x/dashboard.gen'; + +// Raw generated default consts from dashboard entity type. +export { + defaultAnnotationQuery, + defaultDashboardLink, + defaultGridPos, + defaultThresholdsConfig, + defaultDashboardCursorSync, + defaultMatcherConfig, + defaultRowPanel +} from './raw/dashboard/x/dashboard.gen'; + +// The following exported declarations correspond to types in the dashboard@0.0 schema with +// attribute @grafana(TSVeneer="type"). (lineage declared in file: pkg/coremodel/dashboard/coremodel.cue) +// +// The handwritten file for these type and default veneers is expected to be at +// packages/grafana-schema/src/veneer/dashboard.types.ts. +// This re-export declaration enforces that the handwritten veneer file exists, +// and exports all the symbols in the list. +// +// TODO generate code such that tsc enforces type compatibility between raw and veneer decls +export type { + Dashboard, + Panel, + FieldConfigSource, + FieldConfig +} from './veneer/dashboard.types'; + +// The following exported declarations correspond to types in the dashboard@0.0 schema with +// attribute @grafana(TSVeneer="type"). (lineage declared in file: pkg/coremodel/dashboard/coremodel.cue) +// +// The handwritten file for these type and default veneers is expected to be at +// packages/grafana-schema/src/veneer/dashboard.types.ts. +// This re-export declaration enforces that the handwritten veneer file exists, +// and exports all the symbols in the list. +// +// TODO generate code such that tsc enforces type compatibility between raw and veneer decls +export { + defaultDashboard, + defaultPanel, + defaultFieldConfigSource, + defaultFieldConfig +} from './veneer/dashboard.types'; diff --git a/packages/grafana-schema/src/index.ts b/packages/grafana-schema/src/index.ts index 4ba5c03ffa4..d8a77de839d 100644 --- a/packages/grafana-schema/src/index.ts +++ b/packages/grafana-schema/src/index.ts @@ -4,3 +4,4 @@ * @packageDocumentation */ export * from './schema/mudball.gen'; +export * from './index.gen'; diff --git a/packages/grafana-schema/src/raw/dashboard/x/dashboard.gen.ts b/packages/grafana-schema/src/raw/dashboard/x/dashboard.gen.ts new file mode 100644 index 00000000000..267093da15c --- /dev/null +++ b/packages/grafana-schema/src/raw/dashboard/x/dashboard.gen.ts @@ -0,0 +1,676 @@ +// This file is autogenerated. DO NOT EDIT. +// +// Generated by pkg/framework/coremodel/gen.go +// +// Derived from the Thema lineage declared in pkg/coremodel/dashboard/coremodel.cue +// +// Run `make gen-cue` from repository root to regenerate. + +/** + * TODO docs + * FROM: AnnotationQuery in grafana-data/src/types/annotations.ts + */ +export interface AnnotationQuery { + builtIn: number; + /** + * Datasource to use for annotation. + */ + datasource: { + type?: string; + uid?: string; + }; + /** + * Whether annotation is enabled. + */ + enable: boolean; + /** + * Whether to hide annotation. + */ + hide?: boolean; + /** + * Annotation icon color. + */ + iconColor?: string; + /** + * Name of annotation. + */ + name?: string; + /** + * Query for annotation data. + */ + rawQuery?: string; + showIn: number; + target?: Record; + type: string; +} + +export const defaultAnnotationQuery: Partial = { + builtIn: 0, + enable: true, + hide: false, + showIn: 0, + type: 'dashboard', +}; + +/** + * FROM: packages/grafana-data/src/types/templateVars.ts + * TODO docs + * TODO what about what's in public/app/features/types.ts? + * TODO there appear to be a lot of different kinds of [template] vars here? if so need a disjunction + */ +export interface VariableModel { + label?: string; + name: string; + type: VariableType; +} + +/** + * FROM public/app/features/dashboard/state/DashboardModels.ts - ish + * TODO docs + */ +export interface DashboardLink { + asDropdown: boolean; + icon?: string; + includeVars: boolean; + keepTime: boolean; + tags: Array; + targetBlank: boolean; + title: string; + tooltip?: string; + type: DashboardLinkType; + url?: string; +} + +export const defaultDashboardLink: Partial = { + asDropdown: false, + includeVars: false, + keepTime: false, + tags: [], + targetBlank: false, +}; + +/** + * TODO docs + */ +export type DashboardLinkType = ('link' | 'dashboards'); + +/** + * FROM: packages/grafana-data/src/types/templateVars.ts + * TODO docs + * TODO this implies some wider pattern/discriminated union, probably? + */ +export type VariableType = ('query' | 'adhoc' | 'constant' | 'datasource' | 'interval' | 'textbox' | 'custom' | 'system'); + +/** + * TODO docs + */ +export enum FieldColorModeId { + ContinuousGrYlRd = 'continuous-GrYlRd', + Fixed = 'fixed', + PaletteClassic = 'palette-classic', + PaletteSaturated = 'palette-saturated', + Thresholds = 'thresholds', +} + +/** + * TODO docs + */ +export type FieldColorSeriesByMode = ('min' | 'max' | 'last'); + +/** + * TODO docs + */ +export interface FieldColor { + /** + * Stores the fixed color value if mode is fixed + */ + fixedColor?: string; + /** + * The main color scheme mode + */ + mode: FieldColorModeId; + /** + * Some visualizations need to know how to assign a series color from by value color schemes + */ + seriesBy?: FieldColorSeriesByMode; +} + +export interface GridPos { + /** + * Panel + */ + h: number; + /** + * true if fixed + */ + static?: boolean; + /** + * Panel + */ + w: number; + /** + * Panel x + */ + x: number; + /** + * Panel y + */ + y: number; +} + +export const defaultGridPos: Partial = { + h: 9, + w: 12, + x: 0, + y: 0, +}; + +/** + * TODO docs + */ +export interface Threshold { + /** + * 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; + /** + * TODO docs + * FIXME the corresponding typescript field is required/non-optional, but nulls currently appear here when serializing -Infinity to JSON + */ + value?: number; +} + +export enum ThresholdsMode { + Absolute = 'absolute', + Percentage = 'percentage', +} + +export interface ThresholdsConfig { + mode: ThresholdsMode; + /** + * Must be sorted by 'value', first value is always -Infinity + */ + steps: Array; +} + +export const defaultThresholdsConfig: Partial = { + steps: [], +}; + +/** + * TODO docs + */ +export type ValueMapping = (ValueMap | RangeMap | RegexMap | SpecialValueMap); + +/** + * TODO docs + */ +export enum MappingType { + RangeToText = 'range', + RegexToText = 'regex', + SpecialValue = 'special', + ValueToText = 'value', +} + +/** + * TODO docs + */ +export interface ValueMap { + options: Record; + type: MappingType.ValueToText; +} + +/** + * TODO docs + */ +export interface RangeMap { + options: { + /** + * to and from are `number | null` in current ts, really not sure what to do + */ + from: number; + to: number; + result: ValueMappingResult; + }; + type: MappingType.RangeToText; +} + +/** + * TODO docs + */ +export interface RegexMap { + options: { + pattern: string; + result: ValueMappingResult; + }; + type: MappingType.RegexToText; +} + +/** + * TODO docs + */ +export interface SpecialValueMap { + options: { + match: ('true' | 'false'); + pattern: string; + result: ValueMappingResult; + }; + type: MappingType.SpecialValue; +} + +/** + * TODO docs + */ +export enum SpecialValueMatch { + Empty = 'empty', + False = 'false', + NaN = 'nan', + Null = 'null', + NullAndNan = 'null+nan', + True = 'true', +} + +/** + * TODO docs + */ +export interface ValueMappingResult { + color?: string; + icon?: string; + index?: number; + text?: string; +} + +/** + * TODO docs + * FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it + */ +export interface Transformation { + id: string; + options: Record; +} + +/** + * 0 for no shared crosshair or tooltip (default). + * 1 for shared crosshair. + * 2 for shared crosshair AND shared tooltip. + */ +export enum DashboardCursorSync { + Crosshair = 1, + Off = 0, + Tooltip = 2, +} + +export const defaultDashboardCursorSync: DashboardCursorSync = DashboardCursorSync.Off; + +/** + * Dashboard panels. Panels are canonically defined inline + * because they share a version timeline with the dashboard + * schema; they do not evolve independently. + */ +export interface Panel { + /** + * The datasource used in all targets. + */ + datasource?: { + type?: string; + uid?: string; + }; + /** + * Description. + */ + description?: string; + fieldConfig: FieldConfigSource; + /** + * Grid position. + */ + gridPos?: GridPos; + /** + * TODO docs + */ + id?: number; + /** + * TODO docs + * TODO tighter constraint + */ + interval?: string; + /** + * Panel links. + * TODO fill this out - seems there are a couple variants? + */ + links?: Array; + /** + * TODO docs + */ + maxDataPoints?: number; + /** + * options is specified by the PanelOptions field in panel + * plugin schemas. + */ + options: Record; + /** + * FIXME this almost certainly has to be changed in favor of scuemata versions + */ + pluginVersion?: string; + /** + * 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 + */ + tags?: Array; + /** + * TODO docs + */ + targets?: Array>; + /** + * TODO docs - seems to be an old field from old dashboard alerts? + */ + thresholds?: Array; + /** + * TODO docs + * TODO tighter constraint + */ + timeFrom?: string; + /** + * TODO docs + */ + timeRegions?: Array; + /** + * TODO docs + * TODO tighter constraint + */ + timeShift?: string; + /** + * Panel title. + */ + title?: string; + transformations: Array; + /** + * Whether to display the panel without a background. + */ + transparent: boolean; + /** + * The panel plugin type id. May not be empty. + */ + type: string; +} + +export const defaultPanel: Partial = { + links: [], + repeatDirection: 'h', + tags: [], + targets: [], + thresholds: [], + timeRegions: [], + transformations: [], + transparent: false, +}; + +export interface FieldConfigSource { + defaults: FieldConfig; + overrides: Array<{ + matcher: MatcherConfig; + properties: Array<{ + id: string; + value?: any; + }>; + }>; +} + +export const defaultFieldConfigSource: Partial = { + overrides: [], +}; + +export interface MatcherConfig { + id: string; + options?: any; +} + +export const defaultMatcherConfig: Partial = { + id: '', +}; + +export interface FieldConfig { + /** + * Map values to a display color + */ + color?: FieldColor; + /** + * custom is specified by the PanelFieldConfig field + * in panel plugin schemas. + */ + custom?: Record; + /** + * Significant digits (for display) + */ + decimals?: number; + /** + * Human readable field metadata + */ + description?: string; + /** + * 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; + /** + * True if data source field supports ad-hoc filters + */ + filterable?: boolean; + /** + * The behavior when clicking on a result + */ + links?: Array; + /** + * Convert input values into a display string + */ + mappings?: Array; + max?: number; + min?: number; + /** + * Alternative to empty string + */ + noValue?: 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; + /** + * Map numeric values to states + */ + thresholds?: ThresholdsConfig; + /** + * Numeric Options + */ + unit?: string; + /** + * True if data source can write a value to the path. Auth/authz are supported separately + */ + writeable?: boolean; +} + +export const defaultFieldConfig: Partial = { + links: [], + mappings: [], +}; + +/** + * Row panel + */ +export interface RowPanel { + collapsed: boolean; + /** + * Name of default datasource. + */ + datasource?: { + type?: string; + uid?: string; + }; + gridPos?: GridPos; + id: number; + panels: Array<(Panel | { + type: 'graph'; + } | { + type: 'heatmap'; + })>; + /** + * Name of template variable to repeat for. + */ + repeat?: string; + title?: string; + type: 'row'; +} + +export const defaultRowPanel: Partial = { + collapsed: false, + panels: [], +}; + +export interface Dashboard { + /** + * TODO docs + */ + annotations?: { + list: Array; + }; + /** + * Description of dashboard. + */ + description?: string; + /** + * Whether a dashboard is editable or not. + */ + editable: boolean; + /** + * TODO docs + */ + fiscalYearStartMonth?: number; + gnetId?: string; + graphTooltip: DashboardCursorSync; + /** + * Unique numeric identifier for the dashboard. + * TODO must isolate or remove identifiers local to a Grafana instance...? + */ + id?: number; + /** + * TODO docs + */ + links?: Array; + /** + * TODO docs + */ + liveNow?: boolean; + panels?: Array<(Panel | RowPanel | { + type: 'graph'; + } | { + type: 'heatmap'; + })>; + /** + * TODO docs + */ + refresh?: (string | false); + /** + * Version of the JSON schema, incremented each time a Grafana update brings + * changes to said schema. + * TODO this is the existing schema numbering system. It will be replaced by Thema's themaVersion + */ + schemaVersion: number; + /** + * Theme of dashboard. + */ + style: ('light' | 'dark'); + /** + * Tags associated with dashboard. + */ + tags?: Array; + /** + * TODO docs + */ + templating?: { + list: Array; + }; + /** + * Time range for dashboard, e.g. last 6 hours, last 7 days, etc + */ + time?: { + from: string; + to: string; + }; + /** + * TODO docs + * TODO this appears to be spread all over in the frontend. Concepts will likely need tidying in tandem with schema changes + */ + timepicker?: { + /** + * Whether timepicker is collapsed or not. + */ + collapse: boolean; + /** + * Whether timepicker is enabled or not. + */ + enable: boolean; + /** + * Whether timepicker is visible or not. + */ + hidden: boolean; + /** + * Selectable intervals for auto-refresh. + */ + refresh_intervals: Array; + /** + * TODO docs + */ + time_options: Array; + }; + /** + * Timezone of dashboard, + */ + timezone?: ('browser' | 'utc' | ''); + /** + * Title of dashboard. + */ + title?: string; + /** + * Unique dashboard identifier that can be generated by anyone. string (8-40) + */ + uid?: string; + /** + * Version of the dashboard, incremented each time the dashboard is updated. + */ + version?: number; + /** + * TODO docs + */ + weekStart?: string; +} + +export const defaultDashboard: Partial = { + editable: true, + graphTooltip: DashboardCursorSync.Off, + links: [], + panels: [], + schemaVersion: 36, + style: 'dark', + tags: [], + timezone: 'browser', +}; diff --git a/packages/grafana-schema/src/schema/dashboard/dashboard_experimental.gen.ts b/packages/grafana-schema/src/schema/dashboard/dashboard_experimental.gen.ts deleted file mode 100644 index 6ae3b7b6769..00000000000 --- a/packages/grafana-schema/src/schema/dashboard/dashboard_experimental.gen.ts +++ /dev/null @@ -1,274 +0,0 @@ -// This file is autogenerated. DO NOT EDIT. -// -// Generated by pkg/framework/coremodel/gen.go -// -// Derived from the Thema lineage declared in pkg/coremodel/dashboard/coremodel.cue -// -// Run `make gen-cue` from repository root to regenerate. - - -// This model is a WIP and not yet canonical. Consequently, its members are -// not exported to exclude it from grafana-schema's public API surface. - -interface AnnotationQuery { - builtIn: number; - datasource: { - type?: string; - uid?: string; - }; - enable: boolean; - hide?: boolean; - iconColor?: string; - name?: string; - rawQuery?: string; - showIn: number; - target?: {}; - type: string; -} - -const defaultAnnotationQuery: Partial = { - builtIn: 0, - enable: true, - hide: false, - showIn: 0, - type: 'dashboard', -}; - -interface VariableModel { - label?: string; - name: string; - type: VariableType; -} - -interface DashboardLink { - asDropdown: boolean; - icon?: string; - includeVars: boolean; - keepTime: boolean; - tags: string[]; - targetBlank: boolean; - title: string; - tooltip?: string; - type: DashboardLinkType; - url?: string; -} - -const defaultDashboardLink: Partial = { - asDropdown: false, - includeVars: false, - keepTime: false, - tags: [], - targetBlank: false, -}; - -type DashboardLinkType = ('link' | 'dashboards'); - -type VariableType = ('query' | 'adhoc' | 'constant' | 'datasource' | 'interval' | 'textbox' | 'custom' | 'system'); - -enum FieldColorModeId { - ContinuousGrYlRd = 'continuous-GrYlRd', - Fixed = 'fixed', - PaletteClassic = 'palette-classic', - PaletteSaturated = 'palette-saturated', - Thresholds = 'thresholds', -} - -type FieldColorSeriesByMode = ('min' | 'max' | 'last'); - -interface FieldColor { - fixedColor?: string; - mode: (FieldColorModeId | string); - seriesBy?: FieldColorSeriesByMode; -} - -interface GridPos { - h: number; - static?: boolean; - w: number; - x: number; - y: number; -} - -const defaultGridPos: Partial = { - h: 9, - w: 12, - x: 0, - y: 0, -}; - -interface Threshold { - color: string; - state?: string; - value?: number; -} - -enum ThresholdsMode { - Absolute = 'absolute', - Percentage = 'percentage', -} - -interface ThresholdsConfig { - mode: ThresholdsMode; - steps: Threshold[]; -} - -const defaultThresholdsConfig: Partial = { - steps: [], -}; - -interface Transformation { - id: string; - options: {}; -} - -enum DashboardCursorSync { - Crosshair = 1, - Off = 0, - Tooltip = 2, -} - -const defaultDashboardCursorSync: DashboardCursorSync = DashboardCursorSync.Off; - -interface Panel { - datasource?: { - type?: string; - uid?: string; - }; - description?: string; - fieldConfig: { - defaults: { - displayName?: string; - displayNameFromDS?: string; - description?: string; - path?: string; - writeable?: boolean; - filterable?: boolean; - unit?: string; - decimals?: number; - min?: number; - max?: number; - mappings?: {}[]; - thresholds?: ThresholdsConfig; - color?: FieldColor; - links?: any[]; - noValue?: string; - custom?: {}; - }; - overrides: { - matcher: { - id: string; - options?: any; - }; - properties: { - id: string; - value?: any; - }[]; - }[]; - }; - gridPos?: GridPos; - id?: number; - interval?: string; - links?: DashboardLink[]; - maxDataPoints?: number; - options: {}; - pluginVersion?: string; - repeat?: string; - repeatDirection: ('h' | 'v'); - tags?: string[]; - targets?: {}[]; - thresholds?: any[]; - timeFrom?: string; - timeRegions?: any[]; - timeShift?: string; - title?: string; - transformations: Transformation[]; - transparent: boolean; - type: string; -} - -const defaultPanel: Partial = { - links: [], - repeatDirection: 'h', - tags: [], - targets: [], - thresholds: [], - timeRegions: [], - transformations: [], - transparent: false, -}; - -interface RowPanel { - collapsed: boolean; - datasource?: { - type?: string; - uid?: string; - }; - gridPos?: GridPos; - id: number; - panels: (Panel | { - type: 'graph'; - } | { - type: 'heatmap'; - })[]; - repeat?: string; - title?: string; - type: 'row'; -} - -const defaultRowPanel: Partial = { - collapsed: false, - panels: [], -}; - -interface Dashboard { - annotations?: { - list: AnnotationQuery[]; - }; - description?: string; - editable: boolean; - fiscalYearStartMonth?: number; - gnetId?: string; - graphTooltip: DashboardCursorSync; - id?: number; - links?: DashboardLink[]; - liveNow?: boolean; - panels?: (Panel | RowPanel | { - type: 'graph'; - } | { - type: 'heatmap'; - })[]; - refresh?: (string | false); - schemaVersion: number; - style: ('light' | 'dark'); - tags?: string[]; - templating?: { - list: VariableModel[]; - }; - time?: { - from: string; - to: string; - }; - timepicker?: { - collapse: boolean; - enable: boolean; - hidden: boolean; - refresh_intervals: string[]; - time_options: string[]; - }; - timezone?: ('browser' | 'utc' | ''); - title?: string; - uid?: string; - version?: number; - weekStart?: string; -} - -const defaultDashboard: Partial = { - editable: true, - graphTooltip: DashboardCursorSync.Off, - links: [], - panels: [], - schemaVersion: 36, - style: 'dark', - tags: [], - timezone: 'browser', -}; diff --git a/packages/grafana-schema/src/schema/mudball.gen.ts b/packages/grafana-schema/src/schema/mudball.gen.ts index a9688bb097e..c890b686725 100644 --- a/packages/grafana-schema/src/schema/mudball.gen.ts +++ b/packages/grafana-schema/src/schema/mudball.gen.ts @@ -4,6 +4,9 @@ // To regenerate, run "make gen-cue" from the repository root. //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +/** + * TODO docs + */ export enum AxisPlacement { Auto = 'auto', Bottom = 'bottom', @@ -13,28 +16,43 @@ export enum AxisPlacement { Top = 'top', } +/** + * TODO docs + */ export enum AxisColorMode { Series = 'series', Text = 'text', } +/** + * TODO docs + */ export enum VisibilityMode { Always = 'always', Auto = 'auto', Never = 'never', } +/** + * TODO docs + */ export enum GraphDrawStyle { Bars = 'bars', Line = 'line', Points = 'points', } +/** + * TODO docs + */ export enum GraphTransform { Constant = 'constant', NegativeY = 'negative-Y', } +/** + * TODO docs + */ export enum LineInterpolation { Linear = 'linear', Smooth = 'smooth', @@ -42,6 +60,9 @@ export enum LineInterpolation { StepBefore = 'stepBefore', } +/** + * TODO docs + */ export enum ScaleDistribution { Linear = 'linear', Log = 'log', @@ -49,6 +70,9 @@ export enum ScaleDistribution { Symlog = 'symlog', } +/** + * TODO docs + */ export enum GraphGradientMode { Hue = 'hue', None = 'none', @@ -56,23 +80,35 @@ export enum GraphGradientMode { Scheme = 'scheme', } +/** + * TODO docs + */ export enum StackingMode { None = 'none', Normal = 'normal', Percent = 'percent', } +/** + * TODO docs + */ export enum BarAlignment { After = 1, Before = -1, Center = 0, } +/** + * TODO docs + */ export enum ScaleOrientation { Horizontal = 0, Vertical = 1, } +/** + * TODO docs + */ export enum ScaleDirection { Down = -1, Left = -1, @@ -80,8 +116,11 @@ export enum ScaleDirection { Up = 1, } +/** + * TODO docs + */ export interface LineStyle { - dash?: number[]; + dash?: Array; fill?: ('solid' | 'dash' | 'dot' | 'square'); } @@ -89,26 +128,43 @@ export const defaultLineStyle: Partial = { dash: [], }; +/** + * TODO docs + */ export interface LineConfig { lineColor?: string; lineInterpolation?: LineInterpolation; lineStyle?: LineStyle; lineWidth?: number; + /** + * Indicate if null values should be treated as gaps or connected. + * When the value is a number, it represents the maximum delta in the + * X axis that should be considered connected. For timeseries, this is milliseconds + */ spanNulls?: (boolean | number); } +/** + * TODO docs + */ export interface BarConfig { barAlignment?: BarAlignment; barMaxWidth?: number; barWidthFactor?: number; } +/** + * TODO docs + */ export interface FillConfig { fillBelowTo?: string; fillColor?: string; fillOpacity?: number; } +/** + * TODO docs + */ export interface PointsConfig { pointColor?: string; pointSize?: number; @@ -116,12 +172,18 @@ export interface PointsConfig { showPoints?: VisibilityMode; } +/** + * TODO docs + */ export interface ScaleDistributionConfig { linearThreshold?: number; log?: number; type: ScaleDistribution; } +/** + * TODO docs + */ export interface AxisConfig { axisCenteredZero?: boolean; axisColorMode?: AxisColorMode; @@ -134,25 +196,40 @@ export interface AxisConfig { scaleDistribution?: ScaleDistributionConfig; } +/** + * TODO docs + */ export interface HideSeriesConfig { legend: boolean; tooltip: boolean; viz: boolean; } +/** + * TODO docs + */ export interface StackingConfig { group?: string; mode?: StackingMode; } +/** + * TODO docs + */ export interface StackableFieldConfig { stacking?: StackingConfig; } +/** + * TODO docs + */ export interface HideableFieldConfig { hideFrom?: HideSeriesConfig; } +/** + * TODO docs + */ export enum GraphTresholdsStyleMode { Area = 'area', Line = 'line', @@ -161,32 +238,63 @@ export enum GraphTresholdsStyleMode { Series = 'series', } +/** + * TODO docs + */ export interface GraphThresholdsStyleConfig { mode: GraphTresholdsStyleMode; } +/** + * TODO docs + */ export type LegendPlacement = ('bottom' | 'right'); +/** + * TODO docs + * Note: "hidden" needs to remain as an option for plugins compatibility + */ export enum LegendDisplayMode { Hidden = 'hidden', List = 'list', Table = 'table', } +/** + * TODO docs + */ export interface TableSortByFieldState { desc?: boolean; displayName: string; } +/** + * TODO docs + */ export interface SingleStatBaseOptions extends OptionsWithTextFormatting { orientation: VizOrientation; reduceOptions: ReduceDataOptions; } +/** + * TODO docs + */ export interface ReduceDataOptions { - calcs: string[]; + /** + * When !values, pick one value for the whole field + */ + calcs: Array; + /** + * Which fields to show. By default this is only numeric fields + */ fields?: string; + /** + * if showing all values limit + */ limit?: number; + /** + * If true show each row value + */ values?: boolean; } @@ -194,49 +302,76 @@ export const defaultReduceDataOptions: Partial = { calcs: [], }; +/** + * TODO docs + */ export enum VizOrientation { Auto = 'auto', Horizontal = 'horizontal', Vertical = 'vertical', } +/** + * TODO docs + */ export interface OptionsWithTooltip { tooltip: VizTooltipOptions; } +/** + * TODO docs + */ export interface OptionsWithLegend { legend: VizLegendOptions; } +/** + * TODO docs + */ export interface OptionsWithTimezones { - timezone?: string[]; + timezone?: Array; } export const defaultOptionsWithTimezones: Partial = { timezone: [], }; +/** + * TODO docs + */ export interface OptionsWithTextFormatting { text?: VizTextDisplayOptions; } +/** + * TODO docs + */ export enum BigValueColorMode { Background = 'background', None = 'none', Value = 'value', } +/** + * TODO docs + */ export enum BigValueGraphMode { Area = 'area', Line = 'line', None = 'none', } +/** + * TODO docs + */ export enum BigValueJustifyMode { Auto = 'auto', Center = 'center', } +/** + * TODO docs + */ export enum BigValueTextMode { Auto = 'auto', Name = 'name', @@ -245,8 +380,15 @@ export enum BigValueTextMode { ValueAndName = 'value_and_name', } +/** + * TODO -- should not be table specific! + * TODO docs + */ export type FieldTextAlignment = ('auto' | 'left' | 'right' | 'center'); +/** + * TODO docs + */ export enum TableCellDisplayMode { Auto = 'auto', BasicGauge = 'basic', @@ -259,23 +401,41 @@ export enum TableCellDisplayMode { LcdGauge = 'lcd-gauge', } +/** + * TODO docs + */ export interface VizTextDisplayOptions { + /** + * Explicit title text size + */ titleSize?: number; + /** + * Explicit value text size + */ valueSize?: number; } +/** + * TODO docs + */ export enum TooltipDisplayMode { Multi = 'multi', None = 'none', Single = 'single', } +/** + * TODO docs + */ export enum SortOrder { Ascending = 'asc', Descending = 'desc', None = 'none', } +/** + * TODO docs + */ export interface GraphFieldConfig extends LineConfig, FillConfig, PointsConfig, AxisConfig, BarConfig, StackableFieldConfig, HideableFieldConfig { drawStyle?: GraphDrawStyle; gradientMode?: GraphGradientMode; @@ -283,9 +443,12 @@ export interface GraphFieldConfig extends LineConfig, FillConfig, PointsConfig, transform?: GraphTransform; } +/** + * TODO docs + */ export interface VizLegendOptions { asTable?: boolean; - calcs: string[]; + calcs: Array; displayMode: LegendDisplayMode; isVisible?: boolean; placement: LegendPlacement; @@ -299,12 +462,18 @@ export const defaultVizLegendOptions: Partial = { calcs: [], }; +/** + * TODO docs + */ export enum BarGaugeDisplayMode { Basic = 'basic', Gradient = 'gradient', Lcd = 'lcd', } +/** + * TODO docs + */ export interface TableFieldOptions { align: FieldTextAlignment; displayMode: TableCellDisplayMode; @@ -321,6 +490,9 @@ export const defaultTableFieldOptions: Partial = { inspect: false, }; +/** + * TODO docs + */ export interface VizTooltipOptions { mode: TooltipDisplayMode; sort: SortOrder; diff --git a/packages/grafana-schema/src/veneer/dashboard.types.ts b/packages/grafana-schema/src/veneer/dashboard.types.ts new file mode 100644 index 00000000000..4dc0c1b0297 --- /dev/null +++ b/packages/grafana-schema/src/veneer/dashboard.types.ts @@ -0,0 +1,32 @@ +import * as raw from '../raw/dashboard/x/dashboard.gen'; + +export interface Dashboard extends raw.Dashboard { + panels?: Array< + | Panel + | raw.RowPanel + | { + type: 'graph'; + } + | { + type: 'heatmap'; + } + >; +} + +export interface Panel, TCustomFieldConfig = Record> + extends raw.Panel { + fieldConfig: FieldConfigSource; +} + +export interface FieldConfig> extends raw.FieldConfig { + custom?: TOptions & Record; +} + +export interface FieldConfigSource> extends raw.FieldConfigSource { + defaults: FieldConfig; +} + +export const defaultDashboard: Partial = raw.defaultDashboard; +export const defaultPanel: Partial = raw.defaultPanel; +export const defaultFieldConfig: Partial = raw.defaultFieldConfig; +export const defaultFieldConfigSource: Partial = raw.defaultFieldConfigSource; diff --git a/pkg/codegen/coremodel.go b/pkg/codegen/coremodel.go index 55e276deb5b..07676ef1a24 100644 --- a/pkg/codegen/coremodel.go +++ b/pkg/codegen/coremodel.go @@ -17,14 +17,15 @@ import ( "github.com/deepmap/oapi-codegen/pkg/codegen" "github.com/getkin/kin-openapi/openapi3" "github.com/grafana/cuetsy" + tsast "github.com/grafana/cuetsy/ts/ast" "github.com/grafana/grafana/pkg/cuectx" "github.com/grafana/thema" "github.com/grafana/thema/encoding/openapi" ) -// ExtractedLineage contains the results of statically analyzing a Grafana +// CoremodelDeclaration contains the results of statically analyzing a Grafana // directory for a Thema lineage. -type ExtractedLineage struct { +type CoremodelDeclaration struct { Lineage thema.Lineage // Absolute path to the coremodel's coremodel.cue file. LineagePath string @@ -48,12 +49,12 @@ type ExtractedLineage struct { // This loading approach is intended primarily for use with code generators, or // other use cases external to grafana-server backend. For code within // grafana-server, prefer lineage loaders provided in e.g. pkg/coremodel/*. -func ExtractLineage(path string, lib thema.Library) (*ExtractedLineage, error) { +func ExtractLineage(path string, lib thema.Library) (*CoremodelDeclaration, error) { if !filepath.IsAbs(path) { return nil, fmt.Errorf("must provide an absolute path, got %q", path) } - ec := &ExtractedLineage{ + ec := &CoremodelDeclaration{ LineagePath: path, } @@ -107,14 +108,14 @@ func ExtractLineage(path string, lib thema.Library) (*ExtractedLineage, error) { } // toTemplateObj extracts creates a struct with all the useful strings for template generation. -func (ls *ExtractedLineage) toTemplateObj() tplVars { - lin := ls.Lineage +func (cd *CoremodelDeclaration) toTemplateObj() tplVars { + lin := cd.Lineage sch := thema.SchemaP(lin, thema.LatestVersion(lin)) return tplVars{ Name: lin.Name(), - LineagePath: ls.RelativePath, - PkgPath: filepath.ToSlash(filepath.Join("github.com/grafana/grafana", filepath.Dir(ls.RelativePath))), + LineagePath: cd.RelativePath, + PkgPath: filepath.ToSlash(filepath.Join("github.com/grafana/grafana", filepath.Dir(cd.RelativePath))), TitleName: strings.Title(lin.Name()), // nolint LatestSeqv: sch.Version()[0], LatestSchv: sch.Version()[1], @@ -139,13 +140,22 @@ var nonAPITypes = map[string]bool{ "pluginmeta": true, } +// PathVersion returns the string path element to use for the latest schema. +// "x" if not yet canonical, otherwise, "v" +func (cd *CoremodelDeclaration) PathVersion() string { + if !cd.IsCanonical { + return "x" + } + return fmt.Sprintf("v%v", thema.LatestVersion(cd.Lineage)[0]) +} + // GenerateGoCoremodel generates a standard Go model struct and coremodel // implementation from a coremodel CUE declaration. // // The provided path must be a directory. Generated code files will be written // to that path. The final element of the path must match the Lineage.Name(). -func (ls *ExtractedLineage) GenerateGoCoremodel(path string) (WriteDiffer, error) { - lin, lib := ls.Lineage, ls.Lineage.Library() +func (cd *CoremodelDeclaration) GenerateGoCoremodel(path string) (WriteDiffer, error) { + lin, lib := cd.Lineage, cd.Lineage.Library() _, name := filepath.Split(path) if name != lin.Name() { return nil, fmt.Errorf("lineage name %q must match final element of path, got %q", lin.Name(), path) @@ -190,7 +200,7 @@ func (ls *ExtractedLineage) GenerateGoCoremodel(path string) (WriteDiffer, error buf := new(bytes.Buffer) if err = tmpls.Lookup("autogen_header.tmpl").Execute(buf, tvars_autogen_header{ - LineagePath: ls.RelativePath, + LineagePath: cd.RelativePath, GeneratorPath: "pkg/framework/coremodel/gen.go", // FIXME hardcoding is not OK }); err != nil { return nil, fmt.Errorf("error executing header template: %w", err) @@ -198,7 +208,7 @@ func (ls *ExtractedLineage) GenerateGoCoremodel(path string) (WriteDiffer, error fmt.Fprint(buf, "\n", gostr) - vars := ls.toTemplateObj() + vars := cd.toTemplateObj() err = tmpls.Lookup("addenda.tmpl").Execute(buf, vars) if err != nil { panic(err) @@ -228,59 +238,38 @@ type tplVars struct { IsComposed bool } -func (ls *ExtractedLineage) GenerateTypescriptCoremodel(path string) (WriteDiffer, error) { - _, name := filepath.Split(path) - if name != ls.Lineage.Name() { - return nil, fmt.Errorf("lineage name %q must match final element of path, got %q", ls.Lineage.Name(), path) - } +func (cd *CoremodelDeclaration) GenerateTypescriptCoremodel() (*tsast.File, error) { + schv := thema.SchemaP(cd.Lineage, thema.LatestVersion(cd.Lineage)).UnwrapCUE() - schv := thema.SchemaP(ls.Lineage, thema.LatestVersion(ls.Lineage)).UnwrapCUE() - - parts, err := cuetsy.GenerateAST(schv, cuetsy.Config{}) + tf, err := cuetsy.GenerateAST(schv, cuetsy.Config{ + Export: true, + }) if err != nil { - return nil, fmt.Errorf("cuetsy parts gen failed: %w", err) + return nil, fmt.Errorf("cuetsy tf gen failed: %w", err) } - top, err := cuetsy.GenerateSingleAST(strings.Title(ls.Lineage.Name()), schv, cuetsy.TypeInterface) + top, err := cuetsy.GenerateSingleAST(strings.Title(cd.Lineage.Name()), schv, cuetsy.TypeInterface) if err != nil { return nil, fmt.Errorf("cuetsy top gen failed: %s", cerrors.Details(err, nil)) } - // TODO until cuetsy can toposort its outputs, put the top/parent type at the bottom of the file. - parts.Nodes = append(parts.Nodes, top.T) - if top.D != nil { - parts.Nodes = append(parts.Nodes, top.D) - } - - var strb strings.Builder - var str string - fpath := ls.Lineage.Name() + ".gen.ts" - if err := tmpls.Lookup("autogen_header.tmpl").Execute(&strb, tvars_autogen_header{ - LineagePath: ls.RelativePath, + buf := new(bytes.Buffer) + if err := tmpls.Lookup("autogen_header.tmpl").Execute(buf, tvars_autogen_header{ + LineagePath: cd.RelativePath, GeneratorPath: "pkg/framework/coremodel/gen.go", // FIXME hardcoding is not OK }); err != nil { return nil, fmt.Errorf("error executing header template: %w", err) } - - if !ls.IsCanonical { - fpath = fmt.Sprintf("%s_experimental.gen.ts", ls.Lineage.Name()) - strb.WriteString(` -// This model is a WIP and not yet canonical. Consequently, its members are -// not exported to exclude it from grafana-schema's public API surface. - -`) - strb.WriteString(fmt.Sprint(parts)) - // TODO replace this regexp with cuetsy config for whether members are exported - re := regexp.MustCompile(`(?m)^export `) - str = re.ReplaceAllLiteralString(strb.String(), "") - } else { - strb.WriteString(fmt.Sprint(parts)) - str = strb.String() + tf.Doc = &tsast.Comment{ + Text: buf.String(), } - wd := NewWriteDiffer() - wd[filepath.Join(path, fpath)] = []byte(str) - return wd, nil + // TODO until cuetsy can toposort its outputs, put the top/parent type at the bottom of the file. + tf.Nodes = append(tf.Nodes, top.T) + if top.D != nil { + tf.Nodes = append(tf.Nodes, top.D) + } + return tf, nil } type prefixDropper struct { @@ -319,7 +308,7 @@ func (d prefixDropper) Visit(n ast.Node) ast.Visitor { // GenerateCoremodelRegistry produces Go files that define a registry with // references to all the Go code that is expected to be generated from the // provided lineages. -func GenerateCoremodelRegistry(path string, ecl []*ExtractedLineage) (WriteDiffer, error) { +func GenerateCoremodelRegistry(path string, ecl []*CoremodelDeclaration) (WriteDiffer, error) { var cml []tplVars for _, ec := range ecl { cml = append(cml, ec.toTemplateObj()) diff --git a/pkg/codegen/pluggen.go b/pkg/codegen/pluggen.go index 91be7911a69..1f6c8bba4c6 100644 --- a/pkg/codegen/pluggen.go +++ b/pkg/codegen/pluggen.go @@ -14,6 +14,7 @@ import ( "github.com/deepmap/oapi-codegen/pkg/codegen" "github.com/getkin/kin-openapi/openapi3" "github.com/grafana/cuetsy" + tsast "github.com/grafana/cuetsy/ts/ast" "github.com/grafana/grafana/pkg/framework/coremodel" "github.com/grafana/grafana/pkg/plugins/pfs" "github.com/grafana/thema" @@ -101,15 +102,22 @@ type PluginTreeOrErr struct { // It is, for now, tailored specifically to Grafana core's codegen needs. type PluginTree pfs.Tree -func (pt *PluginTree) GenerateTS(path string) (WriteDiffer, error) { +func (pt *PluginTree) GenerateTypeScriptAST() (*tsast.File, error) { t := (*pfs.Tree)(pt) + f := &tsast.File{} - // TODO replace with cuetsy's TS AST - f := &tvars_cuetsy_multi{ - Header: tvars_autogen_header{ - GeneratorPath: "public/app/plugins/gen.go", // FIXME hardcoding is not OK - LineagePath: "models.cue", - }, + tf := tvars_autogen_header{ + GeneratorPath: "public/app/plugins/gen.go", // FIXME hardcoding is not OK + LineagePath: "models.cue", + } + var buf bytes.Buffer + err := tmpls.Lookup("autogen_header.tmpl").Execute(&buf, tf) + if err != nil { + return nil, fmt.Errorf("error executing header template: %w", err) + } + + f.Doc = &tsast.Comment{ + Text: buf.String(), } pi := t.RootPlugin() @@ -118,7 +126,9 @@ func (pt *PluginTree) GenerateTS(path string) (WriteDiffer, error) { return nil, nil } for _, im := range pi.CUEImports() { - if tsim := convertImport(im); tsim != nil { + if tsim, err := convertImport(im); err != nil { + return nil, err + } else if tsim.From.Value != "" { f.Imports = append(f.Imports, tsim) } } @@ -126,37 +136,36 @@ func (pt *PluginTree) GenerateTS(path string) (WriteDiffer, error) { for slotname, lin := range slotimps { v := thema.LatestVersion(lin) sch := thema.SchemaP(lin, v) - // TODO need call expressions in cuetsy tsast to be able to do these - sec := tsSection{ - V: v, - ModelName: slotname, - } + // Inject a node for the const with the version + f.Nodes = append(f.Nodes, tsast.Raw{ + // TODO need call expressions in cuetsy tsast to be able to do these properly + Data: fmt.Sprintf("export const %sModelVersion = Object.freeze([%v, %v]);", slotname, v[0], v[1]), + }) + // TODO this is hardcoded for now, but should ultimately be a property of + // whether the slot is a grouped lineage: + // https://github.com/grafana/thema/issues/62 if isGroupLineage(slotname) { - b, err := cuetsy.Generate(sch.UnwrapCUE(), cuetsy.Config{}) + tsf, err := cuetsy.GenerateAST(sch.UnwrapCUE(), cuetsy.Config{ + Export: true, + }) if err != nil { - return nil, fmt.Errorf("%s: error translating %s lineage to TypeScript: %w", path, slotname, err) + return nil, fmt.Errorf("error translating %s lineage to TypeScript: %w", slotname, err) } - sec.Body = string(b) + f.Nodes = append(f.Nodes, tsf.Nodes...) } else { - a, err := cuetsy.GenerateSingleAST(strings.Title(lin.Name()), sch.UnwrapCUE(), cuetsy.TypeInterface) + pair, err := cuetsy.GenerateSingleAST(strings.Title(lin.Name()), sch.UnwrapCUE(), cuetsy.TypeInterface) if err != nil { - return nil, fmt.Errorf("%s: error translating %s lineage to TypeScript: %w", path, slotname, err) + return nil, fmt.Errorf("error translating %s lineage to TypeScript: %w", slotname, err) + } + f.Nodes = append(f.Nodes, pair.T) + if pair.D != nil { + f.Nodes = append(f.Nodes, pair.D) } - sec.Body = fmt.Sprint(a) } - - f.Sections = append(f.Sections, sec) } - wd := NewWriteDiffer() - var buf bytes.Buffer - err := tmpls.Lookup("cuetsy_multi.tmpl").Execute(&buf, f) - if err != nil { - return nil, fmt.Errorf("%s: error executing plugin TS generator template: %w", path, err) - } - wd[filepath.Join(path, "models.gen.ts")] = buf.Bytes() - return wd, nil + return f, nil } func isGroupLineage(slotname string) bool { @@ -414,41 +423,27 @@ type TreeAndPath struct { } // TODO convert this to use cuetsy ts types, once import * form is supported -func convertImport(im *ast.ImportSpec) *tsImport { - var err error - tsim := &tsImport{} - tsim.Pkg, err = MapCUEImportToTS(strings.Trim(im.Path.Value, "\"")) - if err != nil { - // should be unreachable if paths has been verified already - panic(err) +func convertImport(im *ast.ImportSpec) (tsast.ImportSpec, error) { + tsim := tsast.ImportSpec{} + pkg, err := MapCUEImportToTS(strings.Trim(im.Path.Value, "\"")) + if err != nil || pkg == "" { + // err should be unreachable if paths has been verified already + // Empty string mapping means skip it + return tsim, err } - if tsim.Pkg == "" { - // Empty string mapping means skip it - return nil - } + tsim.From = tsast.Str{Value: pkg} if im.Name != nil && im.Name.String() != "" { - tsim.Ident = im.Name.String() + tsim.AsName = im.Name.String() } else { sl := strings.Split(im.Path.Value, "/") final := sl[len(sl)-1] if idx := strings.Index(final, ":"); idx != -1 { - tsim.Pkg = final[idx:] + tsim.AsName = final[idx:] } else { - tsim.Pkg = final + tsim.AsName = final } } - return tsim -} - -type tsSection struct { - V thema.SyntacticVersion - ModelName string - Body string -} - -type tsImport struct { - Ident string - Pkg string + return tsim, nil } diff --git a/pkg/codegen/tmpl.go b/pkg/codegen/tmpl.go index c2eac90d329..8c20a30eba9 100644 --- a/pkg/codegen/tmpl.go +++ b/pkg/codegen/tmpl.go @@ -1,6 +1,7 @@ package codegen import ( + "bytes" "embed" "text/template" "time" @@ -48,11 +49,6 @@ type ( SlotImpls []tvars_plugin_lineage_binding Header tvars_autogen_header } - tvars_cuetsy_multi struct { - Header tvars_autogen_header - Imports []*tsImport - Sections []tsSection - } tvars_plugin_registry struct { Header tvars_autogen_header Plugins []struct { @@ -63,3 +59,14 @@ type ( } } ) + +type HeaderVars = tvars_autogen_header + +// GenGrafanaHeader creates standard header elements for generated Grafana files. +func GenGrafanaHeader(vars HeaderVars) string { + buf := new(bytes.Buffer) + if err := tmpls.Lookup("autogen_header.tmpl").Execute(buf, vars); err != nil { + panic(err) + } + return buf.String() +} diff --git a/pkg/codegen/tmpl/cuetsy_multi.tmpl b/pkg/codegen/tmpl/cuetsy_multi.tmpl index 2aebe8d5e9b..686a8e55509 100644 --- a/pkg/codegen/tmpl/cuetsy_multi.tmpl +++ b/pkg/codegen/tmpl/cuetsy_multi.tmpl @@ -1,7 +1,2 @@ {{ template "autogen_header.tmpl" .Header -}} -{{range .Imports}} -import * as {{.Ident}} from '{{.Pkg}}';{{end}} -{{range .Sections}}{{if ne .ModelName "" }} -export const {{.ModelName}}ModelVersion = Object.freeze([{{index .V 0}}, {{index .V 1}}]); -{{end}} -{{.Body}}{{end}} +{{ .Body }} diff --git a/pkg/coremodel/dashboard/coremodel.cue b/pkg/coremodel/dashboard/coremodel.cue index 258e4868421..c6ff050c2fb 100644 --- a/pkg/coremodel/dashboard/coremodel.cue +++ b/pkg/coremodel/dashboard/coremodel.cue @@ -12,6 +12,8 @@ seqs: [ { schemas: [ {// 0.0 + @grafana(TSVeneer="type") + // Unique numeric identifier for the dashboard. // TODO must isolate or remove identifiers local to a Grafana instance...? id?: int64 @@ -192,6 +194,59 @@ seqs: [ steps: [...#Threshold] @reviewme() } @cuetsy(kind="interface") @reviewme() + // TODO docs + #ValueMapping: #ValueMap | #RangeMap | #RegexMap | #SpecialValueMap @cuetsy(kind="type") @reviewme() + + // TODO docs + #MappingType: "value" | "range" | "regex" | "special" @cuetsy(kind="enum",memberNames="ValueToText|RangeToText|RegexToText|SpecialValue") @reviewme() + + // TODO docs + #ValueMap: { + type: #MappingType & "value" + options: [string]: #ValueMappingResult + } @cuetsy(kind="interface") + + // TODO docs + #RangeMap: { + type: #MappingType & "range" + options: { + // to and from are `number | null` in current ts, really not sure what to do + from: int32 @reviewme() + to: int32 @reviewme() + result: #ValueMappingResult + } + } @cuetsy(kind="interface") @reviewme() + + // TODO docs + #RegexMap: { + type: #MappingType & "regex" + options: { + pattern: string + result: #ValueMappingResult + } + } @cuetsy(kind="interface") @reviewme() + + // TODO docs + #SpecialValueMap: { + type: #MappingType & "special" + options: { + match: "true" | "false" + pattern: string + result: #ValueMappingResult + } + } @cuetsy(kind="interface") @reviewme() + + // TODO docs + #SpecialValueMatch: "true" | "false" | "null" | "nan" | "null+nan" | "empty" @cuetsy(kind="enum",memberNames="True|False|Null|NaN|NullAndNan|Empty") + + // TODO docs + #ValueMappingResult: { + text?: string + color?: string + icon?: string + index?: int32 + } @cuetsy(kind="interface") + // TODO docs // FIXME this is extremely underspecfied; wasn't obvious which typescript types corresponded to it #Transformation: { @@ -282,82 +337,82 @@ seqs: [ // plugin schemas. options: {...} @reviewme() - fieldConfig: { - defaults: { - // The display value for this field. This supports template variables blank is auto - displayName?: string @reviewme() + fieldConfig: #FieldConfigSource + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @reviewme() - // 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 @reviewme() + #FieldConfigSource: { + defaults: #FieldConfig + overrides: [...{ + matcher: #MatcherConfig + properties: [...#DynamicConfigValue] + }] @reviewme() + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @reviewme() - // Human readable field metadata - description?: string @reviewme() + #MatcherConfig: { + id: string | *"" @reviewme() + options?: _ @reviewme() + } @cuetsy(kind="interface") - // 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 @reviewme() + #DynamicConfigValue: { + id: string | *"" @reviewme() + value?: _ @reviewme() + } - // True if data source can write a value to the path. Auth/authz are supported separately - writeable?: bool @reviewme() + #FieldConfig: { + // The display value for this field. This supports template variables blank is auto + displayName?: string @reviewme() - // True if data source field supports ad-hoc filters - filterable?: bool @reviewme() + // 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 @reviewme() - // Numeric Options - unit?: string @reviewme() + // Human readable field metadata + description?: string @reviewme() - // Significant digits (for display) - decimals?: number @reviewme() + // 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 @reviewme() - min?: number @reviewme() - max?: number @reviewme() + // True if data source can write a value to the path. Auth/authz are supported separately + writeable?: bool @reviewme() - // 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?: [...{...}] @reviewme() + // True if data source field supports ad-hoc filters + filterable?: bool @reviewme() - // Map numeric values to states - thresholds?: #ThresholdsConfig @reviewme() + // Numeric Options + unit?: string @reviewme() - // // Map values to a display color - color?: #FieldColor @reviewme() + // Significant digits (for display) + decimals?: number @reviewme() - // // Used when reducing field values - // nullValueMode?: NullValueMode + min?: number @reviewme() + max?: number @reviewme() - // // The behavior when clicking on a result - links?: [...] @reviewme() + // Convert input values into a display string + mappings?: [...#ValueMapping] @reviewme() - // Alternative to empty string - noValue?: string @reviewme() + // Map numeric values to states + thresholds?: #ThresholdsConfig @reviewme() - // custom is specified by the PanelFieldConfig field - // in panel plugin schemas. - custom?: {...} @reviewme() - } @reviewme() - overrides: [...{ - matcher: { - id: string | *"" @reviewme() - options?: _ @reviewme() - } - properties: [...{ - id: string | *"" @reviewme() - value?: _ @reviewme() - }] - }] @reviewme() - } - } @cuetsy(kind="interface") @reviewme() + // Map values to a display color + color?: #FieldColor @reviewme() + + // Used when reducing field values + // nullValueMode?: NullValueMode + + // The behavior when clicking on a result + links?: [...] @reviewme() + + // Alternative to empty string + noValue?: string @reviewme() + + // custom is specified by the PanelFieldConfig field + // in panel plugin schemas. + custom?: {...} @reviewme() + } @cuetsy(kind="interface") @grafana(TSVeneer="type") @reviewme() // Row panel #RowPanel: { diff --git a/pkg/coremodel/dashboard/dashboard_gen.go b/pkg/coremodel/dashboard/dashboard_gen.go index fb377496dd7..22ee2662100 100644 --- a/pkg/coremodel/dashboard/dashboard_gen.go +++ b/pkg/coremodel/dashboard/dashboard_gen.go @@ -90,6 +90,17 @@ const ( HeatmapPanelTypeHeatmap HeatmapPanelType = "heatmap" ) +// Defines values for MappingType. +const ( + MappingTypeRange MappingType = "range" + + MappingTypeRegex MappingType = "regex" + + MappingTypeSpecial MappingType = "special" + + MappingTypeValue MappingType = "value" +) + // Defines values for PanelRepeatDirection. const ( PanelRepeatDirectionH PanelRepeatDirection = "h" @@ -97,11 +108,48 @@ const ( PanelRepeatDirectionV PanelRepeatDirection = "v" ) +// Defines values for RangeMapType. +const ( + RangeMapTypeRange RangeMapType = "range" +) + +// Defines values for RegexMapType. +const ( + RegexMapTypeRegex RegexMapType = "regex" +) + // Defines values for RowPanelType. const ( RowPanelTypeRow RowPanelType = "row" ) +// Defines values for SpecialValueMapOptionsMatch. +const ( + SpecialValueMapOptionsMatchFalse SpecialValueMapOptionsMatch = "false" + + SpecialValueMapOptionsMatchTrue SpecialValueMapOptionsMatch = "true" +) + +// Defines values for SpecialValueMapType. +const ( + SpecialValueMapTypeSpecial SpecialValueMapType = "special" +) + +// Defines values for SpecialValueMatch. +const ( + SpecialValueMatchEmpty SpecialValueMatch = "empty" + + SpecialValueMatchFalse SpecialValueMatch = "false" + + SpecialValueMatchNan SpecialValueMatch = "nan" + + SpecialValueMatchNull SpecialValueMatch = "null" + + SpecialValueMatchNullNan SpecialValueMatch = "null+nan" + + SpecialValueMatchTrue SpecialValueMatch = "true" +) + // Defines values for ThresholdsConfigMode. const ( ThresholdsConfigModeAbsolute ThresholdsConfigMode = "absolute" @@ -116,6 +164,11 @@ const ( ThresholdsModePercentage ThresholdsMode = "percentage" ) +// Defines values for ValueMapType. +const ( + ValueMapTypeValue ValueMapType = "value" +) + // Defines values for VariableModelType. const ( VariableModelTypeAdhoc VariableModelType = "adhoc" @@ -336,6 +389,15 @@ type DashboardLink struct { // Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. type DashboardLinkType string +// DynamicConfigValue is the Go representation of a dashboard.DynamicConfigValue. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type DynamicConfigValue struct { + Id string `json:"id"` + Value *interface{} `json:"value,omitempty"` +} + // TODO docs // // THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. @@ -363,6 +425,126 @@ type FieldColorModeId string // Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. type FieldColorSeriesByMode string +// FieldConfig is the Go representation of a dashboard.FieldConfig. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type FieldConfig struct { + // TODO docs + Color *FieldColor `json:"color,omitempty"` + + // custom is specified by the PanelFieldConfig field + // in panel plugin schemas. + Custom *map[string]interface{} `json:"custom,omitempty"` + + // Significant digits (for display) + Decimals *float32 `json:"decimals,omitempty"` + + // Human readable field metadata + Description *string `json:"description,omitempty"` + + // The display value for this field. This supports template variables blank is auto + DisplayName *string `json:"displayName,omitempty"` + + // 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 `json:"displayNameFromDS,omitempty"` + + // True if data source field supports ad-hoc filters + Filterable *bool `json:"filterable,omitempty"` + + // The behavior when clicking on a result + Links *[]interface{} `json:"links,omitempty"` + + // Convert input values into a display string + Mappings *[]ValueMapping `json:"mappings,omitempty"` + Max *float32 `json:"max,omitempty"` + Min *float32 `json:"min,omitempty"` + + // Alternative to empty string + NoValue *string `json:"noValue,omitempty"` + + // 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 `json:"path,omitempty"` + Thresholds *ThresholdsConfig `json:"thresholds,omitempty"` + + // Numeric Options + Unit *string `json:"unit,omitempty"` + + // True if data source can write a value to the path. Auth/authz are supported separately + Writeable *bool `json:"writeable,omitempty"` +} + +// FieldConfigSource is the Go representation of a dashboard.FieldConfigSource. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type FieldConfigSource struct { + Defaults struct { + // TODO docs + Color *FieldColor `json:"color,omitempty"` + + // custom is specified by the PanelFieldConfig field + // in panel plugin schemas. + Custom *map[string]interface{} `json:"custom,omitempty"` + + // Significant digits (for display) + Decimals *float32 `json:"decimals,omitempty"` + + // Human readable field metadata + Description *string `json:"description,omitempty"` + + // The display value for this field. This supports template variables blank is auto + DisplayName *string `json:"displayName,omitempty"` + + // 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 `json:"displayNameFromDS,omitempty"` + + // True if data source field supports ad-hoc filters + Filterable *bool `json:"filterable,omitempty"` + + // The behavior when clicking on a result + Links *[]interface{} `json:"links,omitempty"` + + // Convert input values into a display string + Mappings *[]ValueMapping `json:"mappings,omitempty"` + Max *float32 `json:"max,omitempty"` + Min *float32 `json:"min,omitempty"` + + // Alternative to empty string + NoValue *string `json:"noValue,omitempty"` + + // 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 `json:"path,omitempty"` + Thresholds *ThresholdsConfig `json:"thresholds,omitempty"` + + // Numeric Options + Unit *string `json:"unit,omitempty"` + + // True if data source can write a value to the path. Auth/authz are supported separately + Writeable *bool `json:"writeable,omitempty"` + } `json:"defaults"` + Overrides []struct { + Matcher struct { + Id string `json:"id"` + Options *interface{} `json:"options,omitempty"` + } `json:"matcher"` + Properties []struct { + Id string `json:"id"` + Value *interface{} `json:"value,omitempty"` + } `json:"properties"` + } `json:"overrides"` +} + // GraphPanel is the Go representation of a dashboard.GraphPanel. // // THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. @@ -413,6 +595,21 @@ type HeatmapPanel struct { // Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. type HeatmapPanelType string +// TODO docs +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type MappingType string + +// MatcherConfig is the Go representation of a dashboard.MatcherConfig. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type MatcherConfig struct { + Id string `json:"id"` + Options *interface{} `json:"options,omitempty"` +} + // Model panels. Panels are canonically defined inline // because they share a version timeline with the dashboard // schema; they do not evolve independently. @@ -453,21 +650,13 @@ type Panel struct { // True if data source field supports ad-hoc filters Filterable *bool `json:"filterable,omitempty"` - // // The behavior when clicking on a result + // The behavior when clicking on a result Links *[]interface{} `json:"links,omitempty"` // 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[string]interface{} `json:"mappings,omitempty"` - Max *float32 `json:"max,omitempty"` - Min *float32 `json:"min,omitempty"` + Mappings *[]ValueMapping `json:"mappings,omitempty"` + Max *float32 `json:"max,omitempty"` + Min *float32 `json:"min,omitempty"` // Alternative to empty string NoValue *string `json:"noValue,omitempty"` @@ -568,6 +757,54 @@ type Panel struct { // Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. type PanelRepeatDirection string +// TODO docs +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type RangeMap struct { + Options struct { + // to and from are `number | null` in current ts, really not sure what to do + From int32 `json:"from"` + Result struct { + Color *string `json:"color,omitempty"` + Icon *string `json:"icon,omitempty"` + Index *int32 `json:"index,omitempty"` + Text *string `json:"text,omitempty"` + } `json:"result"` + To int32 `json:"to"` + } `json:"options"` + Type RangeMapType `json:"type"` +} + +// RangeMapType is the Go representation of a RangeMap.Type. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type RangeMapType string + +// TODO docs +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type RegexMap struct { + Options struct { + Pattern string `json:"pattern"` + Result struct { + Color *string `json:"color,omitempty"` + Icon *string `json:"icon,omitempty"` + Index *int32 `json:"index,omitempty"` + Text *string `json:"text,omitempty"` + } `json:"result"` + } `json:"options"` + Type RegexMapType `json:"type"` +} + +// RegexMapType is the Go representation of a RegexMap.Type. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type RegexMapType string + // Row panel // // THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. @@ -596,6 +833,42 @@ type RowPanel struct { // Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. type RowPanelType string +// TODO docs +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type SpecialValueMap struct { + Options struct { + Match SpecialValueMapOptionsMatch `json:"match"` + Pattern string `json:"pattern"` + Result struct { + Color *string `json:"color,omitempty"` + Icon *string `json:"icon,omitempty"` + Index *int32 `json:"index,omitempty"` + Text *string `json:"text,omitempty"` + } `json:"result"` + } `json:"options"` + Type SpecialValueMapType `json:"type"` +} + +// SpecialValueMapOptionsMatch is the Go representation of a SpecialValueMap.Options.Match. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type SpecialValueMapOptionsMatch string + +// SpecialValueMapType is the Go representation of a SpecialValueMap.Type. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type SpecialValueMapType string + +// TODO docs +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type SpecialValueMatch string + // 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 @@ -671,6 +944,38 @@ type Transformation struct { Options map[string]interface{} `json:"options"` } +// TODO docs +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type ValueMap struct { + Options map[string]interface{} `json:"options"` + Type ValueMapType `json:"type"` +} + +// ValueMapType is the Go representation of a ValueMap.Type. +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type ValueMapType string + +// TODO docs +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type ValueMapping interface{} + +// TODO docs +// +// THIS TYPE IS INTENDED FOR INTERNAL USE BY THE GRAFANA BACKEND, AND IS SUBJECT TO BREAKING CHANGES. +// Equivalent Go types at stable import paths are provided in https://github.com/grafana/grok. +type ValueMappingResult struct { + Color *string `json:"color,omitempty"` + Icon *string `json:"icon,omitempty"` + Index *int32 `json:"index,omitempty"` + Text *string `json:"text,omitempty"` +} + // FROM: packages/grafana-data/src/types/templateVars.ts // TODO docs // TODO what about what's in public/app/features/types.ts? diff --git a/pkg/framework/coremodel/gen.go b/pkg/framework/coremodel/gen.go index c261edd66c9..06e5723df35 100644 --- a/pkg/framework/coremodel/gen.go +++ b/pkg/framework/coremodel/gen.go @@ -7,21 +7,42 @@ package main import ( "fmt" "os" + "path" "path/filepath" "sort" + "strings" + "cuelang.org/go/cue" "cuelang.org/go/cue/cuecontext" + "cuelang.org/go/cue/errors" "cuelang.org/go/cue/load" "github.com/grafana/cuetsy" - "github.com/grafana/thema" - + "github.com/grafana/cuetsy/ts" + "github.com/grafana/cuetsy/ts/ast" gcgen "github.com/grafana/grafana/pkg/codegen" + "github.com/grafana/thema" ) var lib = thema.NewLibrary(cuecontext.New()) const sep = string(filepath.Separator) +var tsroot, cmroot, groot string + +func init() { + cwd, err := os.Getwd() + if err != nil { + fmt.Fprintf(os.Stderr, "could not get working directory: %s", err) + os.Exit(1) + } + + // TODO this binds us to only having coremodels in a single directory. If we need more, compgen is the way + groot = filepath.Dir(filepath.Dir(filepath.Dir(cwd))) // the working dir is /pkg/framework/coremodel. Going up 3 dirs we get the grafana root + + cmroot = filepath.Join(groot, "pkg", "coremodel") + tsroot = filepath.Join(groot, "packages", "grafana-schema", "src") +} + // Generate Go and Typescript implementations for all coremodels, and populate the // coremodel static registry. func main() { @@ -30,25 +51,13 @@ func main() { os.Exit(1) } - cwd, err := os.Getwd() - if err != nil { - fmt.Fprintf(os.Stderr, "could not get working directory: %s", err) - os.Exit(1) - } - - // TODO this binds us to only having coremodels in a single directory. If we need more, compgen is the way - groot := filepath.Dir(filepath.Dir(filepath.Dir(cwd))) // the working dir is /pkg/framework/coremodel. Going up 3 dirs we get the grafana root - - cmroot := filepath.Join(groot, "pkg", "coremodel") - tsroot := filepath.Join(groot, "packages", "grafana-schema", "src", "schema") - items, err := os.ReadDir(cmroot) if err != nil { fmt.Fprintf(os.Stderr, "could not read coremodels parent dir %s: %s\n", cmroot, err) os.Exit(1) } - var lins []*gcgen.ExtractedLineage + var lins []*gcgen.CoremodelDeclaration for _, item := range items { if item.IsDir() { lin, err := gcgen.ExtractLineage(filepath.Join(cmroot, item.Name(), "coremodel.cue"), lib) @@ -64,6 +73,9 @@ func main() { return lins[i].Lineage.Name() < lins[j].Lineage.Name() }) + // The typescript veneer index.gen.ts file, which we'll build up over time + // from the exported types. + tsvidx := new(ast.File) wd := gcgen.NewWriteDiffer() for _, ls := range lins { gofiles, err := ls.GenerateGoCoremodel(filepath.Join(cmroot, ls.Lineage.Name())) @@ -75,15 +87,26 @@ func main() { // Only generate TS for API types if ls.IsAPIType { - tsfiles, err := ls.GenerateTypescriptCoremodel(filepath.Join(tsroot, ls.Lineage.Name())) + tsf, err := ls.GenerateTypescriptCoremodel() if err != nil { - fmt.Fprintf(os.Stderr, "failed to generate TypeScript for %s: %s\n", ls.Lineage.Name(), err) + fmt.Fprintf(os.Stderr, "error generating TypeScript for %s: %s\n", ls.Lineage.Name(), err) os.Exit(1) } - wd.Merge(tsfiles) + tsf.Doc = mkTSHeader(ls) + wd[filepath.FromSlash(filepath.Join(tsroot, rawTSGenPath(ls)))] = []byte(tsf.String()) + + decls, err := extractTSIndexVeneerElements(ls, tsf) + if err != nil { + fmt.Fprintf(os.Stderr, "error generating TypeScript veneer for %s: %s\n", ls.Lineage.Name(), errors.Details(err, nil)) + os.Exit(1) + } + tsvidx.Nodes = append(tsvidx.Nodes, decls...) } } + tsvidx.Doc = mkTSHeader(nil) + wd[filepath.Join(tsroot, "index.gen.ts")] = []byte(tsvidx.String()) + regfiles, err := gcgen.GenerateCoremodelRegistry(filepath.Join(groot, "pkg", "framework", "coremodel", "registry", "registry_gen.go"), lins) if err != nil { fmt.Fprintf(os.Stderr, "failed to generate coremodel registry: %s\n", err) @@ -113,6 +136,26 @@ func main() { } } } + +// generates the path relative to packages/grafana-schema/src at which the raw +// type definitions should be exported for the latest schema of this type +func rawTSGenPath(cm *gcgen.CoremodelDeclaration) string { + return fmt.Sprintf("raw/%s/%s/%s.gen.ts", cm.Lineage.Name(), cm.PathVersion(), cm.Lineage.Name()) +} + +func mkTSHeader(cm *gcgen.CoremodelDeclaration) *ast.Comment { + v := gcgen.HeaderVars{ + GeneratorPath: "pkg/framework/coremodel/gen.go", + } + if cm != nil { + v.LineagePath = cm.RelativePath + } + v.GeneratorPath = "pkg/framework/coremodel/gen.go" + return &ast.Comment{ + Text: strings.TrimSpace(gcgen.GenGrafanaHeader(v)), + } +} + func genSharedSchemas(groot string) (gcgen.WriteDiffer, error) { abspath := filepath.Join(groot, "packages", "grafana-schema", "src", "schema") cfg := &load.Config{ @@ -132,7 +175,9 @@ func genSharedSchemas(groot string) (gcgen.WriteDiffer, error) { return nil, fmt.Errorf("errors while building CUE in %s: %s", abspath, v.Err()) } - b, err := cuetsy.Generate(v, cuetsy.Config{}) + b, err := cuetsy.Generate(v, cuetsy.Config{ + Export: true, + }) if err != nil { return nil, fmt.Errorf("failed to generate TS: %w", err) } @@ -146,3 +191,259 @@ func genSharedSchemas(groot string) (gcgen.WriteDiffer, error) { `), b...) return wd, nil } + +// TODO make this more generic and reusable +func extractTSIndexVeneerElements(cm *gcgen.CoremodelDeclaration, tf *ast.File) ([]ast.Decl, error) { + lin := cm.Lineage + sch := thema.SchemaP(lin, thema.LatestVersion(lin)) + + // Check the root, then walk the tree + rootv := sch.UnwrapCUE() + + var raw, custom, rawD, customD ast.Idents + + var terr errors.Error + visit := func(p cue.Path, wv cue.Value) bool { + var name string + sels := p.Selectors() + switch len(sels) { + case 0: + name = strings.Title(cm.Lineage.Name()) + fallthrough + case 1: + // Only deal with subpaths that are definitions, for now + // TODO incorporate smarts about grouped lineages here + if name == "" { + if !sels[0].IsDefinition() { + return false + } + // It might seem to make sense that we'd strip out the leading # here for + // definitions. However, cuetsy's tsast actually has the # still present in its + // Ident types, stripping it out on the fly when stringifying. + name = sels[0].String() + } + + // Search the generated TS AST for the type and default decl nodes + pair := findDeclNode(name, tf) + if pair.T == nil { + // No generated type for this item, skip it + return false + } + + cust, perr := getCustomVeneerAttr(wv) + if perr != nil { + terr = errors.Append(terr, errors.Promote(perr, fmt.Sprintf("%s: ", p.String()))) + } + var has bool + for _, tgt := range cust { + has = has || tgt.target == "type" + } + if has { + custom = append(custom, *pair.T) + if pair.D != nil { + customD = append(customD, *pair.D) + } + } else { + raw = append(raw, *pair.T) + if pair.D != nil { + rawD = append(rawD, *pair.D) + } + } + } + + return true + } + walk(rootv, visit, nil) + + if len(errors.Errors(terr)) != 0 { + return nil, terr + } + + ret := make([]ast.Decl, 0) + if len(raw) > 0 { + ret = append(ret, ast.ExportSet{ + CommentList: []ast.Comment{ts.CommentFromString(fmt.Sprintf("Raw generated types from %s entity type.", cm.Lineage.Name()), 80, false)}, + TypeOnly: true, + Exports: raw, + From: ast.Str{Value: fmt.Sprintf("./raw/%s/%s/%s.gen", cm.Lineage.Name(), cm.PathVersion(), cm.Lineage.Name())}, + }) + } + if len(rawD) > 0 { + ret = append(ret, ast.ExportSet{ + CommentList: []ast.Comment{ts.CommentFromString(fmt.Sprintf("Raw generated default consts from %s entity type.", cm.Lineage.Name()), 80, false)}, + TypeOnly: false, + Exports: rawD, + From: ast.Str{Value: fmt.Sprintf("./raw/%s/%s/%s.gen", cm.Lineage.Name(), cm.PathVersion(), cm.Lineage.Name())}, + }) + } + vtfile := fmt.Sprintf("./veneer/%s.types", cm.Lineage.Name()) + customstr := fmt.Sprintf(`// The following exported declarations correspond to types in the %s@%s schema with +// attribute @grafana(TSVeneer="type"). (lineage declared in file: %s) +// +// The handwritten file for these type and default veneers is expected to be at +// %s.ts. +// This re-export declaration enforces that the handwritten veneer file exists, +// and exports all the symbols in the list. +// +// TODO generate code such that tsc enforces type compatibility between raw and veneer decls`, + cm.Lineage.Name(), thema.LatestVersion(cm.Lineage), cm.RelativePath, filepath.Clean(path.Join("packages", "grafana-schema", "src", vtfile))) + + customComments := []ast.Comment{{Text: customstr}} + if len(custom) > 0 { + ret = append(ret, ast.ExportSet{ + CommentList: customComments, + TypeOnly: true, + Exports: custom, + From: ast.Str{Value: vtfile}, + }) + } + if len(customD) > 0 { + ret = append(ret, ast.ExportSet{ + CommentList: customComments, + TypeOnly: false, + Exports: customD, + From: ast.Str{Value: vtfile}, + }) + } + + // TODO emit a decl in the index.gen.ts that ensures any custom veneer types are "compatible" with current version raw types + return ret, nil +} + +type declPair struct { + T, D *ast.Ident +} + +func findDeclNode(name string, tf *ast.File) declPair { + var p declPair + for _, decl := range tf.Nodes { + // Peer through export keywords + if ex, is := decl.(ast.ExportKeyword); is { + decl = ex.Decl + } + + switch x := decl.(type) { + case ast.TypeDecl: + if x.Name.Name == name { + p.T = &x.Name + } + case ast.VarDecl: + if x.Names.Idents[0].Name == "default"+name { + p.D = &x.Names.Idents[0] + } + } + } + return p +} + +type tsVeneerAttr struct { + target string +} + +func walk(v cue.Value, before func(cue.Path, cue.Value) bool, after func(cue.Path, cue.Value)) { + innerWalk(cue.MakePath(), v, before, after) +} + +func innerWalk(p cue.Path, v cue.Value, before func(cue.Path, cue.Value) bool, after func(cue.Path, cue.Value)) { + // switch v.IncompleteKind() { + switch v.Kind() { + default: + if before != nil && !before(p, v) { + return + } + case cue.StructKind: + if before != nil && !before(p, v) { + return + } + iter, err := v.Fields(cue.All()) + if err != nil { + panic(err) + } + + for iter.Next() { + innerWalk(appendPath(p, iter.Selector()), iter.Value(), before, after) + } + if lv := v.LookupPath(cue.MakePath(cue.AnyString)); lv.Exists() { + innerWalk(appendPath(p, cue.AnyString), lv, before, after) + } + case cue.ListKind: + if before != nil && !before(p, v) { + return + } + list, err := v.List() + if err != nil { + panic(err) + } + for i := 0; list.Next(); i++ { + innerWalk(appendPath(p, cue.Index(i)), list.Value(), before, after) + } + if lv := v.LookupPath(cue.MakePath(cue.AnyIndex)); lv.Exists() { + innerWalk(appendPath(p, cue.AnyString), lv, before, after) + } + } + if after != nil { + after(p, v) + } +} + +func appendPath(p cue.Path, sel cue.Selector) cue.Path { + return cue.MakePath(append(p.Selectors(), sel)...) +} + +var allowedTSVeneers = map[string]bool{ + "type": true, +} + +func allowedTSVeneersString() string { + var list []string + for tgt := range allowedTSVeneers { + list = append(list, tgt) + } + sort.Strings(list) + + return strings.Join(list, "|") +} + +func getCustomVeneerAttr(v cue.Value) ([]tsVeneerAttr, error) { + var attrs []tsVeneerAttr + for _, a := range v.Attributes(cue.ValueAttr) { + if a.Name() != "grafana" { + continue + } + for i := 0; i < a.NumArgs(); i++ { + key, av := a.Arg(i) + if key != "TSVeneer" { + return nil, valError(v, "attribute 'grafana' only allows the arg 'TSVeneer'") + } + + aterr := valError(v, "@grafana(TSVeneer=\"x\") requires one or more of the following separated veneer types for x: %s", allowedTSVeneersString()) + var some bool + for _, tgt := range strings.Split(av, "|") { + some = true + if !allowedTSVeneers[tgt] { + return nil, aterr + } + attrs = append(attrs, tsVeneerAttr{ + target: tgt, + }) + } + if !some { + return nil, aterr + } + } + } + + sort.Slice(attrs, func(i, j int) bool { + return attrs[i].target < attrs[j].target + }) + + return attrs, nil +} + +func valError(v cue.Value, format string, args ...interface{}) error { + s := v.Source() + if s == nil { + return fmt.Errorf(format, args...) + } + return errors.Newf(s.Pos(), format, args...) +} diff --git a/public/app/plugins/gen.go b/public/app/plugins/gen.go index 7a57abe2fda..6cbc14861c0 100644 --- a/public/app/plugins/gen.go +++ b/public/app/plugins/gen.go @@ -89,12 +89,15 @@ func main() { var wdm codegen.WriteDiffer for _, ptp := range ptrees { - wdm, err = ptp.Tree.GenerateTS(ptp.Path) + tfast, err := ptp.Tree.GenerateTypeScriptAST() if err != nil { fmt.Fprintf(os.Stderr, "generating typescript failed for %s: %s\n", ptp.Path, err) os.Exit(1) } - wd.Merge(wdm) + // nil return if there was nothing to generate (no slot implementations) + if tfast != nil { + wd[filepath.Join(ptp.Path, "models.gen.ts")] = []byte(tfast.String()) + } relp, _ := filepath.Rel(groot, ptp.Path) wdm, err = ptp.Tree.GenerateGo(ptp.Path, codegen.GoGenConfig{ diff --git a/public/app/plugins/panel/annolist/models.gen.ts b/public/app/plugins/panel/annolist/models.gen.ts index a5a5a4fcd69..80bf06caa5b 100644 --- a/public/app/plugins/panel/annolist/models.gen.ts +++ b/public/app/plugins/panel/annolist/models.gen.ts @@ -10,7 +10,6 @@ export const PanelModelVersion = Object.freeze([0, 0]); - export interface PanelOptions { limit: number; navigateAfter: string; @@ -21,7 +20,7 @@ export interface PanelOptions { showTags: boolean; showTime: boolean; showUser: boolean; - tags: string[]; + tags: Array; } export const defaultPanelOptions: Partial = { @@ -36,4 +35,3 @@ export const defaultPanelOptions: Partial = { showUser: true, tags: [], }; - diff --git a/public/app/plugins/panel/barchart/models.gen.ts b/public/app/plugins/panel/barchart/models.gen.ts index 46522acf1e9..680dbc13c6c 100644 --- a/public/app/plugins/panel/barchart/models.gen.ts +++ b/public/app/plugins/panel/barchart/models.gen.ts @@ -7,22 +7,56 @@ // Run `make gen-cue` from repository root to regenerate. + import * as ui from '@grafana/schema'; export const PanelModelVersion = Object.freeze([0, 0]); - export interface PanelOptions extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui.OptionsWithTextFormatting { + /** + * TODO docs + */ barRadius?: number; + /** + * Controls the width of bars. 1 = Max width, 0 = Min width. + */ barWidth: number; + /** + * TODO docs + */ colorByField?: string; + /** + * Controls the width of groups. 1 = max with, 0 = min width. + */ groupWidth: number; + /** + * TODO docs + */ orientation: ui.VizOrientation; + /** + * This controls whether values are shown on top or to the left of bars. + */ showValue: ui.VisibilityMode; + /** + * TODO docs + */ stacking: ui.StackingMode; + /** + * TODO docs + */ xField?: string; + /** + * TODO docs + */ xTickLabelMaxLength: number; + /** + * TODO docs + */ xTickLabelRotation: number; + /** + * TODO docs + * negative values indicate backwards skipping behavior + */ xTickLabelSpacing?: number; } @@ -38,8 +72,18 @@ export const defaultPanelOptions: Partial = { }; export interface PanelFieldConfig extends ui.AxisConfig, ui.HideableFieldConfig { + /** + * Controls the fill opacity of the bars. + */ fillOpacity?: number; + /** + * Set the mode of the gradient fill. Fill gradient is based on the line color. To change the color, use the standard color scheme field option. + * Gradient appearance is influenced by the Fill opacity setting. + */ gradientMode?: ui.GraphGradientMode; + /** + * Controls line width of the bars. + */ lineWidth?: number; } @@ -48,4 +92,3 @@ export const defaultPanelFieldConfig: Partial = { gradientMode: ui.GraphGradientMode.None, lineWidth: 1, }; - diff --git a/public/app/plugins/panel/bargauge/models.gen.ts b/public/app/plugins/panel/bargauge/models.gen.ts index c57f0927b40..b02bb2c0e98 100644 --- a/public/app/plugins/panel/bargauge/models.gen.ts +++ b/public/app/plugins/panel/bargauge/models.gen.ts @@ -7,11 +7,11 @@ // Run `make gen-cue` from repository root to regenerate. + import * as ui from '@grafana/schema'; export const PanelModelVersion = Object.freeze([0, 0]); - export interface PanelOptions extends ui.SingleStatBaseOptions { displayMode: ui.BarGaugeDisplayMode; minVizHeight: number; @@ -25,4 +25,3 @@ export const defaultPanelOptions: Partial = { minVizWidth: 0, showUnfilled: true, }; - diff --git a/public/app/plugins/panel/dashlist/models.gen.ts b/public/app/plugins/panel/dashlist/models.gen.ts index 8c1bfc0d543..98622cdbe2c 100644 --- a/public/app/plugins/panel/dashlist/models.gen.ts +++ b/public/app/plugins/panel/dashlist/models.gen.ts @@ -10,7 +10,6 @@ export const PanelModelVersion = Object.freeze([0, 0]); - export enum PanelLayout { List = 'list', Previews = 'previews', @@ -25,7 +24,7 @@ export interface PanelOptions { showRecentlyViewed: boolean; showSearch: boolean; showStarred: boolean; - tags: string[]; + tags: Array; } export const defaultPanelOptions: Partial = { @@ -38,4 +37,3 @@ export const defaultPanelOptions: Partial = { showStarred: true, tags: [], }; - diff --git a/public/app/plugins/panel/gauge/models.gen.ts b/public/app/plugins/panel/gauge/models.gen.ts index cd4e38d0b80..19bfd5bfd48 100644 --- a/public/app/plugins/panel/gauge/models.gen.ts +++ b/public/app/plugins/panel/gauge/models.gen.ts @@ -7,11 +7,11 @@ // Run `make gen-cue` from repository root to regenerate. + import * as ui from '@grafana/schema'; export const PanelModelVersion = Object.freeze([0, 0]); - export interface PanelOptions extends ui.SingleStatBaseOptions { showThresholdLabels: boolean; showThresholdMarkers: boolean; @@ -21,4 +21,3 @@ export const defaultPanelOptions: Partial = { showThresholdLabels: false, showThresholdMarkers: true, }; - diff --git a/public/app/plugins/panel/histogram/models.gen.ts b/public/app/plugins/panel/histogram/models.gen.ts index 61eabbee500..dbc245a6626 100644 --- a/public/app/plugins/panel/histogram/models.gen.ts +++ b/public/app/plugins/panel/histogram/models.gen.ts @@ -7,14 +7,23 @@ // Run `make gen-cue` from repository root to regenerate. + import * as ui from '@grafana/schema'; export const PanelModelVersion = Object.freeze([0, 0]); - export interface PanelOptions extends ui.OptionsWithLegend, ui.OptionsWithTooltip { + /** + * Offset buckets by this amount + */ bucketOffset?: number; + /** + * Size of each bucket + */ bucketSize?: number; + /** + * Combines multiple series into a single histogram + */ combine?: boolean; } @@ -23,8 +32,18 @@ export const defaultPanelOptions: Partial = { }; export interface PanelFieldConfig extends ui.AxisConfig, ui.HideableFieldConfig { + /** + * Controls the fill opacity of the bars. + */ fillOpacity?: number; + /** + * Set the mode of the gradient fill. Fill gradient is based on the line color. To change the color, use the standard color scheme field option. + * Gradient appearance is influenced by the Fill opacity setting. + */ gradientMode?: ui.GraphGradientMode; + /** + * Controls line width of the bars. + */ lineWidth?: number; } @@ -33,4 +52,3 @@ export const defaultPanelFieldConfig: Partial = { gradientMode: ui.GraphGradientMode.None, lineWidth: 1, }; - diff --git a/public/app/plugins/panel/news/models.gen.ts b/public/app/plugins/panel/news/models.gen.ts index a8ce1fc724b..5e2607b6e3e 100644 --- a/public/app/plugins/panel/news/models.gen.ts +++ b/public/app/plugins/panel/news/models.gen.ts @@ -10,8 +10,10 @@ export const PanelModelVersion = Object.freeze([0, 0]); - export interface PanelOptions { + /** + * empty/missing will default to grafana blog + */ feedUrl?: string; showImage?: boolean; } @@ -19,4 +21,3 @@ export interface PanelOptions { export const defaultPanelOptions: Partial = { showImage: true, }; - diff --git a/public/app/plugins/panel/piechart/models.gen.ts b/public/app/plugins/panel/piechart/models.gen.ts index db9696368b0..38ba45cde4b 100644 --- a/public/app/plugins/panel/piechart/models.gen.ts +++ b/public/app/plugins/panel/piechart/models.gen.ts @@ -7,29 +7,43 @@ // Run `make gen-cue` from repository root to regenerate. + import * as ui from '@grafana/schema'; export const PanelModelVersion = Object.freeze([0, 0]); - +/** + * Select the pie chart display style. + */ export enum PieChartType { Donut = 'donut', Pie = 'pie', } +/** + * Select labels to display on the pie chart. + * - Name - The series or field name. + * - Percent - The percentage of the whole. + * - Value - The raw numerical value. + */ export enum PieChartLabels { Name = 'name', Percent = 'percent', Value = 'value', } +/** + * Select values to display in the legend. + * - Percent: The percentage of the whole. + * - Value: The raw numerical value. + */ export enum PieChartLegendValues { Percent = 'percent', Value = 'value', } export interface PieChartLegendOptions extends ui.VizLegendOptions { - values: PieChartLegendValues[]; + values: Array; } export const defaultPieChartLegendOptions: Partial = { @@ -37,7 +51,7 @@ export const defaultPieChartLegendOptions: Partial = { }; export interface PanelOptions extends ui.OptionsWithTooltip, ui.SingleStatBaseOptions { - displayLabels: PieChartLabels[]; + displayLabels: Array; legend: PieChartLegendOptions; pieType: PieChartType; } @@ -47,4 +61,3 @@ export const defaultPanelOptions: Partial = { }; export interface PanelFieldConfig extends ui.HideableFieldConfig {} - diff --git a/public/app/plugins/panel/stat/models.gen.ts b/public/app/plugins/panel/stat/models.gen.ts index 3da1887896f..9479b590579 100644 --- a/public/app/plugins/panel/stat/models.gen.ts +++ b/public/app/plugins/panel/stat/models.gen.ts @@ -7,11 +7,11 @@ // Run `make gen-cue` from repository root to regenerate. + import * as ui from '@grafana/schema'; export const PanelModelVersion = Object.freeze([0, 0]); - export interface PanelOptions extends ui.SingleStatBaseOptions { colorMode: ui.BigValueColorMode; graphMode: ui.BigValueGraphMode; @@ -25,4 +25,3 @@ export const defaultPanelOptions: Partial = { justifyMode: ui.BigValueJustifyMode.Auto, textMode: ui.BigValueTextMode.Auto, }; - diff --git a/public/app/plugins/panel/text/models.gen.ts b/public/app/plugins/panel/text/models.gen.ts index 818d7667422..50ad46667ff 100644 --- a/public/app/plugins/panel/text/models.gen.ts +++ b/public/app/plugins/panel/text/models.gen.ts @@ -10,7 +10,6 @@ export const PanelModelVersion = Object.freeze([0, 0]); - export enum TextMode { Code = 'code', HTML = 'html', @@ -32,6 +31,9 @@ export enum CodeLanguage { export const defaultCodeLanguage: CodeLanguage = CodeLanguage.Plaintext; export interface CodeOptions { + /** + * The language passed to monaco code editor + */ language: CodeLanguage; showLineNumbers: boolean; showMiniMap: boolean; @@ -55,4 +57,3 @@ export const defaultPanelOptions: Partial = { For markdown syntax help: [commonmark.org/help](https://commonmark.org/help/)`, mode: TextMode.Markdown, }; -