detail.tsx 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365
  1. import {cloneElement, Component, Fragment, isValidElement} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import isEqual from 'lodash/isEqual';
  5. import isEqualWith from 'lodash/isEqualWith';
  6. import omit from 'lodash/omit';
  7. import pick from 'lodash/pick';
  8. import {
  9. createDashboard,
  10. deleteDashboard,
  11. updateDashboard,
  12. updateDashboardPermissions,
  13. } from 'sentry/actionCreators/dashboards';
  14. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  15. import {openWidgetViewerModal} from 'sentry/actionCreators/modal';
  16. import type {Client} from 'sentry/api';
  17. import {hasEveryAccess} from 'sentry/components/acl/access';
  18. import {Breadcrumbs} from 'sentry/components/breadcrumbs';
  19. import HookOrDefault from 'sentry/components/hookOrDefault';
  20. import * as Layout from 'sentry/components/layouts/thirds';
  21. import {
  22. isWidgetViewerPath,
  23. WidgetViewerQueryField,
  24. } from 'sentry/components/modals/widgetViewerModal/utils';
  25. import NoProjectMessage from 'sentry/components/noProjectMessage';
  26. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  27. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  28. import {USING_CUSTOMER_DOMAIN} from 'sentry/constants';
  29. import {t} from 'sentry/locale';
  30. import {space} from 'sentry/styles/space';
  31. import type {PageFilters} from 'sentry/types/core';
  32. import type {PlainRoute, RouteComponentProps} from 'sentry/types/legacyReactRouter';
  33. import type {Organization, Team} from 'sentry/types/organization';
  34. import type {Project} from 'sentry/types/project';
  35. import type {User} from 'sentry/types/user';
  36. import {defined} from 'sentry/utils';
  37. import {trackAnalytics} from 'sentry/utils/analytics';
  38. import {browserHistory} from 'sentry/utils/browserHistory';
  39. import EventView from 'sentry/utils/discover/eventView';
  40. import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
  41. import {MetricsResultsMetaProvider} from 'sentry/utils/performance/contexts/metricsEnhancedPerformanceDataContext';
  42. import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  43. import {OnDemandControlProvider} from 'sentry/utils/performance/contexts/onDemandControl';
  44. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  45. import withApi from 'sentry/utils/withApi';
  46. import withOrganization from 'sentry/utils/withOrganization';
  47. import withPageFilters from 'sentry/utils/withPageFilters';
  48. import withProjects from 'sentry/utils/withProjects';
  49. import {defaultMetricWidget} from 'sentry/views/dashboards/metrics/utils';
  50. import {
  51. cloneDashboard,
  52. getCurrentPageFilters,
  53. getDashboardFiltersFromURL,
  54. hasUnsavedFilterChanges,
  55. isWidgetUsingTransactionName,
  56. resetPageFilters,
  57. } from 'sentry/views/dashboards/utils';
  58. import WidgetBuilderV2 from 'sentry/views/dashboards/widgetBuilder/components/newWidgetBuilder';
  59. import {DataSet} from 'sentry/views/dashboards/widgetBuilder/utils';
  60. import {convertWidgetToBuilderStateParams} from 'sentry/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams';
  61. import {getDefaultWidget} from 'sentry/views/dashboards/widgetBuilder/utils/getDefaultWidget';
  62. import {DATA_SET_TO_WIDGET_TYPE} from 'sentry/views/dashboards/widgetBuilder/widgetBuilder';
  63. import WidgetLegendNameEncoderDecoder from 'sentry/views/dashboards/widgetLegendNameEncoderDecoder';
  64. import {MetricsDataSwitcherAlert} from 'sentry/views/performance/landing/metricsDataSwitcherAlert';
  65. import {generatePerformanceEventView} from '../performance/data';
  66. import {MetricsDataSwitcher} from '../performance/landing/metricsDataSwitcher';
  67. import {DiscoverQueryPageSource} from '../performance/utils';
  68. import type {WidgetViewerContextProps} from './widgetViewer/widgetViewerContext';
  69. import {WidgetViewerContext} from './widgetViewer/widgetViewerContext';
  70. import Controls from './controls';
  71. import Dashboard from './dashboard';
  72. import {DEFAULT_STATS_PERIOD} from './data';
  73. import FiltersBar from './filtersBar';
  74. import {
  75. assignDefaultLayout,
  76. assignTempId,
  77. calculateColumnDepths,
  78. generateWidgetsAfterCompaction,
  79. getDashboardLayout,
  80. } from './layoutUtils';
  81. import DashboardTitle from './title';
  82. import type {
  83. DashboardDetails,
  84. DashboardFilters,
  85. DashboardListItem,
  86. DashboardPermissions,
  87. Widget,
  88. } from './types';
  89. import {
  90. DashboardFilterKeys,
  91. DashboardState,
  92. DashboardWidgetSource,
  93. MAX_WIDGETS,
  94. WidgetType,
  95. } from './types';
  96. import WidgetLegendSelectionState from './widgetLegendSelectionState';
  97. const UNSAVED_MESSAGE = t('You have unsaved changes, are you sure you want to leave?');
  98. export const UNSAVED_FILTERS_MESSAGE = t(
  99. 'You have unsaved dashboard filters. You can save or discard them.'
  100. );
  101. const HookHeader = HookOrDefault({hookName: 'component:dashboards-header'});
  102. type RouteParams = {
  103. dashboardId?: string;
  104. templateId?: string;
  105. widgetId?: number | string;
  106. widgetIndex?: number;
  107. };
  108. type Props = RouteComponentProps<RouteParams, {}> & {
  109. api: Client;
  110. dashboard: DashboardDetails;
  111. dashboards: DashboardListItem[];
  112. initialState: DashboardState;
  113. organization: Organization;
  114. projects: Project[];
  115. route: PlainRoute;
  116. selection: PageFilters;
  117. children?: React.ReactNode;
  118. newWidget?: Widget;
  119. onDashboardUpdate?: (updatedDashboard: DashboardDetails) => void;
  120. onSetNewWidget?: () => void;
  121. };
  122. type State = {
  123. dashboardState: DashboardState;
  124. isWidgetBuilderOpen: boolean;
  125. modifiedDashboard: DashboardDetails | null;
  126. widgetLegendState: WidgetLegendSelectionState;
  127. widgetLimitReached: boolean;
  128. } & WidgetViewerContextProps;
  129. export function handleUpdateDashboardSplit({
  130. widgetId,
  131. splitDecision,
  132. dashboard,
  133. onDashboardUpdate,
  134. modifiedDashboard,
  135. stateSetter,
  136. }: {
  137. dashboard: DashboardDetails;
  138. modifiedDashboard: DashboardDetails | null;
  139. splitDecision: WidgetType;
  140. stateSetter: Component<Props, State, any>['setState'];
  141. widgetId: string;
  142. onDashboardUpdate?: (updatedDashboard: DashboardDetails) => void;
  143. }) {
  144. // The underlying dashboard needs to be updated with the split decision
  145. // because the backend has evaluated the query and stored that value
  146. const updatedDashboard = cloneDashboard(dashboard);
  147. const widgetIndex = updatedDashboard.widgets.findIndex(
  148. widget => widget.id === widgetId
  149. );
  150. if (widgetIndex >= 0) {
  151. updatedDashboard.widgets[widgetIndex]!.widgetType = splitDecision;
  152. }
  153. onDashboardUpdate?.(updatedDashboard);
  154. // The modified dashboard also needs to be updated because that dashboard
  155. // is rendered instead of the original dashboard when editing
  156. if (modifiedDashboard) {
  157. stateSetter(state => ({
  158. ...state,
  159. modifiedDashboard: {
  160. ...state.modifiedDashboard!,
  161. widgets: state.modifiedDashboard!.widgets.map(widget =>
  162. widget.id === widgetId ? {...widget, widgetType: splitDecision} : widget
  163. ),
  164. },
  165. }));
  166. }
  167. }
  168. /* Checks if current user has permissions to edit dashboard */
  169. export function checkUserHasEditAccess(
  170. currentUser: User,
  171. userTeams: Team[],
  172. organization: Organization,
  173. dashboardPermissions?: DashboardPermissions,
  174. dashboardCreator?: User
  175. ): boolean {
  176. if (
  177. !organization.features.includes('dashboards-edit-access') ||
  178. hasEveryAccess(['org:write'], {organization}) || // Managers and Owners
  179. !dashboardPermissions ||
  180. dashboardPermissions.isEditableByEveryone ||
  181. dashboardCreator?.id === currentUser.id
  182. ) {
  183. return true;
  184. }
  185. if (dashboardPermissions.teamsWithEditAccess?.length) {
  186. const userTeamIds = userTeams.map(team => Number(team.id));
  187. return dashboardPermissions.teamsWithEditAccess.some(teamId =>
  188. userTeamIds.includes(teamId)
  189. );
  190. }
  191. return false;
  192. }
  193. function getDashboardLocation({
  194. organization,
  195. dashboardId,
  196. location,
  197. }: {
  198. location: Location<any>;
  199. organization: Organization;
  200. dashboardId?: string;
  201. }) {
  202. // Preserve important filter params
  203. const filterParams = pick(location.query, [
  204. 'release',
  205. 'environment',
  206. 'project',
  207. 'statsPeriod',
  208. 'start',
  209. 'end',
  210. ]);
  211. const commonPath = defined(dashboardId)
  212. ? `/dashboard/${dashboardId}/`
  213. : `/dashboards/new/`;
  214. const dashboardUrl = USING_CUSTOMER_DOMAIN
  215. ? commonPath
  216. : `/organizations/${organization.slug}${commonPath}`;
  217. return normalizeUrl({
  218. pathname: dashboardUrl,
  219. query: filterParams,
  220. });
  221. }
  222. class DashboardDetail extends Component<Props, State> {
  223. state: State = {
  224. dashboardState: this.props.initialState,
  225. modifiedDashboard: this.updateModifiedDashboard(this.props.initialState),
  226. widgetLimitReached: this.props.dashboard.widgets.length >= MAX_WIDGETS,
  227. setData: data => {
  228. this.setState(data);
  229. },
  230. widgetLegendState: new WidgetLegendSelectionState({
  231. dashboard: this.props.dashboard,
  232. organization: this.props.organization,
  233. location: this.props.location,
  234. router: this.props.router,
  235. }),
  236. isWidgetBuilderOpen: this.isRedesignedWidgetBuilder,
  237. };
  238. componentDidMount() {
  239. this.checkIfShouldMountWidgetViewerModal();
  240. }
  241. componentDidUpdate(prevProps: Props) {
  242. this.checkIfShouldMountWidgetViewerModal();
  243. if (prevProps.initialState !== this.props.initialState) {
  244. // Widget builder can toggle Edit state when saving
  245. this.setState({dashboardState: this.props.initialState});
  246. }
  247. if (
  248. prevProps.organization !== this.props.organization ||
  249. prevProps.location !== this.props.location ||
  250. prevProps.router !== this.props.router ||
  251. prevProps.dashboard !== this.props.dashboard
  252. ) {
  253. this.setState({
  254. widgetLegendState: new WidgetLegendSelectionState({
  255. organization: this.props.organization,
  256. location: this.props.location,
  257. router: this.props.router,
  258. dashboard: this.props.dashboard,
  259. }),
  260. });
  261. }
  262. }
  263. checkIfShouldMountWidgetViewerModal() {
  264. const {
  265. params: {widgetId, dashboardId},
  266. organization,
  267. dashboard,
  268. location,
  269. router,
  270. } = this.props;
  271. const {seriesData, tableData, pageLinks, totalIssuesCount, seriesResultsType} =
  272. this.state;
  273. if (isWidgetViewerPath(location.pathname)) {
  274. const widget =
  275. defined(widgetId) &&
  276. (dashboard.widgets.find(({id}) => {
  277. // This ternary exists because widgetId is in some places typed as string, while
  278. // in other cases it is typed as number. Instead of changing the type everywhere,
  279. // we check for both cases at runtime as I am not sure which is the correct type.
  280. return typeof widgetId === 'number' ? id === String(widgetId) : id === widgetId;
  281. }) ??
  282. dashboard.widgets[widgetId]);
  283. if (widget) {
  284. openWidgetViewerModal({
  285. organization,
  286. widget,
  287. seriesData: WidgetLegendNameEncoderDecoder.modifyTimeseriesNames(
  288. widget,
  289. seriesData
  290. ),
  291. seriesResultsType,
  292. tableData,
  293. pageLinks,
  294. totalIssuesCount,
  295. widgetLegendState: this.state.widgetLegendState,
  296. dashboardFilters: getDashboardFiltersFromURL(location) ?? dashboard.filters,
  297. dashboardPermissions: dashboard.permissions,
  298. dashboardCreator: dashboard.createdBy,
  299. onMetricWidgetEdit: (updatedWidget: Widget) => {
  300. const widgets = [...dashboard.widgets];
  301. const widgetIndex = dashboard.widgets.indexOf(widget);
  302. widgets[widgetIndex] = {...widgets[widgetIndex], ...updatedWidget};
  303. this.handleUpdateWidgetList(widgets);
  304. },
  305. onClose: () => {
  306. // Filter out Widget Viewer Modal query params when exiting the Modal
  307. const query = omit(location.query, Object.values(WidgetViewerQueryField));
  308. router.push({
  309. pathname: location.pathname.replace(/widget\/[0-9]+\/$/, ''),
  310. query,
  311. });
  312. },
  313. onEdit: () => {
  314. const widgetIndex = dashboard.widgets.indexOf(widget);
  315. if (organization.features.includes('dashboards-widget-builder-redesign')) {
  316. this.onEditWidget(widget);
  317. return;
  318. }
  319. if (dashboardId) {
  320. const query = omit(location.query, Object.values(WidgetViewerQueryField));
  321. router.push(
  322. normalizeUrl({
  323. pathname: `/organizations/${organization.slug}/dashboard/${dashboardId}/widget/${widgetIndex}/edit/`,
  324. query: {
  325. ...query,
  326. source: DashboardWidgetSource.DASHBOARDS,
  327. },
  328. })
  329. );
  330. return;
  331. }
  332. },
  333. });
  334. trackAnalytics('dashboards_views.widget_viewer.open', {
  335. organization,
  336. widget_type: widget.widgetType ?? WidgetType.DISCOVER,
  337. display_type: widget.displayType,
  338. });
  339. } else {
  340. // Replace the URL if the widget isn't found and raise an error in toast
  341. router.replace(
  342. normalizeUrl({
  343. pathname: `/organizations/${organization.slug}/dashboard/${dashboard.id}/`,
  344. query: location.query,
  345. })
  346. );
  347. addErrorMessage(t('Widget not found'));
  348. }
  349. }
  350. }
  351. updateModifiedDashboard(dashboardState: DashboardState) {
  352. const {dashboard} = this.props;
  353. switch (dashboardState) {
  354. case DashboardState.PREVIEW:
  355. case DashboardState.CREATE:
  356. case DashboardState.EDIT:
  357. return cloneDashboard(dashboard);
  358. default: {
  359. return null;
  360. }
  361. }
  362. }
  363. get isPreview() {
  364. const {dashboardState} = this.state;
  365. return DashboardState.PREVIEW === dashboardState;
  366. }
  367. get isEditingDashboard() {
  368. const {dashboardState} = this.state;
  369. return [
  370. DashboardState.EDIT,
  371. DashboardState.CREATE,
  372. DashboardState.PENDING_DELETE,
  373. ].includes(dashboardState);
  374. }
  375. get isWidgetBuilderRouter() {
  376. const {location, params, organization} = this.props;
  377. const {dashboardId, widgetIndex} = params;
  378. const widgetBuilderRoutes = [
  379. `/organizations/${organization.slug}/dashboards/new/widget/new/`,
  380. `/organizations/${organization.slug}/dashboard/${dashboardId}/widget/new/`,
  381. `/organizations/${organization.slug}/dashboards/new/widget/${widgetIndex}/edit/`,
  382. `/organizations/${organization.slug}/dashboard/${dashboardId}/widget/${widgetIndex}/edit/`,
  383. ];
  384. if (USING_CUSTOMER_DOMAIN) {
  385. // TODO: replace with url generation later on.
  386. widgetBuilderRoutes.push(
  387. ...[
  388. `/dashboards/new/widget/new/`,
  389. `/dashboard/${dashboardId}/widget/new/`,
  390. `/dashboards/new/widget/${widgetIndex}/edit/`,
  391. `/dashboard/${dashboardId}/widget/${widgetIndex}/edit/`,
  392. ]
  393. );
  394. }
  395. return widgetBuilderRoutes.includes(location.pathname);
  396. }
  397. get isRedesignedWidgetBuilder() {
  398. const {organization, location, params} = this.props;
  399. const {dashboardId, widgetIndex} = params;
  400. if (!organization.features.includes('dashboards-widget-builder-redesign')) {
  401. return false;
  402. }
  403. const widgetBuilderRoutes = [
  404. `/organizations/${organization.slug}/dashboard/new/widget-builder/widget/new/`,
  405. `/organizations/${organization.slug}/dashboard/${dashboardId}/widget-builder/widget/new/`,
  406. `/organizations/${organization.slug}/dashboard/new/widget-builder/widget/${widgetIndex}/edit/`,
  407. `/organizations/${organization.slug}/dashboard/${dashboardId}/widget-builder/widget/${widgetIndex}/edit/`,
  408. ];
  409. if (USING_CUSTOMER_DOMAIN) {
  410. widgetBuilderRoutes.push(
  411. ...[
  412. `/dashboards/new/widget-builder/widget/new/`,
  413. `/dashboard/${dashboardId}/widget-builder/widget/new/`,
  414. `/dashboards/new/widget-builder/widget/${widgetIndex}/edit/`,
  415. `/dashboard/${dashboardId}/widget-builder/widget/${widgetIndex}/edit/`,
  416. ]
  417. );
  418. }
  419. return widgetBuilderRoutes.includes(location.pathname);
  420. }
  421. get dashboardTitle() {
  422. const {dashboard} = this.props;
  423. const {modifiedDashboard} = this.state;
  424. return modifiedDashboard ? modifiedDashboard.title : dashboard.title;
  425. }
  426. onEdit = () => {
  427. const {dashboard, organization} = this.props;
  428. trackAnalytics('dashboards2.edit.start', {organization});
  429. this.setState({
  430. dashboardState: DashboardState.EDIT,
  431. modifiedDashboard: cloneDashboard(dashboard),
  432. });
  433. };
  434. onDelete = (dashboard: State['modifiedDashboard']) => () => {
  435. const {api, organization, location} = this.props;
  436. if (!dashboard?.id) {
  437. return;
  438. }
  439. const previousDashboardState = this.state.dashboardState;
  440. this.setState({dashboardState: DashboardState.PENDING_DELETE}, () => {
  441. deleteDashboard(api, organization.slug, dashboard.id)
  442. .then(() => {
  443. addSuccessMessage(t('Dashboard deleted'));
  444. trackAnalytics('dashboards2.delete', {organization});
  445. browserHistory.replace({
  446. pathname: `/organizations/${organization.slug}/dashboards/`,
  447. query: location.query,
  448. });
  449. })
  450. .catch(() => {
  451. this.setState({
  452. dashboardState: previousDashboardState,
  453. });
  454. });
  455. });
  456. };
  457. onCancel = () => {
  458. const {organization, dashboard, location, params} = this.props;
  459. const {modifiedDashboard} = this.state;
  460. let hasDashboardChanged = !isEqual(modifiedDashboard, dashboard);
  461. // If a dashboard has every layout undefined, then ignore the layout field
  462. // when checking equality because it is a dashboard from before the grid feature
  463. const isLegacyLayout = dashboard.widgets.every(({layout}) => !defined(layout));
  464. if (isLegacyLayout) {
  465. hasDashboardChanged = !isEqual(
  466. {
  467. ...modifiedDashboard,
  468. widgets: modifiedDashboard?.widgets.map(widget => omit(widget, 'layout')),
  469. },
  470. {...dashboard, widgets: dashboard.widgets.map(widget => omit(widget, 'layout'))}
  471. );
  472. }
  473. // Don't confirm preview cancellation regardless of dashboard state
  474. if (hasDashboardChanged && !this.isPreview) {
  475. // Ignore no-alert here, so that the confirm on cancel matches onUnload & onRouteLeave
  476. /* eslint no-alert:0 */
  477. if (!confirm(UNSAVED_MESSAGE)) {
  478. return;
  479. }
  480. }
  481. if (params.dashboardId) {
  482. trackAnalytics('dashboards2.edit.cancel', {organization});
  483. this.setState({
  484. dashboardState: DashboardState.VIEW,
  485. modifiedDashboard: null,
  486. });
  487. return;
  488. }
  489. trackAnalytics('dashboards2.create.cancel', {organization});
  490. browserHistory.replace(
  491. normalizeUrl({
  492. pathname: `/organizations/${organization.slug}/dashboards/`,
  493. query: location.query,
  494. })
  495. );
  496. };
  497. handleChangeFilter = (activeFilters: DashboardFilters) => {
  498. const {dashboard, location} = this.props;
  499. const {modifiedDashboard} = this.state;
  500. const newModifiedDashboard = modifiedDashboard || dashboard;
  501. if (
  502. Object.keys(activeFilters).every(
  503. key => !newModifiedDashboard.filters?.[key] && activeFilters[key].length === 0
  504. )
  505. ) {
  506. return;
  507. }
  508. const filterParams: DashboardFilters = {};
  509. Object.keys(activeFilters).forEach(key => {
  510. filterParams[key] = activeFilters[key].length ? activeFilters[key] : '';
  511. });
  512. if (
  513. !isEqualWith(activeFilters, dashboard.filters, (a, b) => {
  514. // This is to handle the case where dashboard filters has release:[] and the new filter is release:""
  515. if (a.length === 0 && b.length === 0) {
  516. return a === b;
  517. }
  518. return undefined;
  519. })
  520. ) {
  521. browserHistory.push({
  522. ...location,
  523. query: {
  524. ...location.query,
  525. ...filterParams,
  526. },
  527. });
  528. }
  529. };
  530. handleUpdateWidgetList = (widgets: Widget[]) => {
  531. const {organization, dashboard, api, onDashboardUpdate, location} = this.props;
  532. const {modifiedDashboard} = this.state;
  533. // Use the new widgets for calculating layout because widgets has
  534. // the most up to date information in edit state
  535. const currentLayout = getDashboardLayout(widgets);
  536. const layoutColumnDepths = calculateColumnDepths(currentLayout);
  537. const newModifiedDashboard = {
  538. ...cloneDashboard(modifiedDashboard || dashboard),
  539. widgets: assignDefaultLayout(widgets, layoutColumnDepths),
  540. };
  541. this.setState({
  542. modifiedDashboard: newModifiedDashboard,
  543. widgetLimitReached: widgets.length >= MAX_WIDGETS,
  544. });
  545. if (this.isEditingDashboard || this.isPreview) {
  546. return null;
  547. }
  548. return updateDashboard(api, organization.slug, newModifiedDashboard).then(
  549. (newDashboard: DashboardDetails) => {
  550. if (onDashboardUpdate) {
  551. onDashboardUpdate(newDashboard);
  552. this.setState({
  553. modifiedDashboard: null,
  554. });
  555. }
  556. const legendQuery =
  557. this.state.widgetLegendState.setMultipleWidgetSelectionStateURL(newDashboard);
  558. if (dashboard && newDashboard.id !== dashboard.id) {
  559. this.props.router.replace(
  560. normalizeUrl({
  561. pathname: `/organizations/${organization.slug}/dashboard/${newDashboard.id}/`,
  562. query: {
  563. ...location.query,
  564. unselectedSeries: legendQuery,
  565. },
  566. })
  567. );
  568. } else {
  569. browserHistory.replace(
  570. normalizeUrl({
  571. pathname: `/organizations/${organization.slug}/dashboard/${dashboard.id}/`,
  572. query: {
  573. ...location.query,
  574. unselectedSeries: legendQuery,
  575. },
  576. })
  577. );
  578. }
  579. addSuccessMessage(t('Dashboard updated'));
  580. return newDashboard;
  581. },
  582. // `updateDashboard` does its own error handling
  583. () => undefined
  584. );
  585. };
  586. handleAddCustomWidget = (widget: Widget) => {
  587. const {dashboard} = this.props;
  588. const {modifiedDashboard} = this.state;
  589. const newModifiedDashboard = modifiedDashboard || dashboard;
  590. this.onUpdateWidget([...newModifiedDashboard.widgets, widget]);
  591. };
  592. handleAddMetricWidget = (layout?: Widget['layout']) => {
  593. const widgetCopy = assignTempId({
  594. layout,
  595. ...defaultMetricWidget(),
  596. });
  597. const currentWidgets =
  598. this.state.modifiedDashboard?.widgets ?? this.props.dashboard.widgets;
  599. openWidgetViewerModal({
  600. organization: this.props.organization,
  601. widget: widgetCopy,
  602. widgetLegendState: this.state.widgetLegendState,
  603. onMetricWidgetEdit: widget => {
  604. const nextList = generateWidgetsAfterCompaction([...currentWidgets, widget]);
  605. this.onUpdateWidget(nextList);
  606. this.handleUpdateWidgetList(nextList);
  607. },
  608. });
  609. };
  610. onAddWidget = (dataset?: DataSet) => {
  611. const {
  612. organization,
  613. dashboard,
  614. router,
  615. location,
  616. params: {dashboardId},
  617. } = this.props;
  618. const {modifiedDashboard} = this.state;
  619. if (dataset === DataSet.METRICS) {
  620. this.handleAddMetricWidget();
  621. return;
  622. }
  623. if (organization.features.includes('dashboards-widget-builder-redesign')) {
  624. this.setState(
  625. {
  626. modifiedDashboard: cloneDashboard(modifiedDashboard ?? dashboard),
  627. },
  628. () => {
  629. this.setState({isWidgetBuilderOpen: true});
  630. let pathname = `/organizations/${organization.slug}/dashboard/${dashboardId}/widget-builder/widget/new/`;
  631. if (!defined(dashboardId)) {
  632. pathname = `/organizations/${organization.slug}/dashboards/new/widget-builder/widget/new/`;
  633. }
  634. router.push(
  635. normalizeUrl({
  636. // TODO: Replace with the old widget builder path when swapping over
  637. pathname,
  638. query: {
  639. ...location.query,
  640. ...convertWidgetToBuilderStateParams(
  641. getDefaultWidget(DATA_SET_TO_WIDGET_TYPE[dataset ?? DataSet.ERRORS])
  642. ),
  643. },
  644. })
  645. );
  646. }
  647. );
  648. return;
  649. }
  650. this.setState(
  651. {
  652. modifiedDashboard: cloneDashboard(dashboard),
  653. },
  654. () => {
  655. if (dashboardId) {
  656. router.push(
  657. normalizeUrl({
  658. pathname: `/organizations/${organization.slug}/dashboard/${dashboardId}/widget/new/`,
  659. query: {
  660. ...location.query,
  661. source: DashboardWidgetSource.DASHBOARDS,
  662. dataset,
  663. },
  664. })
  665. );
  666. }
  667. }
  668. );
  669. };
  670. onEditWidget = (widget: Widget) => {
  671. const {router, organization, params, location, dashboard} = this.props;
  672. const {modifiedDashboard} = this.state;
  673. const currentDashboard = modifiedDashboard ?? dashboard;
  674. const {dashboardId} = params;
  675. const widgetIndex = currentDashboard.widgets.indexOf(widget);
  676. this.setState({
  677. isWidgetBuilderOpen: true,
  678. });
  679. const path = defined(dashboardId)
  680. ? `/organizations/${organization.slug}/dashboard/${dashboardId}/widget-builder/widget/${widgetIndex}/edit/`
  681. : `/organizations/${organization.slug}/dashboards/new/widget-builder/widget/${widgetIndex}/edit/`;
  682. router.push(
  683. normalizeUrl({
  684. pathname: path,
  685. query: {
  686. ...location.query,
  687. ...convertWidgetToBuilderStateParams(widget),
  688. },
  689. })
  690. );
  691. };
  692. handleSaveWidget = async ({
  693. index,
  694. widget,
  695. }: {
  696. index: number | undefined;
  697. widget: Widget;
  698. }) => {
  699. if (
  700. !this.props.organization.features.includes('dashboards-widget-builder-redesign')
  701. ) {
  702. return;
  703. }
  704. const currentDashboard = this.state.modifiedDashboard ?? this.props.dashboard;
  705. // Get the "base" widget and merge the changes to persist information like tempIds and layout
  706. const baseWidget = defined(index) ? currentDashboard.widgets[index] : {};
  707. const mergedWidget = {...baseWidget, ...widget};
  708. const newWidgets = defined(index)
  709. ? [
  710. ...currentDashboard.widgets.slice(0, index),
  711. mergedWidget,
  712. ...currentDashboard.widgets.slice(index + 1),
  713. ]
  714. : [...currentDashboard.widgets, mergedWidget];
  715. try {
  716. if (!this.isEditingDashboard) {
  717. // If we're not in edit mode, send a request to update the dashboard
  718. await this.handleUpdateWidgetList(newWidgets);
  719. } else {
  720. // If we're in edit mode, update the edit state
  721. this.onUpdateWidget(newWidgets);
  722. }
  723. this.handleCloseWidgetBuilder();
  724. } catch (error) {
  725. addErrorMessage(t('Failed to save widget'));
  726. }
  727. };
  728. /* Handles POST request for Edit Access Selector Changes */
  729. onChangeEditAccess = (newDashboardPermissions: DashboardPermissions) => {
  730. const {dashboard, api, organization} = this.props;
  731. const dashboardCopy = cloneDashboard(dashboard);
  732. dashboardCopy.permissions = newDashboardPermissions;
  733. updateDashboardPermissions(api, organization.slug, dashboardCopy).then(
  734. (newDashboard: DashboardDetails) => {
  735. addSuccessMessage(t('Dashboard Edit Access updated.'));
  736. this.props.onDashboardUpdate?.(newDashboard);
  737. this.setState({
  738. modifiedDashboard: null,
  739. });
  740. return newDashboard;
  741. }
  742. );
  743. };
  744. handleCloseWidgetBuilder = () => {
  745. const {organization, router, location, params} = this.props;
  746. this.setState({isWidgetBuilderOpen: false});
  747. router.push(
  748. getDashboardLocation({
  749. organization,
  750. dashboardId: params.dashboardId,
  751. location,
  752. })
  753. );
  754. };
  755. onCommit = () => {
  756. const {api, organization, location, dashboard, onDashboardUpdate} = this.props;
  757. const {modifiedDashboard, dashboardState} = this.state;
  758. switch (dashboardState) {
  759. case DashboardState.PREVIEW:
  760. case DashboardState.CREATE: {
  761. if (modifiedDashboard) {
  762. if (this.isPreview) {
  763. trackAnalytics('dashboards_manage.templates.add', {
  764. organization,
  765. dashboard_id: dashboard.id,
  766. dashboard_title: dashboard.title,
  767. was_previewed: true,
  768. });
  769. }
  770. const newModifiedDashboard = {
  771. ...cloneDashboard(modifiedDashboard),
  772. ...getCurrentPageFilters(location),
  773. filters: getDashboardFiltersFromURL(location) ?? modifiedDashboard.filters,
  774. };
  775. createDashboard(
  776. api,
  777. organization.slug,
  778. newModifiedDashboard,
  779. this.isPreview
  780. ).then(
  781. (newDashboard: DashboardDetails) => {
  782. addSuccessMessage(t('Dashboard created'));
  783. trackAnalytics('dashboards2.create.complete', {organization});
  784. this.setState(
  785. {
  786. dashboardState: DashboardState.VIEW,
  787. },
  788. () => {
  789. // redirect to new dashboard
  790. browserHistory.replace(
  791. normalizeUrl({
  792. pathname: `/organizations/${organization.slug}/dashboard/${newDashboard.id}/`,
  793. query: {
  794. query: omit(location.query, Object.values(DashboardFilterKeys)),
  795. },
  796. })
  797. );
  798. }
  799. );
  800. },
  801. () => undefined
  802. );
  803. }
  804. break;
  805. }
  806. case DashboardState.EDIT: {
  807. // only update the dashboard if there are changes
  808. if (modifiedDashboard) {
  809. if (isEqual(dashboard, modifiedDashboard)) {
  810. this.setState({
  811. dashboardState: DashboardState.VIEW,
  812. modifiedDashboard: null,
  813. });
  814. return;
  815. }
  816. updateDashboard(api, organization.slug, modifiedDashboard).then(
  817. (newDashboard: DashboardDetails) => {
  818. if (onDashboardUpdate) {
  819. onDashboardUpdate(newDashboard);
  820. }
  821. addSuccessMessage(t('Dashboard updated'));
  822. trackAnalytics('dashboards2.edit.complete', {organization});
  823. this.setState(
  824. {
  825. dashboardState: DashboardState.VIEW,
  826. modifiedDashboard: null,
  827. },
  828. () => {
  829. if (dashboard && newDashboard.id !== dashboard.id) {
  830. browserHistory.replace(
  831. normalizeUrl({
  832. pathname: `/organizations/${organization.slug}/dashboard/${newDashboard.id}/`,
  833. query: {
  834. ...location.query,
  835. },
  836. })
  837. );
  838. }
  839. }
  840. );
  841. },
  842. // `updateDashboard` does its own error handling
  843. () => undefined
  844. );
  845. return;
  846. }
  847. this.setState({
  848. dashboardState: DashboardState.VIEW,
  849. modifiedDashboard: null,
  850. });
  851. break;
  852. }
  853. case DashboardState.VIEW:
  854. default: {
  855. this.setState({
  856. dashboardState: DashboardState.VIEW,
  857. modifiedDashboard: null,
  858. });
  859. break;
  860. }
  861. }
  862. };
  863. setModifiedDashboard = (dashboard: DashboardDetails) => {
  864. this.setState({
  865. modifiedDashboard: dashboard,
  866. });
  867. };
  868. onUpdateWidget = (widgets: Widget[]) => {
  869. this.setState((state: State) => ({
  870. ...state,
  871. widgetLimitReached: widgets.length >= MAX_WIDGETS,
  872. modifiedDashboard: {
  873. ...(state.modifiedDashboard || this.props.dashboard),
  874. widgets,
  875. },
  876. }));
  877. };
  878. renderWidgetBuilder = () => {
  879. const {children, dashboard, onDashboardUpdate} = this.props;
  880. const {modifiedDashboard} = this.state;
  881. return (
  882. <Fragment>
  883. {isValidElement(children)
  884. ? cloneElement<any>(children, {
  885. dashboard: modifiedDashboard ?? dashboard,
  886. onSave: this.isEditingDashboard
  887. ? this.onUpdateWidget
  888. : this.handleUpdateWidgetList,
  889. updateDashboardSplitDecision: (
  890. widgetId: string,
  891. splitDecision: WidgetType
  892. ) => {
  893. handleUpdateDashboardSplit({
  894. widgetId,
  895. splitDecision,
  896. dashboard,
  897. modifiedDashboard,
  898. stateSetter: this.setState.bind(this),
  899. onDashboardUpdate,
  900. });
  901. },
  902. })
  903. : children}
  904. </Fragment>
  905. );
  906. };
  907. renderDefaultDashboardDetail() {
  908. const {organization, dashboard, dashboards, params, router, location} = this.props;
  909. const {modifiedDashboard, dashboardState, widgetLimitReached} = this.state;
  910. const {dashboardId} = params;
  911. return (
  912. <PageFiltersContainer
  913. disablePersistence
  914. defaultSelection={{
  915. datetime: {
  916. start: null,
  917. end: null,
  918. utc: false,
  919. period: DEFAULT_STATS_PERIOD,
  920. },
  921. }}
  922. >
  923. <Layout.Page withPadding>
  924. <OnDemandControlProvider location={location}>
  925. <MetricsResultsMetaProvider>
  926. <NoProjectMessage organization={organization}>
  927. <StyledPageHeader>
  928. <Layout.Title>
  929. <DashboardTitle
  930. dashboard={modifiedDashboard ?? dashboard}
  931. onUpdate={this.setModifiedDashboard}
  932. isEditingDashboard={this.isEditingDashboard}
  933. />
  934. </Layout.Title>
  935. <Controls
  936. organization={organization}
  937. dashboards={dashboards}
  938. dashboard={dashboard}
  939. onEdit={this.onEdit}
  940. onCancel={this.onCancel}
  941. onCommit={this.onCommit}
  942. onAddWidget={this.onAddWidget}
  943. onChangeEditAccess={this.onChangeEditAccess}
  944. onDelete={this.onDelete(dashboard)}
  945. dashboardState={dashboardState}
  946. widgetLimitReached={widgetLimitReached}
  947. />
  948. </StyledPageHeader>
  949. <HookHeader organization={organization} />
  950. <FiltersBar
  951. dashboardPermissions={dashboard.permissions}
  952. dashboardCreator={dashboard.createdBy}
  953. filters={{}} // Default Dashboards don't have filters set
  954. location={location}
  955. hasUnsavedChanges={false}
  956. isEditingDashboard={false}
  957. isPreview={false}
  958. onDashboardFilterChange={this.handleChangeFilter}
  959. />
  960. <MetricsCardinalityProvider
  961. organization={organization}
  962. location={location}
  963. >
  964. <MetricsDataSwitcher
  965. organization={organization}
  966. eventView={EventView.fromLocation(location)}
  967. location={location}
  968. >
  969. {metricsDataSide => (
  970. <MEPSettingProvider
  971. location={location}
  972. forceTransactions={metricsDataSide.forceTransactionsOnly}
  973. >
  974. <Dashboard
  975. paramDashboardId={dashboardId}
  976. dashboard={modifiedDashboard ?? dashboard}
  977. organization={organization}
  978. isEditingDashboard={this.isEditingDashboard}
  979. widgetLimitReached={widgetLimitReached}
  980. onUpdate={this.onUpdateWidget}
  981. handleUpdateWidgetList={this.handleUpdateWidgetList}
  982. handleAddCustomWidget={this.handleAddCustomWidget}
  983. handleAddMetricWidget={this.handleAddMetricWidget}
  984. isPreview={this.isPreview}
  985. router={router}
  986. location={location}
  987. widgetLegendState={this.state.widgetLegendState}
  988. />
  989. </MEPSettingProvider>
  990. )}
  991. </MetricsDataSwitcher>
  992. </MetricsCardinalityProvider>
  993. </NoProjectMessage>
  994. </MetricsResultsMetaProvider>
  995. </OnDemandControlProvider>
  996. </Layout.Page>
  997. </PageFiltersContainer>
  998. );
  999. }
  1000. getBreadcrumbLabel() {
  1001. const {dashboardState} = this.state;
  1002. let label = this.dashboardTitle;
  1003. if (dashboardState === DashboardState.CREATE) {
  1004. label = t('Create Dashboard');
  1005. } else if (this.isPreview) {
  1006. label = t('Preview Dashboard');
  1007. }
  1008. return label;
  1009. }
  1010. renderDashboardDetail() {
  1011. const {
  1012. api,
  1013. organization,
  1014. dashboard,
  1015. dashboards,
  1016. params,
  1017. router,
  1018. location,
  1019. newWidget,
  1020. onSetNewWidget,
  1021. onDashboardUpdate,
  1022. projects,
  1023. } = this.props;
  1024. const {modifiedDashboard, dashboardState, widgetLimitReached, seriesData, setData} =
  1025. this.state;
  1026. const {dashboardId} = params;
  1027. const hasUnsavedFilters =
  1028. dashboard.id !== 'default-overview' &&
  1029. dashboardState !== DashboardState.CREATE &&
  1030. hasUnsavedFilterChanges(dashboard, location);
  1031. const eventView = generatePerformanceEventView(location, projects, {}, organization);
  1032. const isDashboardUsingTransaction = dashboard.widgets.some(
  1033. isWidgetUsingTransactionName
  1034. );
  1035. return (
  1036. <SentryDocumentTitle title={dashboard.title} orgSlug={organization.slug}>
  1037. <PageFiltersContainer
  1038. disablePersistence
  1039. defaultSelection={{
  1040. datetime: {
  1041. start: null,
  1042. end: null,
  1043. utc: false,
  1044. period: DEFAULT_STATS_PERIOD,
  1045. },
  1046. }}
  1047. >
  1048. <Layout.Page>
  1049. <OnDemandControlProvider location={location}>
  1050. <MetricsResultsMetaProvider>
  1051. <NoProjectMessage organization={organization}>
  1052. <Layout.Header>
  1053. <Layout.HeaderContent>
  1054. <Breadcrumbs
  1055. crumbs={[
  1056. {
  1057. label: t('Dashboards'),
  1058. to: `/organizations/${organization.slug}/dashboards/`,
  1059. },
  1060. {
  1061. label: this.getBreadcrumbLabel(),
  1062. },
  1063. ]}
  1064. />
  1065. <Layout.Title>
  1066. <DashboardTitle
  1067. dashboard={modifiedDashboard ?? dashboard}
  1068. onUpdate={this.setModifiedDashboard}
  1069. isEditingDashboard={this.isEditingDashboard}
  1070. />
  1071. </Layout.Title>
  1072. </Layout.HeaderContent>
  1073. <Layout.HeaderActions>
  1074. <Controls
  1075. organization={organization}
  1076. dashboards={dashboards}
  1077. dashboard={dashboard}
  1078. hasUnsavedFilters={hasUnsavedFilters}
  1079. onEdit={this.onEdit}
  1080. onCancel={this.onCancel}
  1081. onCommit={this.onCommit}
  1082. onAddWidget={this.onAddWidget}
  1083. onDelete={this.onDelete(dashboard)}
  1084. onChangeEditAccess={this.onChangeEditAccess}
  1085. dashboardState={dashboardState}
  1086. widgetLimitReached={widgetLimitReached}
  1087. />
  1088. </Layout.HeaderActions>
  1089. </Layout.Header>
  1090. <Layout.Body>
  1091. <Layout.Main fullWidth>
  1092. <MetricsCardinalityProvider
  1093. organization={organization}
  1094. location={location}
  1095. >
  1096. <MetricsDataSwitcher
  1097. organization={organization}
  1098. eventView={eventView}
  1099. location={location}
  1100. >
  1101. {metricsDataSide => (
  1102. <MEPSettingProvider
  1103. location={location}
  1104. forceTransactions={metricsDataSide.forceTransactionsOnly}
  1105. >
  1106. {isDashboardUsingTransaction ? (
  1107. <MetricsDataSwitcherAlert
  1108. organization={organization}
  1109. eventView={eventView}
  1110. projects={projects}
  1111. location={location}
  1112. router={router}
  1113. source={DiscoverQueryPageSource.DISCOVER}
  1114. {...metricsDataSide}
  1115. />
  1116. ) : null}
  1117. <FiltersBar
  1118. filters={(modifiedDashboard ?? dashboard).filters}
  1119. dashboardPermissions={dashboard.permissions}
  1120. dashboardCreator={dashboard.createdBy}
  1121. location={location}
  1122. hasUnsavedChanges={hasUnsavedFilters}
  1123. isEditingDashboard={
  1124. dashboardState !== DashboardState.CREATE &&
  1125. this.isEditingDashboard
  1126. }
  1127. isPreview={this.isPreview}
  1128. onDashboardFilterChange={this.handleChangeFilter}
  1129. onCancel={() => {
  1130. resetPageFilters(dashboard, location);
  1131. trackAnalytics('dashboards2.filter.cancel', {
  1132. organization,
  1133. });
  1134. this.setState({
  1135. modifiedDashboard: {
  1136. ...(modifiedDashboard ?? dashboard),
  1137. filters: dashboard.filters,
  1138. },
  1139. });
  1140. }}
  1141. onSave={() => {
  1142. const newModifiedDashboard = {
  1143. ...cloneDashboard(modifiedDashboard ?? dashboard),
  1144. ...getCurrentPageFilters(location),
  1145. filters:
  1146. getDashboardFiltersFromURL(location) ??
  1147. (modifiedDashboard ?? dashboard).filters,
  1148. };
  1149. updateDashboard(
  1150. api,
  1151. organization.slug,
  1152. newModifiedDashboard
  1153. ).then(
  1154. (newDashboard: DashboardDetails) => {
  1155. addSuccessMessage(t('Dashboard filters updated'));
  1156. trackAnalytics('dashboards2.filter.save', {
  1157. organization,
  1158. });
  1159. const navigateToDashboard = () => {
  1160. browserHistory.replace(
  1161. normalizeUrl({
  1162. pathname: `/organizations/${organization.slug}/dashboard/${newDashboard.id}/`,
  1163. query: omit(
  1164. location.query,
  1165. Object.values(DashboardFilterKeys)
  1166. ),
  1167. })
  1168. );
  1169. };
  1170. if (onDashboardUpdate) {
  1171. onDashboardUpdate(newDashboard);
  1172. this.setState(
  1173. {
  1174. modifiedDashboard: null,
  1175. },
  1176. () => {
  1177. // Wait for modifiedDashboard state to update before navigating
  1178. navigateToDashboard();
  1179. }
  1180. );
  1181. return;
  1182. }
  1183. navigateToDashboard();
  1184. },
  1185. // `updateDashboard` does its own error handling
  1186. () => undefined
  1187. );
  1188. }}
  1189. />
  1190. <WidgetViewerContext.Provider value={{seriesData, setData}}>
  1191. <Fragment>
  1192. <Dashboard
  1193. paramDashboardId={dashboardId}
  1194. dashboard={modifiedDashboard ?? dashboard}
  1195. organization={organization}
  1196. isEditingDashboard={this.isEditingDashboard}
  1197. widgetLimitReached={widgetLimitReached}
  1198. onUpdate={this.onUpdateWidget}
  1199. handleUpdateWidgetList={this.handleUpdateWidgetList}
  1200. handleAddCustomWidget={this.handleAddCustomWidget}
  1201. handleAddMetricWidget={this.handleAddMetricWidget}
  1202. router={router}
  1203. location={location}
  1204. newWidget={newWidget}
  1205. onSetNewWidget={onSetNewWidget}
  1206. isPreview={this.isPreview}
  1207. widgetLegendState={this.state.widgetLegendState}
  1208. onAddWidget={this.onAddWidget}
  1209. onEditWidget={this.onEditWidget}
  1210. />
  1211. <WidgetBuilderV2
  1212. isOpen={this.state.isWidgetBuilderOpen}
  1213. onClose={this.handleCloseWidgetBuilder}
  1214. dashboardFilters={
  1215. getDashboardFiltersFromURL(location) ??
  1216. dashboard.filters
  1217. }
  1218. dashboard={modifiedDashboard ?? dashboard}
  1219. onSave={this.handleSaveWidget}
  1220. />
  1221. </Fragment>
  1222. </WidgetViewerContext.Provider>
  1223. </MEPSettingProvider>
  1224. )}
  1225. </MetricsDataSwitcher>
  1226. </MetricsCardinalityProvider>
  1227. </Layout.Main>
  1228. </Layout.Body>
  1229. </NoProjectMessage>
  1230. </MetricsResultsMetaProvider>
  1231. </OnDemandControlProvider>
  1232. </Layout.Page>
  1233. </PageFiltersContainer>
  1234. </SentryDocumentTitle>
  1235. );
  1236. }
  1237. render() {
  1238. const {organization} = this.props;
  1239. if (this.isWidgetBuilderRouter) {
  1240. return this.renderWidgetBuilder();
  1241. }
  1242. if (organization.features.includes('dashboards-edit')) {
  1243. return this.renderDashboardDetail();
  1244. }
  1245. return this.renderDefaultDashboardDetail();
  1246. }
  1247. }
  1248. const StyledPageHeader = styled('div')`
  1249. display: grid;
  1250. grid-template-columns: minmax(0, 1fr);
  1251. grid-row-gap: ${space(2)};
  1252. align-items: center;
  1253. margin-bottom: ${space(2)};
  1254. @media (min-width: ${p => p.theme.breakpoints.medium}) {
  1255. grid-template-columns: minmax(0, 1fr) max-content;
  1256. grid-column-gap: ${space(2)};
  1257. height: 40px;
  1258. }
  1259. `;
  1260. export default withPageFilters(withProjects(withApi(withOrganization(DashboardDetail))));