mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 05:12:29 +08:00
frontend for trim/apply defaults and some bug fixing (#33561)
* remove empty object and workaround on list * frontend * add toggle on frontend
This commit is contained in:
@ -10,7 +10,7 @@ Family: scuemata.#Family & {
|
|||||||
// TODO must isolate or remove identifiers local to a Grafana instance...?
|
// TODO must isolate or remove identifiers local to a Grafana instance...?
|
||||||
id?: number
|
id?: number
|
||||||
// Unique dashboard identifier that can be generated by anyone. string (8-40)
|
// Unique dashboard identifier that can be generated by anyone. string (8-40)
|
||||||
uid: string
|
uid?: string
|
||||||
// Title of dashboard.
|
// Title of dashboard.
|
||||||
title?: string
|
title?: string
|
||||||
// Description of dashboard.
|
// Description of dashboard.
|
||||||
@ -181,7 +181,7 @@ Family: scuemata.#Family & {
|
|||||||
// nullValueMode?: NullValueMode;
|
// nullValueMode?: NullValueMode;
|
||||||
|
|
||||||
// // The behavior when clicking on a result
|
// // The behavior when clicking on a result
|
||||||
// links?: DataLink[];
|
links?: [...]
|
||||||
|
|
||||||
// Alternative to empty string
|
// Alternative to empty string
|
||||||
noValue?: string
|
noValue?: string
|
||||||
|
@ -46,6 +46,7 @@ export interface FeatureToggles {
|
|||||||
|
|
||||||
live: boolean;
|
live: boolean;
|
||||||
ngalert: boolean;
|
ngalert: boolean;
|
||||||
|
trimDefaults: boolean;
|
||||||
panelLibrary: boolean;
|
panelLibrary: boolean;
|
||||||
accesscontrol: boolean;
|
accesscontrol: boolean;
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@ export class GrafanaBootConfig implements GrafanaConfig {
|
|||||||
panelLibrary: false,
|
panelLibrary: false,
|
||||||
reportVariables: false,
|
reportVariables: false,
|
||||||
accesscontrol: false,
|
accesscontrol: false,
|
||||||
|
trimDefaults: false,
|
||||||
};
|
};
|
||||||
licenseInfo: LicenseInfo = {} as LicenseInfo;
|
licenseInfo: LicenseInfo = {} as LicenseInfo;
|
||||||
rendererAvailable = false;
|
rendererAvailable = false;
|
||||||
|
@ -206,10 +206,19 @@ func (hs *HTTPServer) GetPluginMarkdown(c *models.ReqContext) response.Response
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (hs *HTTPServer) ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDashboardCommand) response.Response {
|
func (hs *HTTPServer) ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDashboardCommand) response.Response {
|
||||||
|
var err error
|
||||||
if apiCmd.PluginId == "" && apiCmd.Dashboard == nil {
|
if apiCmd.PluginId == "" && apiCmd.Dashboard == nil {
|
||||||
return response.Error(422, "Dashboard must be set", nil)
|
return response.Error(422, "Dashboard must be set", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trimDefaults := c.QueryBoolWithDefault("trimdefaults", true)
|
||||||
|
if trimDefaults && !hs.LoadSchemaService.IsDisabled() {
|
||||||
|
apiCmd.Dashboard, err = hs.LoadSchemaService.DashboardApplyDefaults(apiCmd.Dashboard)
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(500, "Error while applying default value to the dashboard json", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dashInfo, err := hs.PluginManager.ImportDashboard(apiCmd.PluginId, apiCmd.Path, c.OrgId, apiCmd.FolderId,
|
dashInfo, err := hs.PluginManager.ImportDashboard(apiCmd.PluginId, apiCmd.Path, c.OrgId, apiCmd.FolderId,
|
||||||
apiCmd.Dashboard, apiCmd.Overwrite, apiCmd.Inputs, c.SignedInUser, hs.DataService)
|
apiCmd.Dashboard, apiCmd.Overwrite, apiCmd.Inputs, c.SignedInUser, hs.DataService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -51,7 +51,6 @@ func TestGenerate(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Skip()
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
t.Run(c.Name+" trim default value", func(t *testing.T) {
|
t.Run(c.Name+" trim default value", func(t *testing.T) {
|
||||||
var r cue.Runtime
|
var r cue.Runtime
|
||||||
|
@ -3,6 +3,7 @@ package load
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"cuelang.org/go/cue"
|
"cuelang.org/go/cue"
|
||||||
"cuelang.org/go/cue/load"
|
"cuelang.org/go/cue/load"
|
||||||
@ -149,6 +150,7 @@ func (gvs *genericVersionedSchema) TrimDefaults(r schema.Resource) (schema.Resou
|
|||||||
return r, err
|
return r, err
|
||||||
}
|
}
|
||||||
re, err := convertCUEValueToString(rv)
|
re, err := convertCUEValueToString(rv)
|
||||||
|
fmt.Println("the trimed fields would be: ", re)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return r, err
|
return r, err
|
||||||
}
|
}
|
||||||
@ -171,9 +173,10 @@ func removeDefaultHelper(inputdef cue.Value, input cue.Value) (cue.Value, bool,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return rv, false, err
|
return rv, false, err
|
||||||
}
|
}
|
||||||
|
keySet := make(map[string]bool)
|
||||||
for iter.Next() {
|
for iter.Next() {
|
||||||
lable, _ := iter.Value().Label()
|
lable, _ := iter.Value().Label()
|
||||||
|
keySet[lable] = true
|
||||||
lv := input.LookupPath(cue.MakePath(cue.Str(lable)))
|
lv := input.LookupPath(cue.MakePath(cue.Str(lable)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
@ -185,6 +188,17 @@ func removeDefaultHelper(inputdef cue.Value, input cue.Value) (cue.Value, bool,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Get all the fields that are not defined in schema yet for panel
|
||||||
|
iter, err = input.Fields()
|
||||||
|
if err != nil {
|
||||||
|
return rv, false, err
|
||||||
|
}
|
||||||
|
for iter.Next() {
|
||||||
|
lable, _ := iter.Value().Label()
|
||||||
|
if exists := keySet[lable]; !exists {
|
||||||
|
rv = rv.FillPath(cue.MakePath(cue.Str(lable)), iter.Value())
|
||||||
|
}
|
||||||
|
}
|
||||||
return rv, false, nil
|
return rv, false, nil
|
||||||
case cue.ListKind:
|
case cue.ListKind:
|
||||||
val, _ := inputdef.Default()
|
val, _ := inputdef.Default()
|
||||||
@ -194,7 +208,6 @@ func removeDefaultHelper(inputdef cue.Value, input cue.Value) (cue.Value, bool,
|
|||||||
return rv, true, nil
|
return rv, true, nil
|
||||||
}
|
}
|
||||||
ele := inputdef.LookupPath(cue.MakePath(cue.AnyIndex))
|
ele := inputdef.LookupPath(cue.MakePath(cue.AnyIndex))
|
||||||
fmt.Println("xxxxxxxxxxxxxxxxxxxxx ", ele.IncompleteKind())
|
|
||||||
if ele.IncompleteKind() == cue.BottomKind {
|
if ele.IncompleteKind() == cue.BottomKind {
|
||||||
return rv, true, nil
|
return rv, true, nil
|
||||||
}
|
}
|
||||||
@ -203,17 +216,23 @@ func removeDefaultHelper(inputdef cue.Value, input cue.Value) (cue.Value, bool,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return rv, true, nil
|
return rv, true, nil
|
||||||
}
|
}
|
||||||
index := 0
|
var iterlist []string
|
||||||
for iter.Next() {
|
for iter.Next() {
|
||||||
re, isEqual, err := removeDefaultHelper(ele, iter.Value())
|
re, isEqual, err := removeDefaultHelper(ele, iter.Value())
|
||||||
if err == nil && !isEqual {
|
if err == nil && !isEqual {
|
||||||
rv = rv.FillPath(cue.MakePath(cue.Index(index)), re)
|
reString, err := convertCUEValueToString(re)
|
||||||
index++
|
if err != nil {
|
||||||
|
return rv, true, nil
|
||||||
|
}
|
||||||
|
iterlist = append(iterlist, reString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
iterlistContent := fmt.Sprintf("[%s]", strings.Join(iterlist, ","))
|
||||||
// rv = rv.FillPath(cue.MakePath(cue.Str(lable)), rv)
|
liInstance, err := rt.Compile("resource", []byte(iterlistContent))
|
||||||
return rv, false, nil
|
if err != nil {
|
||||||
|
return rv, false, err
|
||||||
|
}
|
||||||
|
return liInstance.Value(), false, nil
|
||||||
default:
|
default:
|
||||||
val, _ := inputdef.Default()
|
val, _ := inputdef.Default()
|
||||||
err1 := input.Subsume(val)
|
err1 := input.Subsume(val)
|
||||||
|
73
pkg/schema/load/testdata/artifacts/dashboards/trimdefault/test4
vendored
Normal file
73
pkg/schema/load/testdata/artifacts/dashboards/trimdefault/test4
vendored
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
Verifies common usecases for trimdefault/applydefault functions:
|
||||||
|
* open structure should be kept when fields not present
|
||||||
|
|
||||||
|
-- CUE --
|
||||||
|
{
|
||||||
|
templating?: list: [...{...}]
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Full --
|
||||||
|
{
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"allValue": null,
|
||||||
|
"current": {
|
||||||
|
"text": "America",
|
||||||
|
"value": "America"
|
||||||
|
},
|
||||||
|
"datasource": "gdev-postgres",
|
||||||
|
"definition": "",
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Datacenter",
|
||||||
|
"multi": false,
|
||||||
|
"name": "datacenter",
|
||||||
|
"options": [],
|
||||||
|
"query": "SELECT DISTINCT datacenter FROM grafana_metric",
|
||||||
|
"refresh": 1,
|
||||||
|
"regex": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"sort": 1,
|
||||||
|
"tagValuesQuery": "",
|
||||||
|
"tags": [],
|
||||||
|
"tagsQuery": "",
|
||||||
|
"type": "query",
|
||||||
|
"useTags": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Trimed --
|
||||||
|
{
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"allValue": null,
|
||||||
|
"current": {
|
||||||
|
"text": "America",
|
||||||
|
"value": "America"
|
||||||
|
},
|
||||||
|
"datasource": "gdev-postgres",
|
||||||
|
"definition": "",
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": false,
|
||||||
|
"label": "Datacenter",
|
||||||
|
"multi": false,
|
||||||
|
"name": "datacenter",
|
||||||
|
"options": [],
|
||||||
|
"query": "SELECT DISTINCT datacenter FROM grafana_metric",
|
||||||
|
"refresh": 1,
|
||||||
|
"regex": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"sort": 1,
|
||||||
|
"tagValuesQuery": "",
|
||||||
|
"tags": [],
|
||||||
|
"tagsQuery": "",
|
||||||
|
"type": "query",
|
||||||
|
"useTags": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,13 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { Button, Field, Modal, Switch } from '@grafana/ui';
|
import { Button, Field, Modal, Switch } from '@grafana/ui';
|
||||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||||
import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal';
|
import { DashboardExporter } from 'app/features/dashboard/components/DashExportModal';
|
||||||
import { appEvents } from 'app/core/core';
|
import { appEvents } from 'app/core/core';
|
||||||
import { ShowModalReactEvent } from 'app/types/events';
|
import { ShowModalReactEvent } from 'app/types/events';
|
||||||
import { ViewJsonModal } from './ViewJsonModal';
|
import { ViewJsonModal } from './ViewJsonModal';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
@ -15,6 +17,7 @@ interface Props {
|
|||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
shareExternally: boolean;
|
shareExternally: boolean;
|
||||||
|
trimDefaults: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ShareExport extends PureComponent<Props, State> {
|
export class ShareExport extends PureComponent<Props, State> {
|
||||||
@ -24,6 +27,7 @@ export class ShareExport extends PureComponent<Props, State> {
|
|||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
shareExternally: false,
|
shareExternally: false,
|
||||||
|
trimDefaults: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.exporter = new DashboardExporter();
|
this.exporter = new DashboardExporter();
|
||||||
@ -35,29 +39,69 @@ export class ShareExport extends PureComponent<Props, State> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onTrimDefaultsChange = () => {
|
||||||
|
this.setState({
|
||||||
|
trimDefaults: !this.state.trimDefaults,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onSaveAsFile = () => {
|
onSaveAsFile = () => {
|
||||||
const { dashboard } = this.props;
|
const { dashboard } = this.props;
|
||||||
const { shareExternally } = this.state;
|
const { shareExternally } = this.state;
|
||||||
|
const { trimDefaults } = this.state;
|
||||||
|
|
||||||
if (shareExternally) {
|
if (shareExternally) {
|
||||||
this.exporter.makeExportable(dashboard).then((dashboardJson: any) => {
|
this.exporter.makeExportable(dashboard).then((dashboardJson: any) => {
|
||||||
this.openSaveAsDialog(dashboardJson);
|
if (trimDefaults) {
|
||||||
|
getBackendSrv()
|
||||||
|
.post('/api/dashboards/trim', { dashboard: dashboardJson })
|
||||||
|
.then((resp: any) => {
|
||||||
|
this.openSaveAsDialog(resp.dashboard);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.openSaveAsDialog(dashboardJson);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.openSaveAsDialog(dashboard.getSaveModelClone());
|
if (trimDefaults) {
|
||||||
|
getBackendSrv()
|
||||||
|
.post('/api/dashboards/trim', { dashboard: dashboard.getSaveModelClone() })
|
||||||
|
.then((resp: any) => {
|
||||||
|
this.openSaveAsDialog(resp.dashboard);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.openSaveAsDialog(dashboard.getSaveModelClone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onViewJson = () => {
|
onViewJson = () => {
|
||||||
const { dashboard } = this.props;
|
const { dashboard } = this.props;
|
||||||
const { shareExternally } = this.state;
|
const { shareExternally } = this.state;
|
||||||
|
const { trimDefaults } = this.state;
|
||||||
|
|
||||||
if (shareExternally) {
|
if (shareExternally) {
|
||||||
this.exporter.makeExportable(dashboard).then((dashboardJson: any) => {
|
this.exporter.makeExportable(dashboard).then((dashboardJson: any) => {
|
||||||
this.openJsonModal(dashboardJson);
|
if (trimDefaults) {
|
||||||
|
getBackendSrv()
|
||||||
|
.post('/api/dashboards/trim', { dashboard: dashboardJson })
|
||||||
|
.then((resp: any) => {
|
||||||
|
this.openJsonModal(resp.dashboard);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.openJsonModal(dashboardJson);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.openJsonModal(dashboard.getSaveModelClone());
|
if (trimDefaults) {
|
||||||
|
getBackendSrv()
|
||||||
|
.post('/api/dashboards/trim', { dashboard: dashboard.getSaveModelClone() })
|
||||||
|
.then((resp: any) => {
|
||||||
|
this.openJsonModal(resp.dashboard);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.openJsonModal(dashboard.getSaveModelClone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -86,6 +130,7 @@ export class ShareExport extends PureComponent<Props, State> {
|
|||||||
render() {
|
render() {
|
||||||
const { onDismiss } = this.props;
|
const { onDismiss } = this.props;
|
||||||
const { shareExternally } = this.state;
|
const { shareExternally } = this.state;
|
||||||
|
const { trimDefaults } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -93,6 +138,11 @@ export class ShareExport extends PureComponent<Props, State> {
|
|||||||
<Field label="Export for sharing externally">
|
<Field label="Export for sharing externally">
|
||||||
<Switch value={shareExternally} onChange={this.onShareExternallyChange} />
|
<Switch value={shareExternally} onChange={this.onShareExternallyChange} />
|
||||||
</Field>
|
</Field>
|
||||||
|
{config.featureToggles.trimDefaults && (
|
||||||
|
<Field label="Export with trimed dashboard json">
|
||||||
|
<Switch value={trimDefaults} onChange={this.onTrimDefaultsChange} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
<Modal.ButtonRow>
|
<Modal.ButtonRow>
|
||||||
<Button variant="secondary" onClick={onDismiss} fill="outline">
|
<Button variant="secondary" onClick={onDismiss} fill="outline">
|
||||||
Cancel
|
Cancel
|
||||||
|
Reference in New Issue
Block a user