detail.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. import {cloneElement, Component, isValidElement} from 'react';
  2. import {browserHistory, PlainRoute, RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import cloneDeep from 'lodash/cloneDeep';
  5. import isEqual from 'lodash/isEqual';
  6. import omit from 'lodash/omit';
  7. import {
  8. createDashboard,
  9. deleteDashboard,
  10. updateDashboard,
  11. } from 'sentry/actionCreators/dashboards';
  12. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  13. import {
  14. openAddDashboardWidgetModal,
  15. openWidgetViewerModal,
  16. } from 'sentry/actionCreators/modal';
  17. import {Client} from 'sentry/api';
  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 {t} from 'sentry/locale';
  29. import {PageContent} from 'sentry/styles/organization';
  30. import space from 'sentry/styles/space';
  31. import {Organization} from 'sentry/types';
  32. import {defined} from 'sentry/utils';
  33. import {trackAnalyticsEvent} from 'sentry/utils/analytics';
  34. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  35. import withApi from 'sentry/utils/withApi';
  36. import withOrganization from 'sentry/utils/withOrganization';
  37. import Controls from './controls';
  38. import Dashboard from './dashboard';
  39. import {DEFAULT_STATS_PERIOD} from './data';
  40. import {
  41. assignDefaultLayout,
  42. calculateColumnDepths,
  43. getDashboardLayout,
  44. } from './layoutUtils';
  45. import DashboardTitle from './title';
  46. import {
  47. DashboardDetails,
  48. DashboardListItem,
  49. DashboardState,
  50. DashboardWidgetSource,
  51. MAX_WIDGETS,
  52. Widget,
  53. WidgetType,
  54. } from './types';
  55. import {cloneDashboard} from './utils';
  56. const UNSAVED_MESSAGE = t('You have unsaved changes, are you sure you want to leave?');
  57. const HookHeader = HookOrDefault({hookName: 'component:dashboards-header'});
  58. type RouteParams = {
  59. orgId: string;
  60. dashboardId?: string;
  61. widgetId?: number;
  62. widgetIndex?: number;
  63. };
  64. type Props = RouteComponentProps<RouteParams, {}> & {
  65. api: Client;
  66. dashboard: DashboardDetails;
  67. dashboards: DashboardListItem[];
  68. initialState: DashboardState;
  69. organization: Organization;
  70. route: PlainRoute;
  71. newWidget?: Widget;
  72. onDashboardUpdate?: (updatedDashboard: DashboardDetails) => void;
  73. onSetNewWidget?: () => void;
  74. };
  75. type State = {
  76. dashboardState: DashboardState;
  77. modifiedDashboard: DashboardDetails | null;
  78. widgetLimitReached: boolean;
  79. };
  80. class DashboardDetail extends Component<Props, State> {
  81. state: State = {
  82. dashboardState: this.props.initialState,
  83. modifiedDashboard: this.updateModifiedDashboard(this.props.initialState),
  84. widgetLimitReached: this.props.dashboard.widgets.length >= MAX_WIDGETS,
  85. };
  86. componentDidMount() {
  87. const {route, router} = this.props;
  88. router.setRouteLeaveHook(route, this.onRouteLeave);
  89. window.addEventListener('beforeunload', this.onUnload);
  90. this.checkIfShouldMountWidgetViewerModal();
  91. }
  92. componentDidUpdate() {
  93. this.checkIfShouldMountWidgetViewerModal();
  94. }
  95. componentWillUnmount() {
  96. window.removeEventListener('beforeunload', this.onUnload);
  97. }
  98. checkIfShouldMountWidgetViewerModal() {
  99. const {
  100. params: {widgetId},
  101. organization,
  102. dashboard,
  103. location,
  104. router,
  105. } = this.props;
  106. if (isWidgetViewerPath(location.pathname)) {
  107. const widget =
  108. defined(widgetId) &&
  109. (dashboard.widgets.find(({id}) => id === String(widgetId)) ??
  110. dashboard.widgets[widgetId]);
  111. if (widget) {
  112. openWidgetViewerModal({
  113. organization,
  114. widget,
  115. onClose: () => {
  116. // Filter out Widget Viewer Modal query params when exiting the Modal
  117. const query = omit(location.query, Object.values(WidgetViewerQueryField));
  118. router.push({
  119. pathname: location.pathname.replace(/widget\/[0-9]+\/$/, ''),
  120. query,
  121. });
  122. },
  123. onEdit: () => {
  124. openAddDashboardWidgetModal({
  125. organization,
  126. widget,
  127. onUpdateWidget: (nextWidget: Widget) => {
  128. const updateIndex = dashboard.widgets.indexOf(widget);
  129. const nextWidgetsList = cloneDeep(dashboard.widgets);
  130. nextWidgetsList[updateIndex] = nextWidget;
  131. this.handleUpdateWidgetList(nextWidgetsList);
  132. },
  133. source: DashboardWidgetSource.DASHBOARDS,
  134. });
  135. },
  136. });
  137. trackAdvancedAnalyticsEvent('dashboards_views.widget_viewer.open', {
  138. organization,
  139. widget_type: widget.widgetType ?? WidgetType.DISCOVER,
  140. display_type: widget.displayType,
  141. });
  142. } else {
  143. // Replace the URL if the widget isn't found and raise an error in toast
  144. router.replace({
  145. pathname: `/organizations/${organization.slug}/dashboard/${dashboard.id}/`,
  146. query: location.query,
  147. });
  148. addErrorMessage(t('Widget not found'));
  149. }
  150. }
  151. }
  152. updateModifiedDashboard(dashboardState: DashboardState) {
  153. const {dashboard} = this.props;
  154. switch (dashboardState) {
  155. case DashboardState.PREVIEW:
  156. case DashboardState.CREATE:
  157. case DashboardState.EDIT:
  158. return cloneDashboard(dashboard);
  159. default: {
  160. return null;
  161. }
  162. }
  163. }
  164. get isPreview() {
  165. const {dashboardState} = this.state;
  166. return DashboardState.PREVIEW === dashboardState;
  167. }
  168. get isEditing() {
  169. const {dashboardState} = this.state;
  170. return [
  171. DashboardState.EDIT,
  172. DashboardState.CREATE,
  173. DashboardState.PENDING_DELETE,
  174. ].includes(dashboardState);
  175. }
  176. get isWidgetBuilderRouter() {
  177. const {location, params, organization} = this.props;
  178. const {dashboardId, widgetIndex} = params;
  179. const widgetBuilderRoutes = [
  180. `/organizations/${organization.slug}/dashboards/new/widget/new/`,
  181. `/organizations/${organization.slug}/dashboard/${dashboardId}/widget/new/`,
  182. `/organizations/${organization.slug}/dashboards/new/widget/${widgetIndex}/edit/`,
  183. `/organizations/${organization.slug}/dashboard/${dashboardId}/widget/${widgetIndex}/edit/`,
  184. ];
  185. return widgetBuilderRoutes.includes(location.pathname);
  186. }
  187. get dashboardTitle() {
  188. const {dashboard} = this.props;
  189. const {modifiedDashboard} = this.state;
  190. return modifiedDashboard ? modifiedDashboard.title : dashboard.title;
  191. }
  192. onEdit = () => {
  193. const {dashboard} = this.props;
  194. trackAnalyticsEvent({
  195. eventKey: 'dashboards2.edit.start',
  196. eventName: 'Dashboards2: Edit start',
  197. organization_id: parseInt(this.props.organization.id, 10),
  198. });
  199. this.setState({
  200. dashboardState: DashboardState.EDIT,
  201. modifiedDashboard: cloneDashboard(dashboard),
  202. });
  203. };
  204. onRouteLeave = () => {
  205. const {dashboard} = this.props;
  206. const {modifiedDashboard} = this.state;
  207. if (
  208. ![
  209. DashboardState.VIEW,
  210. DashboardState.PENDING_DELETE,
  211. DashboardState.PREVIEW,
  212. ].includes(this.state.dashboardState) &&
  213. !isEqual(modifiedDashboard, dashboard)
  214. ) {
  215. return UNSAVED_MESSAGE;
  216. }
  217. return undefined;
  218. };
  219. onUnload = (event: BeforeUnloadEvent) => {
  220. const {dashboard} = this.props;
  221. const {modifiedDashboard} = this.state;
  222. if (
  223. [
  224. DashboardState.VIEW,
  225. DashboardState.PENDING_DELETE,
  226. DashboardState.PREVIEW,
  227. ].includes(this.state.dashboardState) ||
  228. isEqual(modifiedDashboard, dashboard)
  229. ) {
  230. return;
  231. }
  232. event.preventDefault();
  233. event.returnValue = UNSAVED_MESSAGE;
  234. };
  235. onDelete = (dashboard: State['modifiedDashboard']) => () => {
  236. const {api, organization, location} = this.props;
  237. if (!dashboard?.id) {
  238. return;
  239. }
  240. const previousDashboardState = this.state.dashboardState;
  241. this.setState({dashboardState: DashboardState.PENDING_DELETE}, () => {
  242. deleteDashboard(api, organization.slug, dashboard.id)
  243. .then(() => {
  244. addSuccessMessage(t('Dashboard deleted'));
  245. trackAnalyticsEvent({
  246. eventKey: 'dashboards2.delete',
  247. eventName: 'Dashboards2: Delete',
  248. organization_id: parseInt(this.props.organization.id, 10),
  249. });
  250. browserHistory.replace({
  251. pathname: `/organizations/${organization.slug}/dashboards/`,
  252. query: location.query,
  253. });
  254. })
  255. .catch(() => {
  256. this.setState({
  257. dashboardState: previousDashboardState,
  258. });
  259. });
  260. });
  261. };
  262. onCancel = () => {
  263. const {organization, dashboard, location, params} = this.props;
  264. const {modifiedDashboard} = this.state;
  265. let hasDashboardChanged = !isEqual(modifiedDashboard, dashboard);
  266. // If a dashboard has every layout undefined, then ignore the layout field
  267. // when checking equality because it is a dashboard from before the grid feature
  268. const isLegacyLayout = dashboard.widgets.every(({layout}) => !defined(layout));
  269. if (isLegacyLayout) {
  270. hasDashboardChanged = !isEqual(
  271. {
  272. ...modifiedDashboard,
  273. widgets: modifiedDashboard?.widgets.map(widget => omit(widget, 'layout')),
  274. },
  275. {...dashboard, widgets: dashboard.widgets.map(widget => omit(widget, 'layout'))}
  276. );
  277. }
  278. // Don't confirm preview cancellation regardless of dashboard state
  279. if (hasDashboardChanged && !this.isPreview) {
  280. // Ignore no-alert here, so that the confirm on cancel matches onUnload & onRouteLeave
  281. /* eslint no-alert:0 */
  282. if (!confirm(UNSAVED_MESSAGE)) {
  283. return;
  284. }
  285. }
  286. if (params.dashboardId) {
  287. trackAnalyticsEvent({
  288. eventKey: 'dashboards2.edit.cancel',
  289. eventName: 'Dashboards2: Edit cancel',
  290. organization_id: parseInt(this.props.organization.id, 10),
  291. });
  292. this.setState({
  293. dashboardState: DashboardState.VIEW,
  294. modifiedDashboard: null,
  295. });
  296. return;
  297. }
  298. trackAnalyticsEvent({
  299. eventKey: 'dashboards2.create.cancel',
  300. eventName: 'Dashboards2: Create cancel',
  301. organization_id: parseInt(this.props.organization.id, 10),
  302. });
  303. browserHistory.replace({
  304. pathname: `/organizations/${organization.slug}/dashboards/`,
  305. query: location.query,
  306. });
  307. };
  308. handleUpdateWidgetList = (widgets: Widget[]) => {
  309. const {organization, dashboard, api, onDashboardUpdate, location} = this.props;
  310. const {modifiedDashboard} = this.state;
  311. // Use the new widgets for calculating layout because widgets has
  312. // the most up to date information in edit state
  313. const currentLayout = getDashboardLayout(widgets);
  314. const layoutColumnDepths = calculateColumnDepths(currentLayout);
  315. const newModifiedDashboard = {
  316. ...cloneDashboard(modifiedDashboard || dashboard),
  317. widgets: assignDefaultLayout(widgets, layoutColumnDepths),
  318. };
  319. this.setState({
  320. modifiedDashboard: newModifiedDashboard,
  321. widgetLimitReached: widgets.length >= MAX_WIDGETS,
  322. });
  323. if (this.isEditing || this.isPreview) {
  324. return;
  325. }
  326. updateDashboard(api, organization.slug, newModifiedDashboard).then(
  327. (newDashboard: DashboardDetails) => {
  328. if (onDashboardUpdate) {
  329. onDashboardUpdate(newDashboard);
  330. this.setState({
  331. modifiedDashboard: null,
  332. });
  333. }
  334. addSuccessMessage(t('Dashboard updated'));
  335. if (dashboard && newDashboard.id !== dashboard.id) {
  336. browserHistory.replace({
  337. pathname: `/organizations/${organization.slug}/dashboard/${newDashboard.id}/`,
  338. query: {
  339. ...location.query,
  340. },
  341. });
  342. return;
  343. }
  344. },
  345. () => undefined
  346. );
  347. };
  348. handleAddCustomWidget = (widget: Widget) => {
  349. const {dashboard} = this.props;
  350. const {modifiedDashboard} = this.state;
  351. const newModifiedDashboard = modifiedDashboard || dashboard;
  352. this.onUpdateWidget([...newModifiedDashboard.widgets, widget]);
  353. };
  354. onAddWidget = () => {
  355. const {organization, dashboard} = this.props;
  356. this.setState({
  357. modifiedDashboard: cloneDashboard(dashboard),
  358. });
  359. openAddDashboardWidgetModal({
  360. organization,
  361. dashboard,
  362. onAddLibraryWidget: (widgets: Widget[]) => this.handleUpdateWidgetList(widgets),
  363. source: DashboardWidgetSource.LIBRARY,
  364. });
  365. };
  366. onCommit = () => {
  367. const {api, organization, location, dashboard, onDashboardUpdate} = this.props;
  368. const {modifiedDashboard, dashboardState} = this.state;
  369. switch (dashboardState) {
  370. case DashboardState.PREVIEW:
  371. case DashboardState.CREATE: {
  372. if (modifiedDashboard) {
  373. if (this.isPreview) {
  374. trackAdvancedAnalyticsEvent('dashboards_manage.templates.add', {
  375. organization,
  376. dashboard_id: dashboard.id,
  377. dashboard_title: dashboard.title,
  378. was_previewed: true,
  379. });
  380. }
  381. createDashboard(api, organization.slug, modifiedDashboard, this.isPreview).then(
  382. (newDashboard: DashboardDetails) => {
  383. addSuccessMessage(t('Dashboard created'));
  384. trackAnalyticsEvent({
  385. eventKey: 'dashboards2.create.complete',
  386. eventName: 'Dashboards2: Create complete',
  387. organization_id: parseInt(organization.id, 10),
  388. });
  389. this.setState({
  390. dashboardState: DashboardState.VIEW,
  391. });
  392. // redirect to new dashboard
  393. browserHistory.replace({
  394. pathname: `/organizations/${organization.slug}/dashboard/${newDashboard.id}/`,
  395. query: {
  396. ...location.query,
  397. },
  398. });
  399. },
  400. () => undefined
  401. );
  402. }
  403. break;
  404. }
  405. case DashboardState.EDIT: {
  406. // only update the dashboard if there are changes
  407. if (modifiedDashboard) {
  408. if (isEqual(dashboard, modifiedDashboard)) {
  409. this.setState({
  410. dashboardState: DashboardState.VIEW,
  411. modifiedDashboard: null,
  412. });
  413. return;
  414. }
  415. updateDashboard(api, organization.slug, modifiedDashboard).then(
  416. (newDashboard: DashboardDetails) => {
  417. if (onDashboardUpdate) {
  418. onDashboardUpdate(newDashboard);
  419. }
  420. addSuccessMessage(t('Dashboard updated'));
  421. trackAnalyticsEvent({
  422. eventKey: 'dashboards2.edit.complete',
  423. eventName: 'Dashboards2: Edit complete',
  424. organization_id: parseInt(organization.id, 10),
  425. });
  426. this.setState({
  427. dashboardState: DashboardState.VIEW,
  428. modifiedDashboard: null,
  429. });
  430. if (dashboard && newDashboard.id !== dashboard.id) {
  431. browserHistory.replace({
  432. pathname: `/organizations/${organization.slug}/dashboard/${newDashboard.id}/`,
  433. query: {
  434. ...location.query,
  435. },
  436. });
  437. return;
  438. }
  439. },
  440. () => undefined
  441. );
  442. return;
  443. }
  444. this.setState({
  445. dashboardState: DashboardState.VIEW,
  446. modifiedDashboard: null,
  447. });
  448. break;
  449. }
  450. case DashboardState.VIEW:
  451. default: {
  452. this.setState({
  453. dashboardState: DashboardState.VIEW,
  454. modifiedDashboard: null,
  455. });
  456. break;
  457. }
  458. }
  459. };
  460. setModifiedDashboard = (dashboard: DashboardDetails) => {
  461. this.setState({
  462. modifiedDashboard: dashboard,
  463. });
  464. };
  465. onUpdateWidget = (widgets: Widget[]) => {
  466. this.setState((state: State) => ({
  467. ...state,
  468. widgetLimitReached: widgets.length >= MAX_WIDGETS,
  469. modifiedDashboard: {
  470. ...(state.modifiedDashboard || this.props.dashboard),
  471. widgets,
  472. },
  473. }));
  474. };
  475. renderWidgetBuilder(dashboard: DashboardDetails) {
  476. const {children} = this.props;
  477. const {modifiedDashboard} = this.state;
  478. return isValidElement(children)
  479. ? cloneElement(children, {
  480. dashboard: modifiedDashboard ?? dashboard,
  481. onSave: this.isEditing ? this.onUpdateWidget : this.handleUpdateWidgetList,
  482. })
  483. : children;
  484. }
  485. renderDefaultDashboardDetail() {
  486. const {organization, dashboard, dashboards, params, router, location} = this.props;
  487. const {modifiedDashboard, dashboardState, widgetLimitReached} = this.state;
  488. const {dashboardId} = params;
  489. return (
  490. <PageFiltersContainer
  491. skipLoadLastUsed={organization.features.includes('global-views')}
  492. defaultSelection={{
  493. datetime: {
  494. start: null,
  495. end: null,
  496. utc: false,
  497. period: DEFAULT_STATS_PERIOD,
  498. },
  499. }}
  500. >
  501. <PageContent>
  502. <NoProjectMessage organization={organization}>
  503. <StyledPageHeader>
  504. <StyledTitle>
  505. <DashboardTitle
  506. dashboard={modifiedDashboard ?? dashboard}
  507. onUpdate={this.setModifiedDashboard}
  508. isEditing={this.isEditing}
  509. />
  510. </StyledTitle>
  511. <Controls
  512. organization={organization}
  513. dashboards={dashboards}
  514. onEdit={this.onEdit}
  515. onCancel={this.onCancel}
  516. onCommit={this.onCommit}
  517. onAddWidget={this.onAddWidget}
  518. onDelete={this.onDelete(dashboard)}
  519. dashboardState={dashboardState}
  520. widgetLimitReached={widgetLimitReached}
  521. />
  522. </StyledPageHeader>
  523. <HookHeader organization={organization} />
  524. <Dashboard
  525. paramDashboardId={dashboardId}
  526. dashboard={modifiedDashboard ?? dashboard}
  527. organization={organization}
  528. isEditing={this.isEditing}
  529. widgetLimitReached={widgetLimitReached}
  530. onUpdate={this.onUpdateWidget}
  531. handleUpdateWidgetList={this.handleUpdateWidgetList}
  532. handleAddCustomWidget={this.handleAddCustomWidget}
  533. isPreview={this.isPreview}
  534. router={router}
  535. location={location}
  536. />
  537. </NoProjectMessage>
  538. </PageContent>
  539. </PageFiltersContainer>
  540. );
  541. }
  542. getBreadcrumbLabel() {
  543. const {dashboardState} = this.state;
  544. let label = this.dashboardTitle;
  545. if (dashboardState === DashboardState.CREATE) {
  546. label = t('Create Dashboard');
  547. } else if (this.isPreview) {
  548. label = t('Preview Dashboard');
  549. }
  550. return label;
  551. }
  552. renderDashboardDetail() {
  553. const {
  554. organization,
  555. dashboard,
  556. dashboards,
  557. params,
  558. router,
  559. location,
  560. newWidget,
  561. onSetNewWidget,
  562. } = this.props;
  563. const {modifiedDashboard, dashboardState, widgetLimitReached} = this.state;
  564. const {dashboardId} = params;
  565. return (
  566. <SentryDocumentTitle title={dashboard.title} orgSlug={organization.slug}>
  567. <PageFiltersContainer
  568. skipLoadLastUsed={organization.features.includes('global-views')}
  569. defaultSelection={{
  570. datetime: {
  571. start: null,
  572. end: null,
  573. utc: false,
  574. period: DEFAULT_STATS_PERIOD,
  575. },
  576. }}
  577. >
  578. <StyledPageContent>
  579. <NoProjectMessage organization={organization}>
  580. <Layout.Header>
  581. <Layout.HeaderContent>
  582. <Breadcrumbs
  583. crumbs={[
  584. {
  585. label: t('Dashboards'),
  586. to: `/organizations/${organization.slug}/dashboards/`,
  587. },
  588. {
  589. label: this.getBreadcrumbLabel(),
  590. },
  591. ]}
  592. />
  593. <Layout.Title>
  594. <DashboardTitle
  595. dashboard={modifiedDashboard ?? dashboard}
  596. onUpdate={this.setModifiedDashboard}
  597. isEditing={this.isEditing}
  598. />
  599. </Layout.Title>
  600. </Layout.HeaderContent>
  601. <Layout.HeaderActions>
  602. <Controls
  603. organization={organization}
  604. dashboards={dashboards}
  605. onEdit={this.onEdit}
  606. onCancel={this.onCancel}
  607. onCommit={this.onCommit}
  608. onAddWidget={this.onAddWidget}
  609. onDelete={this.onDelete(dashboard)}
  610. dashboardState={dashboardState}
  611. widgetLimitReached={widgetLimitReached}
  612. />
  613. </Layout.HeaderActions>
  614. </Layout.Header>
  615. <Layout.Body>
  616. <Layout.Main fullWidth>
  617. <Dashboard
  618. paramDashboardId={dashboardId}
  619. dashboard={modifiedDashboard ?? dashboard}
  620. organization={organization}
  621. isEditing={this.isEditing}
  622. widgetLimitReached={widgetLimitReached}
  623. onUpdate={this.onUpdateWidget}
  624. handleUpdateWidgetList={this.handleUpdateWidgetList}
  625. handleAddCustomWidget={this.handleAddCustomWidget}
  626. router={router}
  627. location={location}
  628. newWidget={newWidget}
  629. onSetNewWidget={onSetNewWidget}
  630. isPreview={this.isPreview}
  631. />
  632. </Layout.Main>
  633. </Layout.Body>
  634. </NoProjectMessage>
  635. </StyledPageContent>
  636. </PageFiltersContainer>
  637. </SentryDocumentTitle>
  638. );
  639. }
  640. render() {
  641. const {organization, dashboard} = this.props;
  642. if (this.isWidgetBuilderRouter) {
  643. return this.renderWidgetBuilder(dashboard);
  644. }
  645. if (organization.features.includes('dashboards-edit')) {
  646. return this.renderDashboardDetail();
  647. }
  648. return this.renderDefaultDashboardDetail();
  649. }
  650. }
  651. const StyledPageHeader = styled('div')`
  652. display: grid;
  653. grid-template-columns: minmax(0, 1fr);
  654. grid-row-gap: ${space(2)};
  655. align-items: center;
  656. margin-bottom: ${space(2)};
  657. @media (min-width: ${p => p.theme.breakpoints[1]}) {
  658. grid-template-columns: minmax(0, 1fr) max-content;
  659. grid-column-gap: ${space(2)};
  660. height: 40px;
  661. }
  662. `;
  663. const StyledTitle = styled(Layout.Title)`
  664. margin-top: 0;
  665. `;
  666. const StyledPageContent = styled(PageContent)`
  667. padding: 0;
  668. `;
  669. export default withApi(withOrganization(DashboardDetail));