diff --git a/static/app/routes.tsx b/static/app/routes.tsx index 72d2333c832bc6..99821dee6b4527 100644 --- a/static/app/routes.tsx +++ b/static/app/routes.tsx @@ -1070,6 +1070,17 @@ function buildRoutes() { )} key="orgless-dashboards-new-route" > + {/* New widget builder routes */} + import('sentry/views/dashboards/view'))} + /> + import('sentry/views/dashboards/view'))} + /> + + {/* Old widget builder routes */} import('sentry/views/dashboards/widgetBuilder'))} @@ -1087,6 +1098,17 @@ function buildRoutes() { )} key="org-dashboards-new" > + {/* New widget builder routes */} + import('sentry/views/dashboards/view'))} + /> + import('sentry/views/dashboards/view'))} + /> + + {/* Old widget builder routes */} import('sentry/views/dashboards/widgetBuilder'))} @@ -1137,6 +1159,17 @@ function buildRoutes() { component={make(() => import('sentry/views/dashboards/view'))} withOrgPath > + {/* New widget builder routes */} + import('sentry/views/dashboards/view'))} + /> + import('sentry/views/dashboards/view'))} + /> + + {/* Old widget builder routes */} import('sentry/views/dashboards/widgetBuilder'))} diff --git a/static/app/views/dashboards/dashboard.tsx b/static/app/views/dashboards/dashboard.tsx index cb193aeb76a25d..a17762824551c7 100644 --- a/static/app/views/dashboards/dashboard.tsx +++ b/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 { } 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 { }, }) ); + return; } - router.push( - normalizeUrl({ - pathname: `/organizations/${organization.slug}/dashboards/new/widget/new/`, - query: { - ...location.query, - source: DashboardWidgetSource.DASHBOARDS, - dataset, - }, - }) - ); - + onAddWidget?.(); return; }; diff --git a/static/app/views/dashboards/detail.spec.tsx b/static/app/views/dashboards/detail.spec.tsx index 19672cf1c8388f..b934d7c87d2995 100644 --- a/static/app/views/dashboards/detail.spec.tsx +++ b/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( + {}} + />, + {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( + {}} + />, + {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 = { diff --git a/static/app/views/dashboards/detail.tsx b/static/app/views/dashboards/detail.tsx index 35a9e6979a98c1..73966e36c1ffc6 100644 --- a/static/app/views/dashboards/detail.tsx +++ b/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; + 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 { state: State = { dashboardState: this.props.initialState, @@ -216,7 +250,7 @@ class DashboardDetail extends Component { location: this.props.location, router: this.props.router, }), - isWidgetBuilderOpen: false, + isWidgetBuilderOpen: this.isRedesignedWidgetBuilder, }; componentDidMount() { @@ -389,6 +423,35 @@ class DashboardDetail extends Component { 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 { }); }; - onAddWidget = (dataset: DataSet) => { + onAddWidget = (dataset?: DataSet) => { const { organization, dashboard, @@ -608,11 +671,40 @@ class DashboardDetail extends Component { 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 { /> - + + + + { + this.setState({isWidgetBuilderOpen: false}); + router.push( + getDashboardLocation({ + organization, + dashboardId, + location, + }) + ); + }} + /> + )} @@ -1148,28 +1257,6 @@ class DashboardDetail extends Component { return ; } - renderDevWidgetBuilderUI() { - return ( - - - { - this.setState({isWidgetBuilderOpen: false}); - }} - /> - - ); - } - render() { const {organization, location} = this.props; @@ -1180,13 +1267,6 @@ class DashboardDetail extends Component { 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(); }