orgDashboards.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import {browserHistory} from 'react-router';
  2. import {Location} from 'history';
  3. import isEqual from 'lodash/isEqual';
  4. import {Client} from 'sentry/api';
  5. import AsyncComponent from 'sentry/components/asyncComponent';
  6. import NotFound from 'sentry/components/errors/notFound';
  7. import LoadingIndicator from 'sentry/components/loadingIndicator';
  8. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  9. import {t} from 'sentry/locale';
  10. import {PageContent} from 'sentry/styles/organization';
  11. import {Organization} from 'sentry/types';
  12. import {trackAnalyticsEvent} from 'sentry/utils/analytics';
  13. import {assignTempId} from './layoutUtils';
  14. import {DashboardDetails, DashboardListItem} from './types';
  15. type OrgDashboardsChildrenProps = {
  16. dashboard: DashboardDetails | null;
  17. dashboards: DashboardListItem[];
  18. error: boolean;
  19. onDashboardUpdate: (updatedDashboard: DashboardDetails) => void;
  20. };
  21. type Props = {
  22. api: Client;
  23. children: (props: OrgDashboardsChildrenProps) => React.ReactNode;
  24. location: Location;
  25. organization: Organization;
  26. params: {orgId: string; dashboardId?: string};
  27. };
  28. type State = {
  29. // endpoint response
  30. dashboards: DashboardListItem[] | null;
  31. /**
  32. * The currently selected dashboard.
  33. */
  34. selectedDashboard: DashboardDetails | null;
  35. } & AsyncComponent['state'];
  36. class OrgDashboards extends AsyncComponent<Props, State> {
  37. state: State = {
  38. // AsyncComponent state
  39. loading: true,
  40. reloading: false,
  41. error: false,
  42. errors: {},
  43. dashboards: [],
  44. selectedDashboard: null,
  45. };
  46. componentDidUpdate(prevProps: Props) {
  47. if (!isEqual(prevProps.params.dashboardId, this.props.params.dashboardId)) {
  48. this.remountComponent();
  49. }
  50. }
  51. getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
  52. const {organization, params} = this.props;
  53. const url = `/organizations/${organization.slug}/dashboards/`;
  54. const endpoints: ReturnType<AsyncComponent['getEndpoints']> = [['dashboards', url]];
  55. if (params.dashboardId) {
  56. endpoints.push(['selectedDashboard', `${url}${params.dashboardId}/`]);
  57. trackAnalyticsEvent({
  58. eventKey: 'dashboards2.view',
  59. eventName: 'Dashboards2: View dashboard',
  60. organization_id: parseInt(this.props.organization.id, 10),
  61. dashboard_id: params.dashboardId,
  62. });
  63. }
  64. return endpoints;
  65. }
  66. onDashboardUpdate(updatedDashboard: DashboardDetails) {
  67. this.setState({selectedDashboard: updatedDashboard});
  68. }
  69. getDashboards(): DashboardListItem[] {
  70. const {dashboards} = this.state;
  71. return Array.isArray(dashboards) ? dashboards : [];
  72. }
  73. onRequestSuccess({stateKey, data}) {
  74. const {params, organization, location} = this.props;
  75. if (params.dashboardId || stateKey === 'selectedDashboard') {
  76. return;
  77. }
  78. // If we don't have a selected dashboard, and one isn't going to arrive
  79. // we can redirect to the first dashboard in the list.
  80. const dashboardId = data.length ? data[0].id : 'default-overview';
  81. const url = `/organizations/${organization.slug}/dashboard/${dashboardId}/`;
  82. browserHistory.replace({
  83. pathname: url,
  84. query: {
  85. ...location.query,
  86. },
  87. });
  88. }
  89. renderLoading() {
  90. return (
  91. <PageContent>
  92. <LoadingIndicator />
  93. </PageContent>
  94. );
  95. }
  96. renderBody() {
  97. const {children, organization} = this.props;
  98. const {selectedDashboard, error} = this.state;
  99. let dashboard = selectedDashboard;
  100. if (organization.features.includes('dashboard-grid-layout')) {
  101. // Ensure there are always tempIds for grid layout
  102. // This is needed because there are cases where the dashboard
  103. // renders before the onRequestSuccess setState is processed
  104. // and will caused stacked widgets because of missing tempIds
  105. dashboard = selectedDashboard
  106. ? {
  107. ...selectedDashboard,
  108. widgets: selectedDashboard.widgets.map(assignTempId),
  109. }
  110. : null;
  111. }
  112. return children({
  113. error,
  114. dashboard,
  115. dashboards: this.getDashboards(),
  116. onDashboardUpdate: (updatedDashboard: DashboardDetails) =>
  117. this.onDashboardUpdate(updatedDashboard),
  118. });
  119. }
  120. renderError(error: Error) {
  121. const notFound = Object.values(this.state.errors).find(
  122. resp => resp && resp.status === 404
  123. );
  124. if (notFound) {
  125. return <NotFound />;
  126. }
  127. return super.renderError(error, true);
  128. }
  129. renderComponent() {
  130. const {organization, location} = this.props;
  131. if (!organization.features.includes('dashboards-basic')) {
  132. // Redirect to Dashboards v1
  133. browserHistory.replace({
  134. pathname: `/organizations/${organization.slug}/dashboards/`,
  135. query: {
  136. ...location.query,
  137. },
  138. });
  139. return null;
  140. }
  141. return (
  142. <SentryDocumentTitle title={t('Dashboards')} orgSlug={organization.slug}>
  143. {super.renderComponent() as React.ReactChild}
  144. </SentryDocumentTitle>
  145. );
  146. }
  147. }
  148. export default OrgDashboards;