Browse Source

feat(widget-builder): Open widget builder slideout when adding widget (#81470)

Under the `dashboards-widget-builder-redesign` feature flag, when the
"Add Widget" button is clicked (either in the header bar or the edit
dashboard state) then open up the widget builder.

This is achieved by:
- Add routes specifically for the new widget builder
- These aren't permanent and mostly for dev. We need to find a way to
transition this URL to the old widget builder at some point
- Handle URL management for the redesign in the `detail.tsx` component
  - Required me to pass down `onAddWidget`
- When cancelling/closing the modal, the URL should go back to the
dashboard with the right params from the page filters.

This doesn't support Edit Widget yet.
Nar Saynorath 3 months ago
parent
commit
8e45680a88

+ 33 - 0
static/app/routes.tsx

@@ -1070,6 +1070,17 @@ function buildRoutes() {
             )}
             key="orgless-dashboards-new-route"
           >
+            {/* New widget builder routes */}
+            <Route
+              path="widget-builder/widget/:widgetIndex/edit/"
+              component={make(() => import('sentry/views/dashboards/view'))}
+            />
+            <Route
+              path="widget-builder/widget/new/"
+              component={make(() => import('sentry/views/dashboards/view'))}
+            />
+
+            {/* Old widget builder routes */}
             <Route
               path="widget/:widgetIndex/edit/"
               component={make(() => import('sentry/views/dashboards/widgetBuilder'))}
@@ -1087,6 +1098,17 @@ function buildRoutes() {
           )}
           key="org-dashboards-new"
         >
+          {/* New widget builder routes */}
+          <Route
+            path="widget-builder/widget/:widgetIndex/edit/"
+            component={make(() => import('sentry/views/dashboards/view'))}
+          />
+          <Route
+            path="widget-builder/widget/new/"
+            component={make(() => import('sentry/views/dashboards/view'))}
+          />
+
+          {/* Old widget builder routes */}
           <Route
             path="widget/:widgetIndex/edit/"
             component={make(() => import('sentry/views/dashboards/widgetBuilder'))}
@@ -1137,6 +1159,17 @@ function buildRoutes() {
         component={make(() => import('sentry/views/dashboards/view'))}
         withOrgPath
       >
+        {/* New widget builder routes */}
+        <Route
+          path="widget-builder/widget/:widgetIndex/edit/"
+          component={make(() => import('sentry/views/dashboards/view'))}
+        />
+        <Route
+          path="widget-builder/widget/new/"
+          component={make(() => import('sentry/views/dashboards/view'))}
+        />
+
+        {/* Old widget builder routes */}
         <Route
           path="widget/:widgetIndex/edit/"
           component={make(() => import('sentry/views/dashboards/widgetBuilder'))}

+ 27 - 15
static/app/views/dashboards/dashboard.tsx

@@ -95,6 +95,7 @@ type Props = {
   handleChangeSplitDataset?: (widget: Widget, index: number) => void;
   isPreview?: boolean;
   newWidget?: Widget;
+  onAddWidget?: (dataset?: DataSet) => void;
   onSetNewWidget?: () => void;
   paramDashboardId?: string;
   paramTemplateId?: string;
@@ -222,18 +223,38 @@ class Dashboard extends Component<Props, State> {
   }
 
   handleStartAdd = (dataset?: DataSet) => {
-    const {organization, router, location, paramDashboardId, handleAddMetricWidget} =
-      this.props;
+    const {
+      organization,
+      router,
+      location,
+      paramDashboardId,
+      handleAddMetricWidget,
+      onAddWidget,
+    } = this.props;
 
     if (dataset === DataSet.METRICS) {
       handleAddMetricWidget?.({...this.addWidgetLayout, ...METRIC_WIDGET_MIN_SIZE});
       return;
     }
 
-    if (paramDashboardId) {
+    if (!organization.features.includes('dashboards-widget-builder-redesign')) {
+      if (paramDashboardId) {
+        router.push(
+          normalizeUrl({
+            pathname: `/organizations/${organization.slug}/dashboard/${paramDashboardId}/widget/new/`,
+            query: {
+              ...location.query,
+              source: DashboardWidgetSource.DASHBOARDS,
+              dataset,
+            },
+          })
+        );
+        return;
+      }
+
       router.push(
         normalizeUrl({
-          pathname: `/organizations/${organization.slug}/dashboard/${paramDashboardId}/widget/new/`,
+          pathname: `/organizations/${organization.slug}/dashboards/new/widget/new/`,
           query: {
             ...location.query,
             source: DashboardWidgetSource.DASHBOARDS,
@@ -241,20 +262,11 @@ class Dashboard extends Component<Props, State> {
           },
         })
       );
+
       return;
     }
 
-    router.push(
-      normalizeUrl({
-        pathname: `/organizations/${organization.slug}/dashboards/new/widget/new/`,
-        query: {
-          ...location.query,
-          source: DashboardWidgetSource.DASHBOARDS,
-          dataset,
-        },
-      })
-    );
-
+    onAddWidget?.();
     return;
   };
 

+ 55 - 1
static/app/views/dashboards/detail.spec.tsx

@@ -25,9 +25,12 @@ import ProjectsStore from 'sentry/stores/projectsStore';
 import TeamStore from 'sentry/stores/teamStore';
 import {browserHistory} from 'sentry/utils/browserHistory';
 import CreateDashboard from 'sentry/views/dashboards/create';
-import {handleUpdateDashboardSplit} from 'sentry/views/dashboards/detail';
+import DashboardDetail, {
+  handleUpdateDashboardSplit,
+} from 'sentry/views/dashboards/detail';
 import EditAccessSelector from 'sentry/views/dashboards/editAccessSelector';
 import * as types from 'sentry/views/dashboards/types';
+import {DashboardState} from 'sentry/views/dashboards/types';
 import ViewEditDashboard from 'sentry/views/dashboards/view';
 import {OrganizationContext} from 'sentry/views/organizationContext';
 
@@ -2017,6 +2020,57 @@ describe('Dashboards > Detail', function () {
       );
     });
 
+    describe('widget builder redesign', function () {
+      beforeEach(function () {
+        initialData = initializeOrg({
+          organization: OrganizationFixture({
+            features: [
+              'global-views',
+              'dashboards-basic',
+              'dashboards-edit',
+              'discover-query',
+              'performance-discover-dataset-selector',
+              'dashboards-widget-builder-redesign',
+            ],
+          }),
+        });
+      });
+
+      it('opens the widget builder slideout when clicking add widget', async function () {
+        render(
+          <DashboardDetail
+            {...RouteComponentPropsFixture()}
+            initialState={DashboardState.VIEW}
+            dashboard={DashboardFixture([])}
+            dashboards={[]}
+            onDashboardUpdate={jest.fn()}
+            newWidget={undefined}
+            onSetNewWidget={() => {}}
+          />,
+          {organization: initialData.organization}
+        );
+        await userEvent.click(await screen.findByRole('button', {name: 'Add Widget'}));
+        expect(await screen.findByText('Create Custom Widget')).toBeInTheDocument();
+      });
+
+      it('opens the widget builder slideout when clicking add widget in edit mode', async function () {
+        render(
+          <DashboardDetail
+            {...RouteComponentPropsFixture()}
+            initialState={DashboardState.EDIT}
+            dashboard={DashboardFixture([])}
+            dashboards={[]}
+            onDashboardUpdate={jest.fn()}
+            newWidget={undefined}
+            onSetNewWidget={() => {}}
+          />,
+          {organization: initialData.organization}
+        );
+        await userEvent.click(await screen.findByTestId('widget-add'));
+        expect(await screen.findByText('Create Custom Widget')).toBeInTheDocument();
+      });
+    });
+
     describe('discover split', function () {
       it('calls the dashboard callbacks with the correct widgetType for discover split', function () {
         const widget = {

+ 129 - 49
static/app/views/dashboards/detail.tsx

@@ -1,8 +1,10 @@
 import {cloneElement, Component, Fragment, isValidElement} from 'react';
 import styled from '@emotion/styled';
+import type {Location} from 'history';
 import isEqual from 'lodash/isEqual';
 import isEqualWith from 'lodash/isEqualWith';
 import omit from 'lodash/omit';
+import pick from 'lodash/pick';
 
 import {
   createDashboard,
@@ -15,7 +17,6 @@ import {openWidgetViewerModal} from 'sentry/actionCreators/modal';
 import type {Client} from 'sentry/api';
 import {hasEveryAccess} from 'sentry/components/acl/access';
 import {Breadcrumbs} from 'sentry/components/breadcrumbs';
-import {Button} from 'sentry/components/button';
 import HookOrDefault from 'sentry/components/hookOrDefault';
 import * as Layout from 'sentry/components/layouts/thirds';
 import {
@@ -202,6 +203,39 @@ export function checkUserHasEditAccess(
   return false;
 }
 
+function getDashboardLocation({
+  organization,
+  dashboardId,
+  location,
+}: {
+  location: Location<any>;
+  organization: Organization;
+  dashboardId?: string;
+}) {
+  // Preserve important filter params
+  const filterParams = pick(location.query, [
+    'release',
+    'environment',
+    'project',
+    'statsPeriod',
+    'start',
+    'end',
+  ]);
+
+  const commonPath = defined(dashboardId)
+    ? `/dashboard/${dashboardId}/`
+    : `/dashboards/new/`;
+
+  const dashboardUrl = USING_CUSTOMER_DOMAIN
+    ? commonPath
+    : `/organizations/${organization.slug}${commonPath}`;
+
+  return normalizeUrl({
+    pathname: dashboardUrl,
+    query: filterParams,
+  });
+}
+
 class DashboardDetail extends Component<Props, State> {
   state: State = {
     dashboardState: this.props.initialState,
@@ -216,7 +250,7 @@ class DashboardDetail extends Component<Props, State> {
       location: this.props.location,
       router: this.props.router,
     }),
-    isWidgetBuilderOpen: false,
+    isWidgetBuilderOpen: this.isRedesignedWidgetBuilder,
   };
 
   componentDidMount() {
@@ -389,6 +423,35 @@ class DashboardDetail extends Component<Props, State> {
     return widgetBuilderRoutes.includes(location.pathname);
   }
 
+  get isRedesignedWidgetBuilder() {
+    const {organization, location, params} = this.props;
+    const {dashboardId, widgetIndex} = params;
+
+    if (!organization.features.includes('dashboards-widget-builder-redesign')) {
+      return false;
+    }
+
+    const widgetBuilderRoutes = [
+      `/organizations/${organization.slug}/dashboard/new/widget-builder/widget/new/`,
+      `/organizations/${organization.slug}/dashboard/${dashboardId}/widget-builder/widget/new/`,
+      `/organizations/${organization.slug}/dashboard/new/widget-builder/widget/${widgetIndex}/edit/`,
+      `/organizations/${organization.slug}/dashboard/${dashboardId}/widget-builder/widget/${widgetIndex}/edit/`,
+    ];
+
+    if (USING_CUSTOMER_DOMAIN) {
+      widgetBuilderRoutes.push(
+        ...[
+          `/dashboards/new/widget-builder/widget/new/`,
+          `/dashboard/${dashboardId}/widget-builder/widget/new/`,
+          `/dashboards/new/widget-builder/widget/${widgetIndex}/edit/`,
+          `/dashboard/${dashboardId}/widget-builder/widget/${widgetIndex}/edit/`,
+        ]
+      );
+    }
+
+    return widgetBuilderRoutes.includes(location.pathname);
+  }
+
   get dashboardTitle() {
     const {dashboard} = this.props;
     const {modifiedDashboard} = this.state;
@@ -600,7 +663,7 @@ class DashboardDetail extends Component<Props, State> {
     });
   };
 
-  onAddWidget = (dataset: DataSet) => {
+  onAddWidget = (dataset?: DataSet) => {
     const {
       organization,
       dashboard,
@@ -608,11 +671,40 @@ class DashboardDetail extends Component<Props, State> {
       location,
       params: {dashboardId},
     } = this.props;
+    const {modifiedDashboard} = this.state;
 
     if (dataset === DataSet.METRICS) {
       this.handleAddMetricWidget();
       return;
     }
+
+    if (organization.features.includes('dashboards-widget-builder-redesign')) {
+      this.setState(
+        {
+          modifiedDashboard: cloneDashboard(modifiedDashboard ?? dashboard),
+        },
+        () => {
+          this.setState({isWidgetBuilderOpen: true});
+          let pathname = `/organizations/${organization.slug}/dashboard/${dashboardId}/widget-builder/widget/new/`;
+          if (!defined(dashboardId)) {
+            pathname = `/organizations/${organization.slug}/dashboards/new/widget-builder/widget/new/`;
+          }
+          router.push(
+            normalizeUrl({
+              // TODO: Replace with the old widget builder path when swapping over
+              pathname,
+              query: {
+                ...location.query,
+                dataset,
+              },
+            })
+          );
+        }
+      );
+
+      return;
+    }
+
     this.setState(
       {
         modifiedDashboard: cloneDashboard(dashboard),
@@ -1108,23 +1200,40 @@ class DashboardDetail extends Component<Props, State> {
                               />
 
                               <WidgetViewerContext.Provider value={{seriesData, setData}}>
-                                <Dashboard
-                                  paramDashboardId={dashboardId}
-                                  dashboard={modifiedDashboard ?? dashboard}
-                                  organization={organization}
-                                  isEditingDashboard={this.isEditingDashboard}
-                                  widgetLimitReached={widgetLimitReached}
-                                  onUpdate={this.onUpdateWidget}
-                                  handleUpdateWidgetList={this.handleUpdateWidgetList}
-                                  handleAddCustomWidget={this.handleAddCustomWidget}
-                                  handleAddMetricWidget={this.handleAddMetricWidget}
-                                  router={router}
-                                  location={location}
-                                  newWidget={newWidget}
-                                  onSetNewWidget={onSetNewWidget}
-                                  isPreview={this.isPreview}
-                                  widgetLegendState={this.state.widgetLegendState}
-                                />
+                                <Fragment>
+                                  <Dashboard
+                                    paramDashboardId={dashboardId}
+                                    dashboard={modifiedDashboard ?? dashboard}
+                                    organization={organization}
+                                    isEditingDashboard={this.isEditingDashboard}
+                                    widgetLimitReached={widgetLimitReached}
+                                    onUpdate={this.onUpdateWidget}
+                                    handleUpdateWidgetList={this.handleUpdateWidgetList}
+                                    handleAddCustomWidget={this.handleAddCustomWidget}
+                                    handleAddMetricWidget={this.handleAddMetricWidget}
+                                    router={router}
+                                    location={location}
+                                    newWidget={newWidget}
+                                    onSetNewWidget={onSetNewWidget}
+                                    isPreview={this.isPreview}
+                                    widgetLegendState={this.state.widgetLegendState}
+                                    onAddWidget={this.onAddWidget}
+                                  />
+
+                                  <DevWidgetBuilder
+                                    isOpen={this.state.isWidgetBuilderOpen}
+                                    onClose={() => {
+                                      this.setState({isWidgetBuilderOpen: false});
+                                      router.push(
+                                        getDashboardLocation({
+                                          organization,
+                                          dashboardId,
+                                          location,
+                                        })
+                                      );
+                                    }}
+                                  />
+                                </Fragment>
                               </WidgetViewerContext.Provider>
                             </MEPSettingProvider>
                           )}
@@ -1148,28 +1257,6 @@ class DashboardDetail extends Component<Props, State> {
     return <DevBuilder />;
   }
 
-  renderDevWidgetBuilderUI() {
-    return (
-      <Fragment>
-        <Button
-          aria-label="new widget builder"
-          onClick={() => {
-            this.setState({isWidgetBuilderOpen: true});
-          }}
-          style={{maxWidth: '250px', alignSelf: 'center'}}
-        >
-          {'Open Widget Builder'}
-        </Button>
-        <DevWidgetBuilder
-          isOpen={this.state.isWidgetBuilderOpen}
-          onClose={() => {
-            this.setState({isWidgetBuilderOpen: false});
-          }}
-        />
-      </Fragment>
-    );
-  }
-
   render() {
     const {organization, location} = this.props;
 
@@ -1180,13 +1267,6 @@ class DashboardDetail extends Component<Props, State> {
       return this.renderDevWidgetBuilder();
     }
 
-    if (
-      organization.features.includes('dashboards-widget-builder-redesign') &&
-      decodeScalar(location.query?.devBuilderUI) === 'true'
-    ) {
-      return this.renderDevWidgetBuilderUI();
-    }
-
     if (this.isWidgetBuilderRouter) {
       return this.renderWidgetBuilder();
     }