Browse Source

feat(widget-builder): Add helper to convert widget to builder state (#81478)

This lets us put the widget builder state into the URL so it can be
parsed by the hook when the builder mounts. To demonstrate this, I've
hooked up the "Edit Widget" button which should put the state in the
URL, open the new slideout, and put my dev builder in the slideout if
you set `showDevBuilder` in your local storage to `true`

It should load up with the fields populated from the query (although
keep in mind I'm not doing any fancy UI stuff to filter, so e.g. a table
widget will still open up and show a "Y-axis" field)
Nar Saynorath 3 months ago
parent
commit
4332d82620

+ 7 - 1
static/app/views/dashboards/dashboard.tsx

@@ -96,6 +96,7 @@ type Props = {
   isPreview?: boolean;
   newWidget?: Widget;
   onAddWidget?: (dataset?: DataSet) => void;
+  onEditWidget?: (widget: Widget) => void;
   onSetNewWidget?: () => void;
   paramDashboardId?: string;
   paramTemplateId?: string;
@@ -373,7 +374,7 @@ class Dashboard extends Component<Props, State> {
   };
 
   handleEditWidget = (index: number) => () => {
-    const {organization, router, location, paramDashboardId} = this.props;
+    const {organization, router, location, paramDashboardId, onEditWidget} = this.props;
     const widget = this.props.dashboard.widgets[index];
 
     trackAnalytics('dashboards_views.widget.edit', {
@@ -385,6 +386,11 @@ class Dashboard extends Component<Props, State> {
       return;
     }
 
+    if (organization.features.includes('dashboards-widget-builder-redesign')) {
+      onEditWidget?.(widget);
+      return;
+    }
+
     if (paramDashboardId) {
       router.push(
         normalizeUrl({

+ 28 - 17
static/app/views/dashboards/detail.tsx

@@ -42,7 +42,6 @@ import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metr
 import {MetricsResultsMetaProvider} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
 import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
 import {OnDemandControlProvider} from 'sentry/utils/performance/contexts/onDemandControl';
-import {decodeScalar} from 'sentry/utils/queryString';
 import normalizeUrl from 'sentry/utils/url/normalizeUrl';
 import withApi from 'sentry/utils/withApi';
 import withOrganization from 'sentry/utils/withOrganization';
@@ -57,9 +56,9 @@ import {
   isWidgetUsingTransactionName,
   resetPageFilters,
 } from 'sentry/views/dashboards/utils';
-import DevBuilder from 'sentry/views/dashboards/widgetBuilder/components/devBuilder';
 import DevWidgetBuilder from 'sentry/views/dashboards/widgetBuilder/components/newWidgetBuilder';
 import {DataSet} from 'sentry/views/dashboards/widgetBuilder/utils';
+import {convertWidgetToBuilderStateParams} from 'sentry/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams';
 import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder';
 import {MetricsDataSwitcherAlert} from 'sentry/views/performance/landing/metricsDataSwitcherAlert';
 
@@ -336,6 +335,10 @@ class DashboardDetail extends Component<Props, State> {
           },
           onEdit: () => {
             const widgetIndex = dashboard.widgets.indexOf(widget);
+            if (organization.features.includes('dashboards-widget-builder-redesign')) {
+              this.onEditWidget(widget);
+              return;
+            }
             if (dashboardId) {
               const query = omit(location.query, Object.values(WidgetViewerQueryField));
 
@@ -726,6 +729,27 @@ class DashboardDetail extends Component<Props, State> {
     );
   };
 
+  onEditWidget = (widget: Widget) => {
+    const {router, organization, params, location, dashboard} = this.props;
+    const {dashboardId} = params;
+    const widgetIndex = dashboard.widgets.indexOf(widget);
+    this.setState({
+      isWidgetBuilderOpen: true,
+    });
+    const path = defined(dashboardId)
+      ? `/organizations/${organization.slug}/dashboard/${dashboardId}/widget-builder/widget/${widgetIndex}/edit/`
+      : `/organizations/${organization.slug}/dashboards/new/widget-builder/widget/${widgetIndex}/edit/`;
+    router.push(
+      normalizeUrl({
+        pathname: path,
+        query: {
+          ...location.query,
+          ...convertWidgetToBuilderStateParams(widget),
+        },
+      })
+    );
+  };
+
   /* Handles POST request for Edit Access Selector Changes */
   onChangeEditAccess = (newDashboardPermissions: DashboardPermissions) => {
     const {dashboard, api, organization} = this.props;
@@ -1218,6 +1242,7 @@ class DashboardDetail extends Component<Props, State> {
                                     isPreview={this.isPreview}
                                     widgetLegendState={this.state.widgetLegendState}
                                     onAddWidget={this.onAddWidget}
+                                    onEditWidget={this.onEditWidget}
                                   />
 
                                   <DevWidgetBuilder
@@ -1250,22 +1275,8 @@ class DashboardDetail extends Component<Props, State> {
     );
   }
 
-  /**
-   * This is a temporary component to test the new widget builder hook during development.
-   */
-  renderDevWidgetBuilder() {
-    return <DevBuilder />;
-  }
-
   render() {
-    const {organization, location} = this.props;
-
-    if (
-      organization.features.includes('dashboards-widget-builder-redesign') &&
-      decodeScalar(location.query?.devBuilder) === 'true'
-    ) {
-      return this.renderDevWidgetBuilder();
-    }
+    const {organization} = this.props;
 
     if (this.isWidgetBuilderRouter) {
       return this.renderWidgetBuilder();

+ 6 - 0
static/app/views/dashboards/widgetBuilder/components/devBuilder.tsx

@@ -6,6 +6,7 @@ import Input from 'sentry/components/input';
 import {space} from 'sentry/styles/space';
 import {CustomMeasurementsProvider} from 'sentry/utils/customMeasurements/customMeasurementsProvider';
 import {type Column, generateFieldAsString} from 'sentry/utils/discover/fields';
+import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
 import useOrganization from 'sentry/utils/useOrganization';
 import usePageFilters from 'sentry/utils/usePageFilters';
 import {getDatasetConfig} from 'sentry/views/dashboards/datasetConfig/base';
@@ -20,6 +21,11 @@ import ResultsSearchQueryBuilder from 'sentry/views/discover/resultsSearchQueryB
 
 function DevBuilder() {
   const {state, dispatch} = useWidgetBuilderState();
+  const [showDevBuilder] = useLocalStorageState('showDevBuilder', false);
+
+  if (!showDevBuilder) {
+    return null;
+  }
 
   return (
     <Body>

+ 2 - 0
static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx

@@ -5,6 +5,7 @@ import SlideOverPanel from 'sentry/components/slideOverPanel';
 import {IconClose} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
+import DevBuilder from 'sentry/views/dashboards/widgetBuilder/components/devBuilder';
 import WidgetBuilderFilterBar from 'sentry/views/dashboards/widgetBuilder/components/filtersBar';
 
 type WidgetBuilderSlideoutProps = {
@@ -30,6 +31,7 @@ function WidgetBuilderSlideout({isOpen, onClose}: WidgetBuilderSlideoutProps) {
       </SlideoutHeaderWrapper>
       <SlideoutBodyWrapper>
         <WidgetBuilderFilterBar />
+        <DevBuilder />
       </SlideoutBodyWrapper>
     </SlideOverPanel>
   );

+ 9 - 0
static/app/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState.tsx

@@ -9,6 +9,15 @@ import {decodeList} from 'sentry/utils/queryString';
 import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
 import {useQueryParamState} from 'sentry/views/dashboards/widgetBuilder/hooks/useQueryParamState';
 
+export type WidgetBuilderStateQueryParams = {
+  dataset?: WidgetType;
+  description?: string;
+  displayType?: DisplayType;
+  field?: (string | undefined)[];
+  title?: string;
+  yAxis?: string[];
+};
+
 export const BuilderStateAction = {
   SET_TITLE: 'SET_TITLE',
   SET_DESCRIPTION: 'SET_DESCRIPTION',

+ 22 - 0
static/app/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams.ts

@@ -0,0 +1,22 @@
+import {DisplayType, type Widget, WidgetType} from 'sentry/views/dashboards/types';
+import type {WidgetBuilderStateQueryParams} from 'sentry/views/dashboards/widgetBuilder/hooks/useWidgetBuilderState';
+
+/**
+ * Converts a widget to a set of query params that can be used to
+ * restore the widget builder state.
+ */
+export function convertWidgetToBuilderStateParams(
+  widget: Widget
+): WidgetBuilderStateQueryParams {
+  const yAxis = widget.queries.flatMap(q => q.aggregates);
+  const field = widget.queries.flatMap(q => q.fields);
+
+  return {
+    title: widget.title,
+    description: widget.description ?? '',
+    dataset: widget.widgetType ?? WidgetType.ERRORS,
+    displayType: widget.displayType ?? DisplayType.TABLE,
+    field,
+    yAxis,
+  };
+}