dashboardImportModal.tsx 6.5 KB

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