createDashboardFromScratchpadModal.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import {useEffect, useState} from 'react';
  2. import type {InjectedRouter} from 'react-router';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import type {Location} from 'history';
  6. import {createDashboard, updateDashboard} from 'sentry/actionCreators/dashboards';
  7. import {addErrorMessage, addSuccessMessage} from 'sentry/actionCreators/indicator';
  8. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  9. import {Button} from 'sentry/components/button';
  10. import ButtonBar from 'sentry/components/buttonBar';
  11. import SelectControl from 'sentry/components/forms/controls/selectControl';
  12. import Input from 'sentry/components/input';
  13. import LoadingError from 'sentry/components/loadingError';
  14. import {t, tct} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {Organization, SelectValue} from 'sentry/types';
  17. import {useApiQuery} from 'sentry/utils/queryClient';
  18. import useApi from 'sentry/utils/useApi';
  19. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  20. import type {DashboardDetails, DashboardListItem} from 'sentry/views/dashboards/types';
  21. import {MAX_WIDGETS} from 'sentry/views/dashboards/types';
  22. import {NEW_DASHBOARD_ID} from 'sentry/views/dashboards/widgetBuilder/utils';
  23. import {OrganizationContext} from 'sentry/views/organizationContext';
  24. export type AddToDashboardModalProps = {
  25. location: Location;
  26. newDashboard: DashboardDetails;
  27. organization: Organization;
  28. router: InjectedRouter;
  29. };
  30. type Props = ModalRenderProps & AddToDashboardModalProps;
  31. const MISSING_NAME_MESSAGE = t('You need to name your dashboard');
  32. function CreateDashboardFromScratchpadModal({
  33. Header,
  34. Body,
  35. Footer,
  36. closeModal,
  37. organization,
  38. router,
  39. newDashboard,
  40. }: Props) {
  41. const api = useApi();
  42. const [dashboardName, setDashboardName] = useState<string>(newDashboard.title);
  43. const [selectedDashboardId, setSelectedDashboardId] =
  44. useState<string>(NEW_DASHBOARD_ID);
  45. const {data: dashboards, isError: isDashboardError} = useApiQuery<DashboardListItem[]>(
  46. [
  47. `/organizations/${organization.slug}/dashboards/`,
  48. {
  49. query: {sort: 'myDashboardsAndRecentlyViewed'},
  50. },
  51. ],
  52. {staleTime: 0}
  53. );
  54. const shouldFetchSelectedDashboard = selectedDashboardId !== NEW_DASHBOARD_ID;
  55. const isMissingName = selectedDashboardId === NEW_DASHBOARD_ID && !dashboardName;
  56. const {data: selectedDashboard, isError: isSelectedDashboardError} =
  57. useApiQuery<DashboardDetails>(
  58. [`/organizations/${organization.slug}/dashboards/${selectedDashboardId}/`],
  59. {
  60. staleTime: 0,
  61. enabled: shouldFetchSelectedDashboard,
  62. }
  63. );
  64. useEffect(() => {
  65. if (isSelectedDashboardError) {
  66. addErrorMessage(t('Unable to load dashboard'));
  67. }
  68. }, [isSelectedDashboardError]);
  69. async function createOrUpdateDashboard() {
  70. if (selectedDashboardId === NEW_DASHBOARD_ID) {
  71. if (!dashboardName) {
  72. addErrorMessage(MISSING_NAME_MESSAGE);
  73. return null;
  74. }
  75. try {
  76. const dashboard = await createDashboard(api, organization.slug, {
  77. ...newDashboard,
  78. title: dashboardName,
  79. });
  80. addSuccessMessage(t('Successfully created dashboard'));
  81. return dashboard;
  82. } catch (err) {
  83. // createDashboard already shows an error message
  84. return null;
  85. }
  86. }
  87. if (!selectedDashboard) {
  88. return null;
  89. }
  90. const updatedDashboard = {
  91. ...selectedDashboard,
  92. widgets: [...selectedDashboard.widgets, ...newDashboard.widgets],
  93. };
  94. const dashboard = await updateDashboard(api, organization.slug, updatedDashboard);
  95. addSuccessMessage(t('Successfully added widgets to dashboard'));
  96. return dashboard;
  97. }
  98. async function handleAddAndStayOnCurrentPage() {
  99. const dashboard = await createOrUpdateDashboard();
  100. if (dashboard) {
  101. closeModal();
  102. }
  103. }
  104. async function handleGoToDashboard() {
  105. const dashboard = await createOrUpdateDashboard();
  106. if (!dashboard) {
  107. return;
  108. }
  109. router.push(
  110. normalizeUrl({
  111. pathname: `/organizations/${organization.slug}/dashboards/${dashboard.id}/`,
  112. })
  113. );
  114. closeModal();
  115. }
  116. return (
  117. <OrganizationContext.Provider value={organization}>
  118. <Header closeButton>
  119. <h4>{t('Add to Dashboard')}</h4>
  120. </Header>
  121. <Body>
  122. {isDashboardError && <LoadingError message={t('Unable to load dashboards')} />}
  123. <Wrapper>
  124. <SelectControl
  125. disabled={dashboards === null}
  126. menuPlacement="auto"
  127. name="dashboard"
  128. placeholder={t('Select Dashboard')}
  129. value={selectedDashboardId}
  130. options={
  131. dashboards && [
  132. {label: t('+ Create New Dashboard'), value: 'new'},
  133. ...dashboards.map(({title, id, widgetDisplay}) => ({
  134. label: title,
  135. value: id,
  136. disabled: widgetDisplay.length >= MAX_WIDGETS,
  137. tooltip:
  138. widgetDisplay.length >= MAX_WIDGETS &&
  139. tct('Max widgets ([maxWidgets]) per dashboard reached.', {
  140. maxWidgets: MAX_WIDGETS,
  141. }),
  142. tooltipOptions: {position: 'right'},
  143. })),
  144. ]
  145. }
  146. onChange={(option: SelectValue<string>) => {
  147. if (option.disabled) {
  148. return;
  149. }
  150. setSelectedDashboardId(option.value);
  151. }}
  152. />
  153. {selectedDashboardId === NEW_DASHBOARD_ID && (
  154. <Input
  155. placeholder={t('Name your dashboard')}
  156. value={dashboardName}
  157. onChange={event => {
  158. setDashboardName(event.target.value);
  159. }}
  160. />
  161. )}
  162. </Wrapper>
  163. </Body>
  164. <Footer>
  165. <StyledButtonBar gap={1.5}>
  166. <Button
  167. disabled={isSelectedDashboardError || isMissingName}
  168. onClick={handleAddAndStayOnCurrentPage}
  169. title={isMissingName ? MISSING_NAME_MESSAGE : undefined}
  170. >
  171. {t('Add + Stay on this Page')}
  172. </Button>
  173. <Button
  174. disabled={isSelectedDashboardError || isMissingName}
  175. priority="primary"
  176. onClick={handleGoToDashboard}
  177. title={isMissingName ? MISSING_NAME_MESSAGE : undefined}
  178. >
  179. {t('Open in Dashboards')}
  180. </Button>
  181. </StyledButtonBar>
  182. </Footer>
  183. </OrganizationContext.Provider>
  184. );
  185. }
  186. export default CreateDashboardFromScratchpadModal;
  187. const Wrapper = styled('div')`
  188. display: grid;
  189. grid-template-columns: 1fr;
  190. gap: ${space(2)};
  191. margin-bottom: ${space(2)};
  192. `;
  193. const StyledButtonBar = styled(ButtonBar)`
  194. @media (max-width: ${props => props.theme.breakpoints.small}) {
  195. grid-template-rows: repeat(2, 1fr);
  196. gap: ${space(1.5)};
  197. width: 100%;
  198. > button {
  199. width: 100%;
  200. }
  201. }
  202. `;
  203. export const modalCss = css`
  204. max-width: 700px;
  205. margin: 70px auto;
  206. `;