addToDashboardModal.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import {useEffect, useState} from 'react';
  2. import {InjectedRouter} from 'react-router';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {Location, Query} from 'history';
  6. import {
  7. fetchDashboard,
  8. fetchDashboards,
  9. updateDashboard,
  10. } from 'sentry/actionCreators/dashboards';
  11. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  12. import {ModalRenderProps} from 'sentry/actionCreators/modal';
  13. import {Button} from 'sentry/components/button';
  14. import ButtonBar from 'sentry/components/buttonBar';
  15. import SelectControl from 'sentry/components/forms/controls/selectControl';
  16. import {t, tct} from 'sentry/locale';
  17. import {space} from 'sentry/styles/space';
  18. import {DateString, Organization, PageFilters, SelectValue} from 'sentry/types';
  19. import getXhrErrorResponseHandler from 'sentry/utils/handleXhrErrorResponse';
  20. import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
  21. import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  22. import useApi from 'sentry/utils/useApi';
  23. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  24. import {
  25. DashboardDetails,
  26. DashboardListItem,
  27. DisplayType,
  28. MAX_WIDGETS,
  29. Widget,
  30. } from 'sentry/views/dashboards/types';
  31. import {
  32. eventViewFromWidget,
  33. getDashboardFiltersFromURL,
  34. getSavedFiltersAsPageFilters,
  35. getSavedPageFilters,
  36. } from 'sentry/views/dashboards/utils';
  37. import {NEW_DASHBOARD_ID} from 'sentry/views/dashboards/widgetBuilder/utils';
  38. import WidgetCard from 'sentry/views/dashboards/widgetCard';
  39. import {OrganizationContext} from 'sentry/views/organizationContext';
  40. import {MetricsDataSwitcher} from 'sentry/views/performance/landing/metricsDataSwitcher';
  41. type WidgetAsQueryParams = Query<{
  42. defaultTableColumns: string[];
  43. defaultTitle: string;
  44. defaultWidgetQuery: string;
  45. displayType: DisplayType;
  46. environment: string[];
  47. project: number[];
  48. source: string;
  49. end?: DateString;
  50. start?: DateString;
  51. statsPeriod?: string | null;
  52. }>;
  53. export type AddToDashboardModalProps = {
  54. location: Location;
  55. organization: Organization;
  56. router: InjectedRouter;
  57. selection: PageFilters;
  58. widget: Widget;
  59. widgetAsQueryParams: WidgetAsQueryParams;
  60. };
  61. type Props = ModalRenderProps & AddToDashboardModalProps;
  62. const SELECT_DASHBOARD_MESSAGE = t('Select a dashboard');
  63. function AddToDashboardModal({
  64. Header,
  65. Body,
  66. Footer,
  67. closeModal,
  68. location,
  69. organization,
  70. router,
  71. selection,
  72. widget,
  73. widgetAsQueryParams,
  74. }: Props) {
  75. const api = useApi();
  76. const [dashboards, setDashboards] = useState<DashboardListItem[] | null>(null);
  77. const [selectedDashboard, setSelectedDashboard] = useState<DashboardDetails | null>(
  78. null
  79. );
  80. const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(null);
  81. useEffect(() => {
  82. // Track mounted state so we dont call setState on unmounted components
  83. let unmounted = false;
  84. fetchDashboards(api, organization.slug).then(response => {
  85. // If component has unmounted, dont set state
  86. if (unmounted) {
  87. return;
  88. }
  89. setDashboards(response);
  90. });
  91. return () => {
  92. unmounted = true;
  93. };
  94. }, [api, organization.slug]);
  95. useEffect(() => {
  96. // Track mounted state so we dont call setState on unmounted components
  97. let unmounted = false;
  98. if (selectedDashboardId === NEW_DASHBOARD_ID || selectedDashboardId === null) {
  99. setSelectedDashboard(null);
  100. } else {
  101. fetchDashboard(api, organization.slug, selectedDashboardId).then(response => {
  102. // If component has unmounted, dont set state
  103. if (unmounted) {
  104. return;
  105. }
  106. setSelectedDashboard(response);
  107. });
  108. }
  109. return () => {
  110. unmounted = true;
  111. };
  112. }, [api, organization.slug, selectedDashboardId]);
  113. function handleGoToBuilder() {
  114. const pathname =
  115. selectedDashboardId === NEW_DASHBOARD_ID
  116. ? `/organizations/${organization.slug}/dashboards/new/widget/new/`
  117. : `/organizations/${organization.slug}/dashboard/${selectedDashboardId}/widget/new/`;
  118. router.push(
  119. normalizeUrl({
  120. pathname,
  121. query: {
  122. ...widgetAsQueryParams,
  123. ...(selectedDashboard ? getSavedPageFilters(selectedDashboard) : {}),
  124. },
  125. })
  126. );
  127. closeModal();
  128. }
  129. async function handleAddAndStayInDiscover() {
  130. if (selectedDashboard === null) {
  131. return;
  132. }
  133. let orderby = widget.queries[0].orderby;
  134. if (!(DisplayType.AREA && widget.queries[0].columns.length)) {
  135. orderby = ''; // Clear orderby if its not a top n visualization.
  136. }
  137. const query = widget.queries[0];
  138. const newWidget = {
  139. ...widget,
  140. title: widget.title === '' ? t('All Events') : widget.title,
  141. queries: [{...query, orderby}],
  142. };
  143. try {
  144. const newDashboard = {
  145. ...selectedDashboard,
  146. widgets: [...selectedDashboard.widgets, newWidget],
  147. };
  148. await updateDashboard(api, organization.slug, newDashboard);
  149. closeModal();
  150. addSuccessMessage(t('Successfully added widget to dashboard'));
  151. } catch (e) {
  152. const errorMessage = t('Unable to add widget to dashboard');
  153. getXhrErrorResponseHandler(errorMessage)(e);
  154. addErrorMessage(errorMessage);
  155. }
  156. }
  157. const canSubmit = selectedDashboardId !== null;
  158. return (
  159. <OrganizationContext.Provider value={organization}>
  160. <Header closeButton>
  161. <h4>{t('Add to Dashboard')}</h4>
  162. </Header>
  163. <Body>
  164. <Wrapper>
  165. <SelectControl
  166. disabled={dashboards === null}
  167. menuPlacement="auto"
  168. name="dashboard"
  169. placeholder={t('Select Dashboard')}
  170. value={selectedDashboardId}
  171. options={
  172. dashboards && [
  173. {label: t('+ Create New Dashboard'), value: 'new'},
  174. ...dashboards.map(({title, id, widgetDisplay}) => ({
  175. label: title,
  176. value: id,
  177. disabled: widgetDisplay.length >= MAX_WIDGETS,
  178. tooltip:
  179. widgetDisplay.length >= MAX_WIDGETS &&
  180. tct('Max widgets ([maxWidgets]) per dashboard reached.', {
  181. maxWidgets: MAX_WIDGETS,
  182. }),
  183. tooltipOptions: {position: 'right'},
  184. })),
  185. ]
  186. }
  187. onChange={(option: SelectValue<string>) => {
  188. if (option.disabled) {
  189. return;
  190. }
  191. setSelectedDashboardId(option.value);
  192. }}
  193. />
  194. </Wrapper>
  195. <Wrapper>
  196. {t(
  197. 'Any conflicting filters from this query will be overridden by Dashboard filters. This is a preview of how the widget will appear in your dashboard.'
  198. )}
  199. </Wrapper>
  200. <MetricsCardinalityProvider organization={organization} location={location}>
  201. <MetricsDataSwitcher
  202. organization={organization}
  203. eventView={eventViewFromWidget(
  204. widget.title,
  205. widget.queries[0],
  206. selection,
  207. widget.displayType
  208. )}
  209. location={location}
  210. hideLoadingIndicator
  211. >
  212. {metricsDataSide => (
  213. <MEPSettingProvider
  214. location={location}
  215. forceTransactions={metricsDataSide.forceTransactionsOnly}
  216. >
  217. <WidgetCard
  218. organization={organization}
  219. isEditing={false}
  220. widgetLimitReached={false}
  221. selection={
  222. selectedDashboard
  223. ? getSavedFiltersAsPageFilters(selectedDashboard)
  224. : selection
  225. }
  226. dashboardFilters={
  227. getDashboardFiltersFromURL(location) ?? selectedDashboard?.filters
  228. }
  229. widget={widget}
  230. showStoredAlert
  231. />
  232. </MEPSettingProvider>
  233. )}
  234. </MetricsDataSwitcher>
  235. </MetricsCardinalityProvider>
  236. </Body>
  237. <Footer>
  238. <StyledButtonBar gap={1.5}>
  239. <Button
  240. onClick={handleAddAndStayInDiscover}
  241. disabled={!canSubmit || selectedDashboardId === NEW_DASHBOARD_ID}
  242. title={canSubmit ? undefined : SELECT_DASHBOARD_MESSAGE}
  243. >
  244. {t('Add + Stay in Discover')}
  245. </Button>
  246. <Button
  247. priority="primary"
  248. onClick={handleGoToBuilder}
  249. disabled={!canSubmit}
  250. title={canSubmit ? undefined : SELECT_DASHBOARD_MESSAGE}
  251. >
  252. {t('Open in Widget Builder')}
  253. </Button>
  254. </StyledButtonBar>
  255. </Footer>
  256. </OrganizationContext.Provider>
  257. );
  258. }
  259. export default AddToDashboardModal;
  260. const Wrapper = styled('div')`
  261. margin-bottom: ${space(2)};
  262. `;
  263. const StyledButtonBar = styled(ButtonBar)`
  264. @media (max-width: ${props => props.theme.breakpoints.small}) {
  265. grid-template-rows: repeat(2, 1fr);
  266. gap: ${space(1.5)};
  267. width: 100%;
  268. > button {
  269. width: 100%;
  270. }
  271. }
  272. `;
  273. export const modalCss = css`
  274. max-width: 700px;
  275. margin: 70px auto;
  276. `;