mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 05:21:50 +08:00
Dashboard: Add dashboard validation warning to save drawer (#55732)
* add api route for validating a dashboard json * add feature flag for showDashboardValidationWarnings * tidy up * comments and messages * swagger specs * fix typo * more swagger * tests! * tidy test a little bit * no more ioutil * api will return different status code depending on validation error * clean up * handle 4xx errors * remove console.log * fix backend tests * tidy up * Swagger: Exclude alpha endpoints Co-authored-by: Sofia Papagiannaki <1632407+papagian@users.noreply.github.com>
This commit is contained in:
3
Makefile
3
Makefile
@ -46,7 +46,8 @@ $(SPEC_TARGET): $(SWAGGER) ## Generate API Swagger specification
|
|||||||
SWAGGER_GENERATE_EXTENSION=false $(SWAGGER) generate spec -m -w pkg/server -o $(SPEC_TARGET) \
|
SWAGGER_GENERATE_EXTENSION=false $(SWAGGER) generate spec -m -w pkg/server -o $(SPEC_TARGET) \
|
||||||
-x "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" \
|
-x "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" \
|
||||||
-x "github.com/prometheus/alertmanager" \
|
-x "github.com/prometheus/alertmanager" \
|
||||||
-i pkg/api/swagger_tags.json
|
-i pkg/api/swagger_tags.json \
|
||||||
|
--exclude-tag=alpha
|
||||||
|
|
||||||
swagger-api-spec: gen-go $(SPEC_TARGET) $(MERGED_SPEC_TARGET) validate-api-spec
|
swagger-api-spec: gen-go $(SPEC_TARGET) $(MERGED_SPEC_TARGET) validate-api-spec
|
||||||
|
|
||||||
|
@ -74,5 +74,6 @@ export interface FeatureToggles {
|
|||||||
increaseInMemDatabaseQueryCache?: boolean;
|
increaseInMemDatabaseQueryCache?: boolean;
|
||||||
newPanelChromeUI?: boolean;
|
newPanelChromeUI?: boolean;
|
||||||
queryLibrary?: boolean;
|
queryLibrary?: boolean;
|
||||||
|
showDashboardValidationWarnings?: boolean;
|
||||||
mysqlAnsiQuotes?: boolean;
|
mysqlAnsiQuotes?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -469,6 +469,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
dashboardRoute.Post("/calculate-diff", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff))
|
dashboardRoute.Post("/calculate-diff", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.CalculateDashboardDiff))
|
||||||
|
dashboardRoute.Post("/validate", authorize(reqSignedIn, ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.ValidateDashboard))
|
||||||
dashboardRoute.Post("/trim", routing.Wrap(hs.TrimDashboard))
|
dashboardRoute.Post("/trim", routing.Wrap(hs.TrimDashboard))
|
||||||
|
|
||||||
dashboardRoute.Post("/db", authorize(reqSignedIn, ac.EvalAny(ac.EvalPermission(dashboards.ActionDashboardsCreate), ac.EvalPermission(dashboards.ActionDashboardsWrite))), routing.Wrap(hs.PostDashboard))
|
dashboardRoute.Post("/db", authorize(reqSignedIn, ac.EvalAny(ac.EvalPermission(dashboards.ActionDashboardsCreate), ac.EvalPermission(dashboards.ActionDashboardsWrite))), routing.Wrap(hs.PostDashboard))
|
||||||
|
@ -752,6 +752,70 @@ func (hs *HTTPServer) GetDashboardVersion(c *models.ReqContext) response.Respons
|
|||||||
return response.JSON(http.StatusOK, dashVersionMeta)
|
return response.JSON(http.StatusOK, dashVersionMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swagger:route POST /dashboards/validate dashboards alpha validateDashboard
|
||||||
|
//
|
||||||
|
// Validates a dashboard JSON against the schema.
|
||||||
|
//
|
||||||
|
// Produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// Responses:
|
||||||
|
// 200: validateDashboardResponse
|
||||||
|
// 412: validateDashboardResponse
|
||||||
|
// 422: validateDashboardResponse
|
||||||
|
// 401: unauthorisedError
|
||||||
|
// 403: forbiddenError
|
||||||
|
// 500: internalServerError
|
||||||
|
func (hs *HTTPServer) ValidateDashboard(c *models.ReqContext) response.Response {
|
||||||
|
cmd := models.ValidateDashboardCommand{}
|
||||||
|
|
||||||
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
||||||
|
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cm := hs.Coremodels.Dashboard()
|
||||||
|
dashboardBytes := []byte(cmd.Dashboard)
|
||||||
|
|
||||||
|
// POST api receives dashboard as a string of json (so line numbers for errors stay consistent),
|
||||||
|
// but we need to parse the schema version out of it
|
||||||
|
dashboardJson, err := simplejson.NewJson(dashboardBytes)
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(http.StatusBadRequest, "unable to parse dashboard", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaVersion, err := dashboardJson.Get("schemaVersion").Int()
|
||||||
|
|
||||||
|
isValid := false
|
||||||
|
statusCode := http.StatusOK
|
||||||
|
validationMessage := ""
|
||||||
|
|
||||||
|
// Only try to validate if the schemaVersion is at least the handoff version
|
||||||
|
// (the minimum schemaVersion against which the dashboard schema is known to
|
||||||
|
// work), or if schemaVersion is absent (which will happen once the Thema
|
||||||
|
// schema becomes canonical).
|
||||||
|
if err != nil || schemaVersion >= dashboard.HandoffSchemaVersion {
|
||||||
|
v, _ := cuectx.JSONtoCUE("dashboard.json", dashboardBytes)
|
||||||
|
_, validationErr := cm.CurrentSchema().Validate(v)
|
||||||
|
|
||||||
|
if validationErr == nil {
|
||||||
|
isValid = true
|
||||||
|
} else {
|
||||||
|
validationMessage = validationErr.Error()
|
||||||
|
statusCode = http.StatusUnprocessableEntity
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
validationMessage = "invalid schema version"
|
||||||
|
statusCode = http.StatusPreconditionFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
respData := &ValidateDashboardResponse{
|
||||||
|
IsValid: isValid,
|
||||||
|
Message: validationMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(statusCode, respData)
|
||||||
|
}
|
||||||
|
|
||||||
// swagger:route POST /dashboards/calculate-diff dashboards calculateDashboardDiff
|
// swagger:route POST /dashboards/calculate-diff dashboards calculateDashboardDiff
|
||||||
//
|
//
|
||||||
// Perform diff on two dashboards.
|
// Perform diff on two dashboards.
|
||||||
@ -1185,3 +1249,9 @@ type DashboardVersionResponse struct {
|
|||||||
// in: body
|
// in: body
|
||||||
Body *dashver.DashboardVersionMeta `json:"body"`
|
Body *dashver.DashboardVersionMeta `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// swagger:response validateDashboardResponse
|
||||||
|
type ValidateDashboardResponse struct {
|
||||||
|
IsValid bool `json:"isValid"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
@ -728,6 +728,60 @@ func TestDashboardAPIEndpoint(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Given a dashboard to validate", func(t *testing.T) {
|
||||||
|
sqlmock := mockstore.SQLStoreMock{}
|
||||||
|
|
||||||
|
t.Run("When an invalid dashboard json is posted", func(t *testing.T) {
|
||||||
|
cmd := models.ValidateDashboardCommand{
|
||||||
|
Dashboard: "{\"hello\": \"world\"}",
|
||||||
|
}
|
||||||
|
|
||||||
|
role := org.RoleAdmin
|
||||||
|
postValidateScenario(t, "When calling POST on", "/api/dashboards/validate", "/api/dashboards/validate", cmd, role, func(sc *scenarioContext) {
|
||||||
|
callPostDashboard(sc)
|
||||||
|
|
||||||
|
result := sc.ToJSON()
|
||||||
|
assert.Equal(t, 422, sc.resp.Code)
|
||||||
|
assert.False(t, result.Get("isValid").MustBool())
|
||||||
|
assert.NotEmpty(t, result.Get("message").MustString())
|
||||||
|
}, &sqlmock)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("When a dashboard with a too-low schema version is posted", func(t *testing.T) {
|
||||||
|
cmd := models.ValidateDashboardCommand{
|
||||||
|
Dashboard: "{\"schemaVersion\": 1}",
|
||||||
|
}
|
||||||
|
|
||||||
|
role := org.RoleAdmin
|
||||||
|
postValidateScenario(t, "When calling POST on", "/api/dashboards/validate", "/api/dashboards/validate", cmd, role, func(sc *scenarioContext) {
|
||||||
|
callPostDashboard(sc)
|
||||||
|
|
||||||
|
result := sc.ToJSON()
|
||||||
|
assert.Equal(t, 412, sc.resp.Code)
|
||||||
|
assert.False(t, result.Get("isValid").MustBool())
|
||||||
|
assert.Equal(t, "invalid schema version", result.Get("message").MustString())
|
||||||
|
}, &sqlmock)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("When a valid dashboard is posted", func(t *testing.T) {
|
||||||
|
devenvDashboard, readErr := os.ReadFile("../../devenv/dev-dashboards/home.json")
|
||||||
|
assert.Empty(t, readErr)
|
||||||
|
|
||||||
|
cmd := models.ValidateDashboardCommand{
|
||||||
|
Dashboard: string(devenvDashboard),
|
||||||
|
}
|
||||||
|
|
||||||
|
role := org.RoleAdmin
|
||||||
|
postValidateScenario(t, "When calling POST on", "/api/dashboards/validate", "/api/dashboards/validate", cmd, role, func(sc *scenarioContext) {
|
||||||
|
callPostDashboard(sc)
|
||||||
|
|
||||||
|
result := sc.ToJSON()
|
||||||
|
assert.Equal(t, 200, sc.resp.Code)
|
||||||
|
assert.True(t, result.Get("isValid").MustBool())
|
||||||
|
}, &sqlmock)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Given two dashboards being compared", func(t *testing.T) {
|
t.Run("Given two dashboards being compared", func(t *testing.T) {
|
||||||
fakeDashboardVersionService := dashvertest.NewDashboardVersionServiceFake()
|
fakeDashboardVersionService := dashvertest.NewDashboardVersionServiceFake()
|
||||||
fakeDashboardVersionService.ExpectedDashboardVersions = []*dashver.DashboardVersion{
|
fakeDashboardVersionService.ExpectedDashboardVersions = []*dashver.DashboardVersion{
|
||||||
@ -1053,6 +1107,42 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func postValidateScenario(t *testing.T, desc string, url string, routePattern string, cmd models.ValidateDashboardCommand,
|
||||||
|
role org.RoleType, fn scenarioFunc, sqlmock sqlstore.Store) {
|
||||||
|
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||||
|
cfg := setting.NewCfg()
|
||||||
|
hs := HTTPServer{
|
||||||
|
Cfg: cfg,
|
||||||
|
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
|
||||||
|
Live: newTestLive(t, sqlstore.InitTestDB(t)),
|
||||||
|
QuotaService: "aimpl.Service{Cfg: cfg},
|
||||||
|
LibraryPanelService: &mockLibraryPanelService{},
|
||||||
|
LibraryElementService: &mockLibraryElementService{},
|
||||||
|
SQLStore: sqlmock,
|
||||||
|
Features: featuremgmt.WithFeatures(),
|
||||||
|
Coremodels: registry.NewBase(nil),
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := setupScenarioContext(t, url)
|
||||||
|
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
|
||||||
|
c.Req.Body = mockRequestBody(cmd)
|
||||||
|
c.Req.Header.Add("Content-Type", "application/json")
|
||||||
|
sc.context = c
|
||||||
|
sc.context.SignedInUser = &user.SignedInUser{
|
||||||
|
OrgID: testOrgID,
|
||||||
|
UserID: testUserID,
|
||||||
|
}
|
||||||
|
sc.context.OrgRole = role
|
||||||
|
|
||||||
|
return hs.ValidateDashboard(c)
|
||||||
|
})
|
||||||
|
|
||||||
|
sc.m.Post(routePattern, sc.defaultHandler)
|
||||||
|
|
||||||
|
fn(sc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func postDiffScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions,
|
func postDiffScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions,
|
||||||
role org.RoleType, fn scenarioFunc, sqlmock sqlstore.Store, fakeDashboardVersionService *dashvertest.FakeDashboardVersionService) {
|
role org.RoleType, fn scenarioFunc, sqlmock sqlstore.Store, fakeDashboardVersionService *dashvertest.FakeDashboardVersionService) {
|
||||||
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
|
||||||
|
@ -224,6 +224,10 @@ type SaveDashboardCommand struct {
|
|||||||
Result *Dashboard `json:"-"`
|
Result *Dashboard `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ValidateDashboardCommand struct {
|
||||||
|
Dashboard string `json:"dashboard" binding:"Required"`
|
||||||
|
}
|
||||||
|
|
||||||
type TrimDashboardCommand struct {
|
type TrimDashboardCommand struct {
|
||||||
Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
|
Dashboard *simplejson.Json `json:"dashboard" binding:"Required"`
|
||||||
Meta *simplejson.Json `json:"meta"`
|
Meta *simplejson.Json `json:"meta"`
|
||||||
|
@ -322,6 +322,10 @@ var (
|
|||||||
State: FeatureStateAlpha,
|
State: FeatureStateAlpha,
|
||||||
RequiresDevMode: true,
|
RequiresDevMode: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "showDashboardValidationWarnings",
|
||||||
|
Description: "Show warnings when Dashboards do not validate against the schema",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "mysqlAnsiQuotes",
|
Name: "mysqlAnsiQuotes",
|
||||||
Description: "Use double quote to escape keyword in Mysql query",
|
Description: "Use double quote to escape keyword in Mysql query",
|
||||||
|
@ -239,6 +239,10 @@ const (
|
|||||||
// Reusable query library
|
// Reusable query library
|
||||||
FlagQueryLibrary = "queryLibrary"
|
FlagQueryLibrary = "queryLibrary"
|
||||||
|
|
||||||
|
// FlagShowDashboardValidationWarnings
|
||||||
|
// Show warnings when Dashboards do not validate against the schema
|
||||||
|
FlagShowDashboardValidationWarnings = "showDashboardValidationWarnings"
|
||||||
|
|
||||||
// FlagMysqlAnsiQuotes
|
// FlagMysqlAnsiQuotes
|
||||||
// Use double quote to escape keyword in Mysql query
|
// Use double quote to escape keyword in Mysql query
|
||||||
FlagMysqlAnsiQuotes = "mysqlAnsiQuotes"
|
FlagMysqlAnsiQuotes = "mysqlAnsiQuotes"
|
||||||
|
@ -20075,6 +20075,17 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/UserProfileDTO"
|
"$ref": "#/definitions/UserProfileDTO"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"validateDashboardResponse": {
|
||||||
|
"description": "(empty)",
|
||||||
|
"headers": {
|
||||||
|
"isValid": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securityDefinitions": {
|
"securityDefinitions": {
|
||||||
|
@ -16115,6 +16115,17 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/UserProfileDTO"
|
"$ref": "#/definitions/UserProfileDTO"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"validateDashboardResponse": {
|
||||||
|
"description": "",
|
||||||
|
"headers": {
|
||||||
|
"isValid": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securityDefinitions": {
|
"securityDefinitions": {
|
||||||
|
@ -18,6 +18,7 @@ import { BackendSrv as BackendService, BackendSrvRequest, config, FetchError, Fe
|
|||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { getConfig } from 'app/core/config';
|
import { getConfig } from 'app/core/config';
|
||||||
import { loadUrlToken } from 'app/core/utils/urlToken';
|
import { loadUrlToken } from 'app/core/utils/urlToken';
|
||||||
|
import { DashboardModel } from 'app/features/dashboard/state';
|
||||||
import { DashboardSearchItem } from 'app/features/search/types';
|
import { DashboardSearchItem } from 'app/features/search/types';
|
||||||
import { getGrafanaStorage } from 'app/features/storage/storage';
|
import { getGrafanaStorage } from 'app/features/storage/storage';
|
||||||
import { TokenRevokedModal } from 'app/features/users/TokenRevokedModal';
|
import { TokenRevokedModal } from 'app/features/users/TokenRevokedModal';
|
||||||
@ -455,6 +456,19 @@ export class BackendSrv implements BackendService {
|
|||||||
return this.get<DashboardDTO>(`/api/dashboards/uid/${uid}`);
|
return this.get<DashboardDTO>(`/api/dashboards/uid/${uid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateDashboard(dashboard: DashboardModel) {
|
||||||
|
// We want to send the dashboard as a JSON string (in the JSON body payload) so we can get accurate error line numbers back
|
||||||
|
const dashboardJson = JSON.stringify(dashboard, replaceJsonNulls, 2);
|
||||||
|
|
||||||
|
return this.request<ValidateDashboardResponse>({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/api/dashboards/validate`,
|
||||||
|
data: { dashboard: dashboardJson },
|
||||||
|
showSuccessAlert: false,
|
||||||
|
showErrorAlert: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getPublicDashboardByUid(uid: string) {
|
getPublicDashboardByUid(uid: string) {
|
||||||
return this.get<DashboardDTO>(`/api/public/dashboards/${uid}`);
|
return this.get<DashboardDTO>(`/api/public/dashboards/${uid}`);
|
||||||
}
|
}
|
||||||
@ -472,3 +486,15 @@ export class BackendSrv implements BackendService {
|
|||||||
// Used for testing and things that really need BackendSrv
|
// Used for testing and things that really need BackendSrv
|
||||||
export const backendSrv = new BackendSrv();
|
export const backendSrv = new BackendSrv();
|
||||||
export const getBackendSrv = (): BackendSrv => backendSrv;
|
export const getBackendSrv = (): BackendSrv => backendSrv;
|
||||||
|
|
||||||
|
interface ValidateDashboardResponse {
|
||||||
|
isValid: boolean;
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceJsonNulls<T extends unknown>(key: string, value: T): T | undefined {
|
||||||
|
if (typeof value === 'number' && !Number.isFinite(value)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { FetchError } from '@grafana/runtime';
|
||||||
|
import { Alert, useStyles2 } from '@grafana/ui';
|
||||||
|
import { backendSrv } from 'app/core/services/backend_srv';
|
||||||
|
|
||||||
|
import { DashboardModel } from '../../state';
|
||||||
|
|
||||||
|
interface DashboardValidationProps {
|
||||||
|
dashboard: DashboardModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValidationResponse = Awaited<ReturnType<typeof backendSrv.validateDashboard>>;
|
||||||
|
|
||||||
|
function DashboardValidation({ dashboard }: DashboardValidationProps) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const { loading, value, error } = useAsync(async () => {
|
||||||
|
const saveModel = dashboard.getSaveModelClone();
|
||||||
|
const respPromise = backendSrv
|
||||||
|
.validateDashboard(saveModel)
|
||||||
|
// API returns schema validation errors in 4xx range, so resolve them rather than throwing
|
||||||
|
.catch((err: FetchError<ValidationResponse>) => {
|
||||||
|
if (err.status >= 500) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return err.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
return respPromise;
|
||||||
|
}, [dashboard]);
|
||||||
|
|
||||||
|
let alert: React.ReactNode;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
alert = <Alert severity="info" title="Checking dashboard validity" />;
|
||||||
|
} else if (value) {
|
||||||
|
if (!value.isValid) {
|
||||||
|
alert = (
|
||||||
|
<Alert severity="warning" title="Dashboard failed schema validation">
|
||||||
|
<p>
|
||||||
|
Validation is provided for development purposes and should be safe to ignore. If you are a Grafana
|
||||||
|
developer, consider checking and updating the dashboard schema
|
||||||
|
</p>
|
||||||
|
<div className={styles.error}>{value.message}</div>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const errorMessage = error?.message ?? 'Unknown error';
|
||||||
|
alert = (
|
||||||
|
<Alert severity="info" title="Error checking dashboard validity">
|
||||||
|
<p className={styles.error}>{errorMessage}</p>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alert) {
|
||||||
|
return <div className={styles.root}>{alert}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
root: css({
|
||||||
|
marginTop: theme.spacing(1),
|
||||||
|
}),
|
||||||
|
error: css({
|
||||||
|
fontFamily: theme.typography.fontFamilyMonospace,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
overflowX: 'auto',
|
||||||
|
maxWidth: '100%',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default DashboardValidation;
|
@ -7,6 +7,7 @@ import { backendSrv } from 'app/core/services/backend_srv';
|
|||||||
|
|
||||||
import { jsonDiff } from '../VersionHistory/utils';
|
import { jsonDiff } from '../VersionHistory/utils';
|
||||||
|
|
||||||
|
import DashboardValidation from './DashboardValidation';
|
||||||
import { SaveDashboardDiff } from './SaveDashboardDiff';
|
import { SaveDashboardDiff } from './SaveDashboardDiff';
|
||||||
import { SaveDashboardErrorProxy } from './SaveDashboardErrorProxy';
|
import { SaveDashboardErrorProxy } from './SaveDashboardErrorProxy';
|
||||||
import { SaveDashboardAsForm } from './forms/SaveDashboardAsForm';
|
import { SaveDashboardAsForm } from './forms/SaveDashboardAsForm';
|
||||||
@ -68,7 +69,7 @@ export const SaveDashboardDrawer = ({ dashboard, onDismiss, onSaveSuccess, isCop
|
|||||||
}
|
}
|
||||||
: onDismiss;
|
: onDismiss;
|
||||||
|
|
||||||
const renderBody = () => {
|
const renderSaveBody = () => {
|
||||||
if (showDiff) {
|
if (showDiff) {
|
||||||
return <SaveDashboardDiff diff={data.diff} oldValue={previous.value} newValue={data.clone} />;
|
return <SaveDashboardDiff diff={data.diff} oldValue={previous.value} newValue={data.clone} />;
|
||||||
}
|
}
|
||||||
@ -161,7 +162,9 @@ export const SaveDashboardDrawer = ({ dashboard, onDismiss, onSaveSuccess, isCop
|
|||||||
expandable
|
expandable
|
||||||
scrollableContent
|
scrollableContent
|
||||||
>
|
>
|
||||||
{renderBody()}
|
{renderSaveBody()}
|
||||||
|
|
||||||
|
{config.featureToggles.showDashboardValidationWarnings && <DashboardValidation dashboard={dashboard} />}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1731,6 +1731,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "(empty)"
|
"description": "(empty)"
|
||||||
|
},
|
||||||
|
"validateDashboardResponse": {
|
||||||
|
"description": "(empty)",
|
||||||
|
"headers": {
|
||||||
|
"isValid": {
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemas": {
|
"schemas": {
|
||||||
|
Reference in New Issue
Block a user