dashboardImportModal.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import {Fragment, useCallback, useState} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {createDashboard} from 'sentry/actionCreators/dashboards';
  5. import {ModalRenderProps, openModal} from 'sentry/actionCreators/modal';
  6. import {Button} from 'sentry/components/button';
  7. import TextArea from 'sentry/components/forms/controls/textarea';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import PanelTable from 'sentry/components/panels/panelTable';
  10. import Tag from 'sentry/components/tag';
  11. import {Tooltip} from 'sentry/components/tooltip';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import {Organization} from 'sentry/types';
  15. import {convertToDashboardWidget} from 'sentry/utils/metrics/dashboard';
  16. import {parseDashboard, ParseResult} from 'sentry/utils/metrics/dashboardImport';
  17. import useApi from 'sentry/utils/useApi';
  18. import useOrganization from 'sentry/utils/useOrganization';
  19. import usePageFilters from 'sentry/utils/usePageFilters';
  20. import useRouter from 'sentry/utils/useRouter';
  21. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  22. import {
  23. assignDefaultLayout,
  24. getInitialColumnDepths,
  25. } from 'sentry/views/dashboards/layoutUtils';
  26. import {DDMContextProvider, useDDMContext} from 'sentry/views/ddm/context';
  27. import {OrganizationContext} from 'sentry/views/organizationContext';
  28. export function openDashboardImport(organization: Organization) {
  29. return openModal(
  30. deps => (
  31. <OrganizationContext.Provider value={organization}>
  32. <DDMContextProvider>
  33. <DashboardImportModal {...deps} />
  34. </DDMContextProvider>
  35. </OrganizationContext.Provider>
  36. ),
  37. {modalCss}
  38. );
  39. }
  40. type FormState = {
  41. dashboard: string;
  42. importResult: ParseResult | null;
  43. isValid: boolean;
  44. step: 'initial' | 'importing' | 'add-widgets';
  45. };
  46. function DashboardImportModal({Header, Body, Footer}: ModalRenderProps) {
  47. const api = useApi();
  48. const router = useRouter();
  49. const {metricsMeta} = useDDMContext();
  50. const {selection} = usePageFilters();
  51. const organization = useOrganization();
  52. const [formState, setFormState] = useState<FormState>({
  53. step: 'initial',
  54. dashboard: '',
  55. importResult: null,
  56. isValid: false,
  57. });
  58. const handleImportDashboard = useCallback(async () => {
  59. if (formState.isValid) {
  60. setFormState(curr => ({...curr, step: 'importing'}));
  61. const dashboardJson = JSON.parse(formState.dashboard);
  62. const importResult = await parseDashboard(dashboardJson, metricsMeta);
  63. setFormState(curr => ({
  64. ...curr,
  65. importResult,
  66. step: 'add-widgets',
  67. }));
  68. }
  69. }, [formState.isValid, formState.dashboard, metricsMeta]);
  70. const handleCreateDashboard = useCallback(async () => {
  71. const title = formState.importResult?.title ?? 'Metrics Dashboard';
  72. const importedWidgets = (formState.importResult?.widgets ?? [])
  73. .filter(widget => !!widget.mri)
  74. .map(widget =>
  75. convertToDashboardWidget({...widget, ...selection}, widget.displayType)
  76. )
  77. // Only import the first 30 widgets because of dashboard widget limit
  78. .slice(0, 30);
  79. const newDashboard = {
  80. id: 'temp-id-imported-dashboard',
  81. title: `${title} (Imported)`,
  82. description: formState.importResult?.description ?? '',
  83. filters: {},
  84. dateCreated: '',
  85. widgets: assignDefaultLayout(importedWidgets, getInitialColumnDepths()),
  86. ...selection,
  87. };
  88. const dashboard = await createDashboard(api, organization.slug, newDashboard);
  89. router.push(
  90. normalizeUrl({
  91. pathname: `/organizations/${organization.slug}/dashboards/${dashboard.id}/`,
  92. })
  93. );
  94. }, [formState.importResult, selection, organization, api, router]);
  95. return (
  96. <Fragment>
  97. <Header>
  98. <h4>{t('Import dashboard')}</h4>
  99. </Header>
  100. <Body>
  101. <ContentWrapper>
  102. {formState.step === 'initial' && (
  103. <JSONTextArea
  104. rows={4}
  105. maxRows={20}
  106. name="dashboard"
  107. placeholder={t('Paste dashboard JSON ')}
  108. value={formState.dashboard}
  109. onChange={e => {
  110. const isValid = isValidJson(e.target.value);
  111. setFormState(curr => ({...curr, dashboard: e.target.value, isValid}));
  112. }}
  113. />
  114. )}
  115. {formState.step === 'importing' && <LoadingIndicator />}
  116. {formState.step === 'add-widgets' && (
  117. <Fragment>
  118. <div>
  119. {t(
  120. 'Processed %s widgets from the dashboard',
  121. formState.importResult?.report.length
  122. )}
  123. </div>
  124. <PanelTable headers={['Title', 'Outcome', 'Errors', 'Notes']}>
  125. {formState.importResult?.report.map(widget => {
  126. return (
  127. <Fragment key={widget.id}>
  128. <div>{widget.title}</div>
  129. <div>
  130. <Tag type={widget.outcome}>{widget.outcome}</Tag>
  131. </div>
  132. <div>{widget.errors.join(', ')}</div>
  133. <div>{widget.notes.join(', ')}</div>
  134. </Fragment>
  135. );
  136. })}
  137. </PanelTable>
  138. <div>
  139. {t(
  140. 'Found %s widgets that can be imported',
  141. formState.importResult?.widgets.length
  142. )}
  143. </div>
  144. </Fragment>
  145. )}
  146. </ContentWrapper>
  147. </Body>
  148. <Footer>
  149. <Tooltip
  150. disabled={formState.isValid}
  151. title={t('Please input valid dashboard JSON')}
  152. >
  153. <Button
  154. priority="primary"
  155. disabled={!formState.isValid}
  156. onClick={
  157. formState.step === 'initial' ? handleImportDashboard : handleCreateDashboard
  158. }
  159. >
  160. {formState.step === 'initial' ? t('Import') : t('Create Dashboard')}
  161. </Button>
  162. </Tooltip>
  163. </Footer>
  164. </Fragment>
  165. );
  166. }
  167. const ContentWrapper = styled('div')`
  168. display: grid;
  169. grid-template-columns: 1fr;
  170. gap: ${space(2)};
  171. max-height: 70vh;
  172. overflow-y: scroll;
  173. `;
  174. const JSONTextArea = styled(TextArea)`
  175. min-height: 200px;
  176. `;
  177. const modalCss = css`
  178. width: 80%;
  179. `;
  180. const isValidJson = (str: string) => {
  181. try {
  182. JSON.parse(str);
  183. } catch (e) {
  184. return false;
  185. }
  186. return true;
  187. };