addToDashboardModal.tsx 8.3 KB

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