Browse Source

fix(dashboards): Flatten serializer validation errors (#33314)

Flatten deeply nested serializer validation errors so that
addErrorMessage can consumne it and doesn't break the entire
page.
Shruthi 2 years ago
parent
commit
20b47c3b87

+ 9 - 4
static/app/actionCreators/dashboards.tsx

@@ -4,6 +4,7 @@ import {addErrorMessage} from 'sentry/actionCreators/indicator';
 import {Client} from 'sentry/api';
 import {t} from 'sentry/locale';
 import {DashboardDetails, Widget} from 'sentry/views/dashboardsV2/types';
+import {flattenErrors} from 'sentry/views/dashboardsV2/utils';
 
 export function createDashboard(
   api: Client,
@@ -25,7 +26,8 @@ export function createDashboard(
     const errorResponse = response?.responseJSON ?? null;
 
     if (errorResponse) {
-      addErrorMessage(errorResponse);
+      const errors = flattenErrors(errorResponse, {});
+      addErrorMessage(errors[Object.keys(errors)[0]]);
     } else {
       addErrorMessage(t('Unable to create dashboard'));
     }
@@ -65,7 +67,8 @@ export function fetchDashboard(
     const errorResponse = response?.responseJSON ?? null;
 
     if (errorResponse) {
-      addErrorMessage(errorResponse);
+      const errors = flattenErrors(errorResponse, {});
+      addErrorMessage(errors[Object.keys(errors)[0]]);
     } else {
       addErrorMessage(t('Unable to load dashboard'));
     }
@@ -95,7 +98,8 @@ export function updateDashboard(
     const errorResponse = response?.responseJSON ?? null;
 
     if (errorResponse) {
-      addErrorMessage(errorResponse);
+      const errors = flattenErrors(errorResponse, {});
+      addErrorMessage(errors[Object.keys(errors)[0]]);
     } else {
       addErrorMessage(t('Unable to update dashboard'));
     }
@@ -120,7 +124,8 @@ export function deleteDashboard(
     const errorResponse = response?.responseJSON ?? null;
 
     if (errorResponse) {
-      addErrorMessage(errorResponse);
+      const errors = flattenErrors(errorResponse, {});
+      addErrorMessage(errors[Object.keys(errors)[0]]);
     } else {
       addErrorMessage(t('Unable to delete dashboard'));
     }

+ 11 - 8
static/app/views/dashboardsV2/create.tsx

@@ -3,6 +3,7 @@ import {browserHistory, RouteComponentProps} from 'react-router';
 
 import Feature from 'sentry/components/acl/feature';
 import Alert from 'sentry/components/alert';
+import ErrorBoundary from 'sentry/components/errorBoundary';
 import {t} from 'sentry/locale';
 import {PageContent} from 'sentry/styles/organization';
 import {Organization} from 'sentry/types';
@@ -48,14 +49,16 @@ function CreateDashboard(props: Props) {
       organization={props.organization}
       renderDisabled={renderDisabled}
     >
-      <DashboardDetail
-        {...props}
-        initialState={initialState}
-        dashboard={dashboard}
-        dashboards={[]}
-        newWidget={newWidget}
-        onSetNewWidget={() => setNewWidget(undefined)}
-      />
+      <ErrorBoundary>
+        <DashboardDetail
+          {...props}
+          initialState={initialState}
+          dashboard={dashboard}
+          dashboards={[]}
+          newWidget={newWidget}
+          onSetNewWidget={() => setNewWidget(undefined)}
+        />
+      </ErrorBoundary>
     </Feature>
   );
 }

+ 10 - 7
static/app/views/dashboardsV2/index.tsx

@@ -2,6 +2,7 @@ import {Fragment} from 'react';
 import {RouteComponentProps} from 'react-router';
 
 import {Client} from 'sentry/api';
+import ErrorBoundary from 'sentry/components/errorBoundary';
 import NotFound from 'sentry/components/errors/notFound';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {Organization} from 'sentry/types';
@@ -38,13 +39,15 @@ function DashboardsV2Container(props: Props) {
           return error ? (
             <NotFound />
           ) : dashboard ? (
-            <DashboardDetail
-              {...props}
-              initialState={DashboardState.VIEW}
-              dashboard={dashboard}
-              dashboards={dashboards}
-              onDashboardUpdate={onDashboardUpdate}
-            />
+            <ErrorBoundary>
+              <DashboardDetail
+                {...props}
+                initialState={DashboardState.VIEW}
+                dashboard={dashboard}
+                dashboards={dashboards}
+                onDashboardUpdate={onDashboardUpdate}
+              />
+            </ErrorBoundary>
           ) : (
             <LoadingIndicator />
           );

+ 32 - 0
static/app/views/dashboardsV2/utils.tsx

@@ -30,6 +30,14 @@ import {
   WidgetType,
 } from 'sentry/views/dashboardsV2/types';
 
+export type ValidationError = {
+  [key: string]: string | string[] | ValidationError[] | ValidationError;
+};
+
+export type FlatValidationError = {
+  [key: string]: string | FlatValidationError[] | FlatValidationError;
+};
+
 export function cloneDashboard(dashboard: DashboardDetails): DashboardDetails {
   return cloneDeep(dashboard);
 }
@@ -254,3 +262,27 @@ export function getWidgetIssueUrl(
   })}`;
   return issuesLocation;
 }
+
+export function flattenErrors(
+  data: ValidationError,
+  update: FlatValidationError
+): FlatValidationError {
+  Object.keys(data).forEach((key: string) => {
+    const value = data[key];
+    if (typeof value === 'string') {
+      update[key] = value;
+      return;
+    }
+    // Recurse into nested objects.
+    if (Array.isArray(value) && typeof value[0] === 'string') {
+      update[key] = value[0];
+      return;
+    }
+    if (Array.isArray(value) && typeof value[0] === 'object') {
+      (value as ValidationError[]).map(item => flattenErrors(item, update));
+    } else {
+      flattenErrors(value as ValidationError, update);
+    }
+  });
+  return update;
+}

+ 12 - 9
static/app/views/dashboardsV2/view.tsx

@@ -5,6 +5,7 @@ import pick from 'lodash/pick';
 import {updateDashboardVisit} from 'sentry/actionCreators/dashboards';
 import Feature from 'sentry/components/acl/feature';
 import Alert from 'sentry/components/alert';
+import ErrorBoundary from 'sentry/components/errorBoundary';
 import NotFound from 'sentry/components/errors/notFound';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {t} from 'sentry/locale';
@@ -64,15 +65,17 @@ function ViewEditDashboard(props: Props) {
           return error ? (
             <NotFound />
           ) : dashboard ? (
-            <DashboardDetail
-              {...props}
-              initialState={newWidget ? DashboardState.EDIT : DashboardState.VIEW}
-              dashboard={dashboard}
-              dashboards={dashboards}
-              onDashboardUpdate={onDashboardUpdate}
-              newWidget={newWidget}
-              onSetNewWidget={() => setNewWidget(undefined)}
-            />
+            <ErrorBoundary>
+              <DashboardDetail
+                {...props}
+                initialState={newWidget ? DashboardState.EDIT : DashboardState.VIEW}
+                dashboard={dashboard}
+                dashboards={dashboards}
+                onDashboardUpdate={onDashboardUpdate}
+                newWidget={newWidget}
+                onSetNewWidget={() => setNewWidget(undefined)}
+              />
+            </ErrorBoundary>
           ) : (
             <LoadingIndicator />
           );

+ 2 - 8
static/app/views/dashboardsV2/widgetBuilder/utils.tsx

@@ -19,6 +19,8 @@ import {
 import {generateFieldOptions} from 'sentry/views/eventsV2/utils';
 import {IssueSortOptions} from 'sentry/views/issueList/utils';
 
+import {FlatValidationError, ValidationError} from '../utils';
+
 // Used in the widget builder to limit the number of lines plotted in the chart
 export const DEFAULT_RESULTS_LIMIT = 5;
 export const RESULTS_LIMIT = 10;
@@ -49,14 +51,6 @@ export const displayTypes = {
   [DisplayType.TOP_N]: t('Top 5 Events'),
 };
 
-type ValidationError = {
-  [key: string]: string | string[] | ValidationError[] | ValidationError;
-};
-
-export type FlatValidationError = {
-  [key: string]: string | FlatValidationError[] | FlatValidationError;
-};
-
 export function mapErrors(
   data: ValidationError,
   update: FlatValidationError