dashboardImportModal.tsx 6.5 KB

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