Browse Source

feat(new-widget-builder-experience): Port create widget from discover experience - (#31957)

Priscila Oliveira 3 years ago
parent
commit
6402ebf223

+ 17 - 2
static/app/views/dashboardsV2/widgetBuilder/buildStep.tsx

@@ -8,13 +8,17 @@ type Props = {
   children: React.ReactNode;
   description: string;
   title: string;
+  required?: boolean;
 };
 
-function BuildStep({title, description, children}: Props) {
+function BuildStep({title, description, required = false, children}: Props) {
   return (
     <Wrapper>
       <Header>
-        <Heading>{title}</Heading>
+        <Heading>
+          {title}
+          {required && <RequiredBadge />}
+        </Heading>
         <SubHeading>{description}</SubHeading>
       </Header>
       <Content>{children}</Content>
@@ -47,3 +51,14 @@ const SubHeading = styled('small')`
 const Content = styled('div')`
   display: grid;
 `;
+
+const RequiredBadge = styled('div')`
+  background: ${p => p.theme.red300};
+  opacity: 0.6;
+  width: 5px;
+  height: 5px;
+  border-radius: 5px;
+  margin-left: ${space(0.5)};
+  display: inline-block;
+  vertical-align: super;
+`;

+ 1 - 1
static/app/views/dashboardsV2/widgetBuilder/dashboardSelector.tsx

@@ -17,7 +17,7 @@ interface Props {
 
 export function DashboardSelector({dashboards, disabled, onChange, error}: Props) {
   return (
-    <Field inline={false} flexibleControlStateSize stacked error={error} required>
+    <Field inline={false} flexibleControlStateSize stacked error={error}>
       <SelectControl
         menuPlacement="auto"
         name="dashboard"

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

@@ -15,12 +15,11 @@ type Props = {
   dashboardTitle: DashboardDetails['title'];
   goBackLocation: React.ComponentProps<typeof Link>['to'];
   onChangeTitle: (title: string) => void;
+  onSave: (event: React.MouseEvent) => void;
   orgSlug: string;
   title: string;
-  disabled?: boolean;
   isEditing?: boolean;
   onDelete?: () => void;
-  onSave?: (event: React.MouseEvent) => void;
 };
 
 export function Header({
@@ -85,12 +84,7 @@ export function Header({
               <Button priority="danger">{t('Delete')}</Button>
             </Confirm>
           )}
-          <Button
-            priority="primary"
-            onClick={onSave}
-            disabled={!onSave}
-            title={!onSave ? t('This feature is not yet available') : undefined}
-          >
+          <Button priority="primary" onClick={onSave}>
             {isEditing ? t('Update Widget') : t('Add Widget')}
           </Button>
         </ButtonBar>

+ 16 - 1
static/app/views/dashboardsV2/widgetBuilder/utils.tsx

@@ -6,7 +6,7 @@ import {
   isAggregateFieldOrEquation,
   isLegalYAxisType,
 } from 'sentry/utils/discover/fields';
-import {Widget} from 'sentry/views/dashboardsV2/types';
+import {Widget, WidgetQuery} from 'sentry/views/dashboardsV2/types';
 
 export enum DisplayType {
   AREA = 'area',
@@ -160,3 +160,18 @@ export function normalizeQueries(
 
   return queries;
 }
+
+export function getParsedDefaultWidgetQuery(query = ''): WidgetQuery | undefined {
+  // "any" was needed here because it doesn't pass in getsentry
+  const urlSeachParams = new URLSearchParams(query) as any;
+  const parsedQuery = Object.fromEntries(urlSeachParams.entries());
+
+  if (!Object.keys(parsedQuery).length) {
+    return undefined;
+  }
+
+  return {
+    ...parsedQuery,
+    fields: parsedQuery.fields?.split(',') ?? [],
+  } as WidgetQuery;
+}

+ 111 - 86
static/app/views/dashboardsV2/widgetBuilder/widgetBuilder.tsx

@@ -78,12 +78,14 @@ import {Header} from './header';
 import {
   DataSet,
   DisplayType,
-  FlatValidationError,
+  getParsedDefaultWidgetQuery,
   mapErrors,
   normalizeQueries,
 } from './utils';
 import {YAxisSelector} from './yAxisSelector';
 
+const NEW_DASHBOARD_ID = 'new';
+
 const DATASET_CHOICES: [DataSet, string][] = [
   [DataSet.EVENTS, t('All Events (Errors and Transactions)')],
   [DataSet.ISSUES, t('Issues (States, Assignment, Time, etc.)')],
@@ -136,9 +138,6 @@ type Props = RouteComponentProps<RouteParams, {}> & {
   organization: Organization;
   selection: PageFilters;
   tags: TagCollection;
-  defaultTableColumns?: readonly string[];
-  defaultTitle?: string;
-  defaultWidgetQuery?: WidgetQuery;
   displayType?: DisplayType;
   end?: DateString;
   start?: DateString;
@@ -169,16 +168,15 @@ function WidgetBuilder({
   start,
   end,
   statsPeriod,
-  defaultWidgetQuery,
-  displayType,
-  defaultTitle,
-  defaultTableColumns,
   tags,
   onSave,
   router,
 }: Props) {
   const {widgetId, orgId, dashboardId} = params;
-  const {source} = location.query;
+  const {source, displayType, defaultTitle, defaultTableColumns} = location.query;
+  const defaultWidgetQuery = getParsedDefaultWidgetQuery(
+    location.query.defaultWidgetQuery
+  );
 
   const isEditing = defined(widgetId);
   const orgSlug = organization.slug;
@@ -399,8 +397,6 @@ function WidgetBuilder({
   }
 
   async function handleSave() {
-    setState({...state, loading: true});
-
     const widgetData: Widget = assignTempId(currentWidget);
 
     if (widgetToBeUpdated) {
@@ -414,30 +410,76 @@ function WidgetBuilder({
       });
     }
 
-    let errors: FlatValidationError = {};
+    if (!(await dataIsValid(widgetData))) {
+      return;
+    }
+
+    if (notDashboardsOrigin) {
+      submitFromSelectedDashboard(widgetData);
+      return;
+    }
 
-    try {
-      await validateWidget(api, organization.slug, widgetData);
+    if (!!widgetToBeUpdated) {
+      let nextWidgetList = [...dashboard.widgets];
+      const updateIndex = nextWidgetList.indexOf(widgetToBeUpdated);
+      const nextWidgetData = {...widgetData, id: widgetToBeUpdated.id};
 
-      if (!!widgetToBeUpdated) {
-        updateWidget(widgetToBeUpdated, widgetData);
-        return;
+      // Only modify and re-compact if the default height has changed
+      if (
+        getDefaultWidgetHeight(widgetToBeUpdated.displayType) !==
+        getDefaultWidgetHeight(widgetData.displayType)
+      ) {
+        nextWidgetList[updateIndex] = enforceWidgetHeightValues(nextWidgetData);
+        nextWidgetList = generateWidgetsAfterCompaction(nextWidgetList);
+      } else {
+        nextWidgetList[updateIndex] = nextWidgetData;
       }
 
-      onSave([...dashboard.widgets, widgetData]);
-      addSuccessMessage(t('Added widget.'));
+      onSave(nextWidgetList);
+      addSuccessMessage(t('Updated widget.'));
+      goToDashboards(dashboardId ?? NEW_DASHBOARD_ID);
+      return;
+    }
 
-      goBack();
-    } catch (err) {
-      errors = mapErrors(err?.responseJSON ?? {}, {});
-    } finally {
-      setState({...state, errors, loading: false});
+    onSave([...dashboard.widgets, widgetData]);
+    addSuccessMessage(t('Added widget.'));
+    goToDashboards(dashboardId ?? NEW_DASHBOARD_ID);
+  }
 
-      if (notDashboardsOrigin) {
-        submitFromSelectedDashboard(errors, widgetData);
-        return;
+  async function dataIsValid(widgetData: Widget): Promise<boolean> {
+    if (notDashboardsOrigin) {
+      // Validate that a dashboard was selected since api call to /dashboards/widgets/ does not check for dashboard
+      if (
+        !state.selectedDashboard ||
+        !(
+          state.dashboards.find(
+            ({title, id}) =>
+              title === state.selectedDashboard?.label &&
+              id === state.selectedDashboard?.value
+          ) || state.selectedDashboard.value === NEW_DASHBOARD_ID
+        )
+      ) {
+        setState({
+          ...state,
+          errors: {...state.errors, dashboard: t('This field may not be blank')},
+        });
+        return false;
       }
     }
+
+    setState({...state, loading: true});
+
+    try {
+      await validateWidget(api, organization.slug, widgetData);
+      return true;
+    } catch (error) {
+      setState({
+        ...state,
+        loading: false,
+        errors: {...state.errors, ...mapErrors(error?.responseJSON ?? {}, {})},
+      });
+      return false;
+    }
   }
 
   async function fetchDashboards() {
@@ -460,65 +502,51 @@ function WidgetBuilder({
     }
   }
 
-  function submitFromSelectedDashboard(errors: FlatValidationError, widgetData: Widget) {
-    // Validate that a dashboard was selected since api call to /dashboards/widgets/ does not check for dashboard
-    if (
-      !state.selectedDashboard ||
-      !(
-        state.dashboards.find(({title, id}) => {
-          return (
-            title === state.selectedDashboard?.label &&
-            id === state.selectedDashboard?.value
-          );
-        }) || state.selectedDashboard.value === 'new'
-      )
-    ) {
-      errors.dashboard = t('This field may not be blank');
-      setState({...state, errors});
+  function submitFromSelectedDashboard(widgetData: Widget) {
+    if (!state.selectedDashboard) {
+      return;
     }
 
-    if (!Object.keys(errors).length && state.selectedDashboard) {
-      const queryData: QueryData = {
-        queryNames: [],
-        queryConditions: [],
-        queryFields: widgetData.queries[0].fields,
-        queryOrderby: widgetData.queries[0].orderby,
-      };
+    const queryData: QueryData = {
+      queryNames: [],
+      queryConditions: [],
+      queryFields: widgetData.queries[0].fields,
+      queryOrderby: widgetData.queries[0].orderby,
+    };
 
-      widgetData.queries.forEach(query => {
-        queryData.queryNames.push(query.name);
-        queryData.queryConditions.push(query.conditions);
-      });
+    widgetData.queries.forEach(query => {
+      queryData.queryNames.push(query.name);
+      queryData.queryConditions.push(query.conditions);
+    });
 
-      const query = {
-        displayType: widgetData.displayType,
-        interval: widgetData.interval,
-        title: widgetData.title,
-        ...(queryData ?? {}),
-      };
+    const pathQuery = {
+      displayType: widgetData.displayType,
+      interval: widgetData.interval,
+      title: widgetData.title,
+      ...queryData,
+      // Propagate page filters
+      ...selection.datetime,
+      project: selection.projects,
+      environment: selection.environments,
+    };
 
-      goBack(query);
-    }
+    addSuccessMessage(t('Added widget.'));
+    goToDashboards(state.selectedDashboard.value, pathQuery);
   }
 
-  function updateWidget(prevWidget: Widget, nextWidget: Widget) {
-    let nextWidgetList = [...dashboard.widgets];
-    const updateIndex = nextWidgetList.indexOf(prevWidget);
-    const nextWidgetData = {...nextWidget, id: prevWidget.id};
-
-    // Only modify and re-compact if the default height has changed
-    if (
-      getDefaultWidgetHeight(prevWidget.displayType) !==
-      getDefaultWidgetHeight(nextWidget.displayType)
-    ) {
-      nextWidgetList[updateIndex] = enforceWidgetHeightValues(nextWidgetData);
-      nextWidgetList = generateWidgetsAfterCompaction(nextWidgetList);
-    } else {
-      nextWidgetList[updateIndex] = nextWidgetData;
+  function goToDashboards(id: string, query?: Record<string, any>) {
+    if (id === NEW_DASHBOARD_ID) {
+      router.push({
+        pathname: `/organizations/${organization.slug}/dashboards/new/`,
+        query,
+      });
+      return;
     }
 
-    onSave(nextWidgetList);
-    addSuccessMessage(t('Updated widget.'));
+    router.push({
+      pathname: `/organizations/${organization.slug}/dashboard/${id}/`,
+      query,
+    });
   }
 
   function getAmendedFieldOptions(measurements: MeasurementCollection) {
@@ -530,14 +558,6 @@ function WidgetBuilder({
     });
   }
 
-  function goBack(query?: Record<string, any>) {
-    if (query) {
-      previousLocation.query = {...previousLocation.query, ...query};
-    }
-
-    router.push(previousLocation);
-  }
-
   if (isEditing && !dashboard.widgets.some(({id}) => id === String(widgetId))) {
     return (
       <SentryDocumentTitle title={dashboard.title} orgSlug={orgSlug}>
@@ -841,12 +861,17 @@ function WidgetBuilder({
                   description={t(
                     "Choose which dashboard you'd like to add this query to. It will appear as a widget."
                   )}
+                  required
                 >
                   <DashboardSelector
                     error={state.errors?.dashboard}
                     dashboards={state.dashboards}
                     onChange={selectedDashboard =>
-                      setState({...state, selectedDashboard})
+                      setState({
+                        ...state,
+                        selectedDashboard,
+                        errors: {...state.errors, dashboard: undefined},
+                      })
                     }
                     disabled={state.loading}
                   />

+ 11 - 0
static/app/views/eventsV2/queryList.tsx

@@ -1,6 +1,7 @@
 import * as React from 'react';
 import {browserHistory, InjectedRouter} from 'react-router';
 import styled from '@emotion/styled';
+import {urlEncode} from '@sentry/utils';
 import {Location, Query} from 'history';
 import moment from 'moment';
 
@@ -114,12 +115,22 @@ class QueryList extends React.Component<Props> {
       saved_query: !!savedQuery,
     });
 
+    const defaultTitle =
+      savedQuery?.name ?? (eventView.name !== 'All Events' ? eventView.name : undefined);
+
     if (organization.features.includes('new-widget-builder-experience')) {
       router.push({
         pathname: `/organizations/${organization.slug}/dashboards/new/widget/new/`,
         query: {
           ...location.query,
           source: DashboardWidgetSource.DISCOVERV2,
+          start: eventView.start,
+          end: eventView.end,
+          statsPeriod: eventView.statsPeriod,
+          defaultWidgetQuery: urlEncode(defaultWidgetQuery),
+          defaultTableColumns,
+          defaultTitle,
+          displayType,
         },
       });
       return;

+ 10 - 3
static/app/views/eventsV2/savedQuery/index.tsx

@@ -1,6 +1,7 @@
 import * as React from 'react';
 import {browserHistory, InjectedRouter} from 'react-router';
 import styled from '@emotion/styled';
+import {urlEncode} from '@sentry/utils';
 import {Location} from 'history';
 import isEqual from 'lodash/isEqual';
 
@@ -240,6 +241,7 @@ class SavedQueryButtonGroup extends React.PureComponent<Props, State> {
     const displayType = displayModeToDisplayType(eventView.display as DisplayModes);
     const defaultTableColumns = eventView.fields.map(({field}) => field);
     const sort = eventView.sorts[0];
+
     const defaultWidgetQuery: WidgetQuery = {
       name: '',
       fields: [
@@ -255,12 +257,19 @@ class SavedQueryButtonGroup extends React.PureComponent<Props, State> {
       saved_query: !!savedQuery,
     });
 
+    const defaultTitle =
+      savedQuery?.name ?? (eventView.name !== 'All Events' ? eventView.name : undefined);
+
     if (organization.features.includes('new-widget-builder-experience')) {
       router.push({
         pathname: `/organizations/${organization.slug}/dashboards/new/widget/new/`,
         query: {
           ...location.query,
           source: DashboardWidgetSource.DISCOVERV2,
+          defaultWidgetQuery: urlEncode(defaultWidgetQuery),
+          defaultTableColumns,
+          defaultTitle,
+          displayType,
         },
       });
       return;
@@ -271,9 +280,7 @@ class SavedQueryButtonGroup extends React.PureComponent<Props, State> {
       source: DashboardWidgetSource.DISCOVERV2,
       defaultWidgetQuery,
       defaultTableColumns,
-      defaultTitle:
-        savedQuery?.name ??
-        (eventView.name !== 'All Events' ? eventView.name : undefined),
+      defaultTitle,
       displayType,
     });
   };