dashboardImportModal.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  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 type {ParseResult} from 'sentry/utils/metrics/dashboardImport';
  17. import {parseDashboard} from 'sentry/utils/metrics/dashboardImport';
  18. import {useMetricsMeta} from 'sentry/utils/metrics/useMetricsMeta';
  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 {OrganizationContext} from 'sentry/views/organizationContext';
  29. export function openDashboardImport(organization: Organization) {
  30. return openModal(
  31. deps => (
  32. <OrganizationContext.Provider value={organization}>
  33. <DashboardImportModal {...deps} />
  34. </OrganizationContext.Provider>
  35. ),
  36. {modalCss}
  37. );
  38. }
  39. type FormState = {
  40. dashboard: string;
  41. importResult: ParseResult | null;
  42. isValid: boolean;
  43. step: 'initial' | 'importing' | 'add-widgets';
  44. };
  45. function DashboardImportModal({Header, Body, Footer}: ModalRenderProps) {
  46. const api = useApi();
  47. const router = useRouter();
  48. const {selection} = usePageFilters();
  49. // we want to get all custom metrics for organization
  50. const {data: metricsMeta} = useMetricsMeta({projects: [-1]}, ['custom']);
  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(
  63. dashboardJson,
  64. metricsMeta,
  65. organization.slug
  66. );
  67. setFormState(curr => ({
  68. ...curr,
  69. importResult,
  70. step: 'add-widgets',
  71. }));
  72. }
  73. }, [formState.isValid, formState.dashboard, metricsMeta, organization.slug]);
  74. const handleCreateDashboard = useCallback(async () => {
  75. const title = formState.importResult?.title ?? 'Metrics Dashboard';
  76. const importedWidgets = (formState.importResult?.widgets ?? [])
  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']}>
  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. </Fragment>
  134. );
  135. })}
  136. </PanelTable>
  137. <div>
  138. {t(
  139. 'Found %s widgets that can be imported',
  140. formState.importResult?.widgets.length
  141. )}
  142. </div>
  143. </Fragment>
  144. )}
  145. </ContentWrapper>
  146. </Body>
  147. <Footer>
  148. <Tooltip
  149. disabled={formState.isValid}
  150. title={t('Please input valid dashboard JSON')}
  151. >
  152. <Button
  153. priority="primary"
  154. disabled={!formState.isValid}
  155. onClick={
  156. formState.step === 'initial' ? handleImportDashboard : handleCreateDashboard
  157. }
  158. >
  159. {formState.step === 'initial' ? t('Import') : t('Create Dashboard')}
  160. </Button>
  161. </Tooltip>
  162. </Footer>
  163. </Fragment>
  164. );
  165. }
  166. const ContentWrapper = styled('div')`
  167. display: grid;
  168. grid-template-columns: 1fr;
  169. gap: ${space(2)};
  170. max-height: 70vh;
  171. overflow-y: scroll;
  172. `;
  173. const JSONTextArea = styled(TextArea)`
  174. min-height: 200px;
  175. `;
  176. const modalCss = css`
  177. width: 80%;
  178. `;
  179. const isValidJson = (str: string) => {
  180. try {
  181. JSON.parse(str);
  182. } catch (e) {
  183. return false;
  184. }
  185. return true;
  186. };