detail.tsx 22 KB

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