orgDashboards.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import {useEffect, useState} from 'react';
  2. import isEqual from 'lodash/isEqual';
  3. import NotFound from 'sentry/components/errors/notFound';
  4. import * as Layout from 'sentry/components/layouts/thirds';
  5. import LoadingError from 'sentry/components/loadingError';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  8. import {t} from 'sentry/locale';
  9. import {useApiQuery} from 'sentry/utils/queryClient';
  10. import type {WithRouteAnalyticsProps} from 'sentry/utils/routeAnalytics/withRouteAnalytics';
  11. import withRouteAnalytics from 'sentry/utils/routeAnalytics/withRouteAnalytics';
  12. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  13. import {useLocation} from 'sentry/utils/useLocation';
  14. import {useNavigate} from 'sentry/utils/useNavigate';
  15. import useOrganization from 'sentry/utils/useOrganization';
  16. import {useParams} from 'sentry/utils/useParams';
  17. import {assignTempId} from './layoutUtils';
  18. import type {DashboardDetails, DashboardListItem} from './types';
  19. import {hasSavedPageFilters} from './utils';
  20. type OrgDashboardsChildrenProps = {
  21. dashboard: DashboardDetails | null;
  22. dashboards: DashboardListItem[];
  23. error: boolean;
  24. onDashboardUpdate: (updatedDashboard: DashboardDetails) => void;
  25. };
  26. type Props = WithRouteAnalyticsProps & {
  27. children: (props: OrgDashboardsChildrenProps) => React.ReactNode;
  28. };
  29. function OrgDashboards(props: Props) {
  30. const {children} = props;
  31. const location = useLocation();
  32. const organization = useOrganization();
  33. const navigate = useNavigate();
  34. const {dashboardId} = useParams<{dashboardId: string}>();
  35. const ENDPOINT = `/organizations/${organization.slug}/dashboards/`;
  36. // The currently selected dashboard
  37. const [selectedDashboardState, setSelectedDashboardState] =
  38. useState<DashboardDetails | null>(null);
  39. const {
  40. data: dashboards,
  41. isPending: isDashboardsPending,
  42. isError: isDashboardsError,
  43. error: dashboardsError,
  44. } = useApiQuery<DashboardListItem[]>([ENDPOINT], {staleTime: 0, retry: false});
  45. const {
  46. data: fetchedSelectedDashboard,
  47. isLoading: isSelectedDashboardLoading,
  48. isError: isSelectedDashboardError,
  49. error: selectedDashboardError,
  50. } = useApiQuery<DashboardDetails>([`${ENDPOINT}${dashboardId}/`], {
  51. staleTime: 0,
  52. enabled: !!dashboardId,
  53. retry: false,
  54. });
  55. const selectedDashboard = selectedDashboardState ?? fetchedSelectedDashboard;
  56. useEffect(() => {
  57. if (dashboardId && !isEqual(dashboardId, selectedDashboard?.id)) {
  58. setSelectedDashboardState(null);
  59. }
  60. }, [dashboardId, selectedDashboard]);
  61. // If we don't have a selected dashboard, and one isn't going to arrive
  62. // we can redirect to the first dashboard in the list.
  63. useEffect(() => {
  64. if (!dashboardId) {
  65. const firstDashboardId = dashboards?.length
  66. ? dashboards[0]?.id
  67. : 'default-overview';
  68. navigate(
  69. normalizeUrl({
  70. pathname: `/organizations/${organization.slug}/dashboard/${firstDashboardId}/`,
  71. query: {
  72. ...location.query,
  73. },
  74. }),
  75. {replace: true}
  76. );
  77. }
  78. }, [dashboards, dashboardId, organization.slug, location.query, navigate]);
  79. useEffect(() => {
  80. if (dashboardId || selectedDashboard) {
  81. const queryParamFilters = new Set([
  82. 'project',
  83. 'environment',
  84. 'statsPeriod',
  85. 'start',
  86. 'end',
  87. 'utc',
  88. 'release',
  89. ]);
  90. if (
  91. selectedDashboard &&
  92. // Only redirect if there are saved filters and none of the filters
  93. // appear in the query params
  94. hasSavedPageFilters(selectedDashboard) &&
  95. Object.keys(location.query).filter(unsavedQueryParam =>
  96. queryParamFilters.has(unsavedQueryParam)
  97. ).length === 0
  98. ) {
  99. navigate(
  100. {
  101. ...location,
  102. query: {
  103. ...location.query,
  104. project: selectedDashboard.projects,
  105. environment: selectedDashboard.environment,
  106. statsPeriod: selectedDashboard.period,
  107. start: selectedDashboard.start,
  108. end: selectedDashboard.end,
  109. utc: selectedDashboard.utc,
  110. },
  111. },
  112. {replace: true}
  113. );
  114. }
  115. }
  116. }, [dashboardId, location, navigate, selectedDashboard]);
  117. useEffect(() => {
  118. if (!organization.features.includes('dashboards-basic')) {
  119. // Redirect to Dashboards v1
  120. navigate(
  121. normalizeUrl({
  122. pathname: `/organizations/${organization.slug}/dashboards/`,
  123. query: {
  124. ...location.query,
  125. },
  126. }),
  127. {replace: true}
  128. );
  129. }
  130. }, [location.query, navigate, organization]);
  131. if (isDashboardsPending || isSelectedDashboardLoading) {
  132. return (
  133. <Layout.Page withPadding>
  134. <LoadingIndicator />
  135. </Layout.Page>
  136. );
  137. }
  138. if (
  139. (isDashboardsPending || isSelectedDashboardLoading) &&
  140. selectedDashboard &&
  141. hasSavedPageFilters(selectedDashboard) &&
  142. Object.keys(location.query).length === 0
  143. ) {
  144. // Block dashboard from rendering if the dashboard has filters and
  145. // the URL does not contain filters yet. The filters can either match the
  146. // saved filters, or can be different (i.e. sharing an unsaved state)
  147. return (
  148. <Layout.Page withPadding>
  149. <LoadingIndicator />
  150. </Layout.Page>
  151. );
  152. }
  153. if (isDashboardsError || isSelectedDashboardError) {
  154. const notFound =
  155. dashboardsError?.status === 404 || selectedDashboardError?.status === 404;
  156. if (notFound) {
  157. return <NotFound />;
  158. }
  159. return <LoadingError />;
  160. }
  161. const getDashboards = (): DashboardListItem[] => {
  162. return Array.isArray(dashboards) ? dashboards : [];
  163. };
  164. const renderContent = () => {
  165. // Ensure there are always tempIds for grid layout
  166. // This is needed because there are cases where the dashboard
  167. // renders before the onRequestSuccess setState is processed
  168. // and will caused stacked widgets because of missing tempIds
  169. const dashboard = selectedDashboard
  170. ? {
  171. ...selectedDashboard,
  172. widgets: selectedDashboard.widgets.map(assignTempId),
  173. }
  174. : null;
  175. return children({
  176. error: Boolean(dashboardsError || selectedDashboardError),
  177. dashboard,
  178. dashboards: getDashboards(),
  179. onDashboardUpdate: setSelectedDashboardState,
  180. });
  181. };
  182. return (
  183. <SentryDocumentTitle title={t('Dashboards')} orgSlug={organization.slug}>
  184. {renderContent()}
  185. </SentryDocumentTitle>
  186. );
  187. }
  188. export default withRouteAnalytics(OrgDashboards);