dashboardImportModal.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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 Tag from 'sentry/components/badge/tag';
  8. import {Button} from 'sentry/components/button';
  9. import TextArea from 'sentry/components/forms/controls/textarea';
  10. import LoadingIndicator from 'sentry/components/loadingIndicator';
  11. import {PanelTable} from 'sentry/components/panels/panelTable';
  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/organization';
  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 normalizeUrl from 'sentry/utils/url/normalizeUrl';
  20. import useApi from 'sentry/utils/useApi';
  21. import {useNavigate} from 'sentry/utils/useNavigate';
  22. import useOrganization from 'sentry/utils/useOrganization';
  23. import usePageFilters from 'sentry/utils/usePageFilters';
  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 navigate = useNavigate();
  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(dashboardJson, metricsMeta, organization);
  63. setFormState(curr => ({
  64. ...curr,
  65. importResult,
  66. step: 'add-widgets',
  67. }));
  68. }
  69. }, [formState.isValid, formState.dashboard, metricsMeta, organization]);
  70. const handleCreateDashboard = useCallback(async () => {
  71. const title = formState.importResult?.title ?? 'Metrics Dashboard';
  72. const importedWidgets = (formState.importResult?.widgets ?? [])
  73. // Only import the first 30 widgets because of dashboard widget limit
  74. .slice(0, 30);
  75. const newDashboard = {
  76. id: 'temp-id-imported-dashboard',
  77. title: `${title} (Imported)`,
  78. description: formState.importResult?.description ?? '',
  79. filters: {},
  80. dateCreated: '',
  81. widgets: assignDefaultLayout(importedWidgets, getInitialColumnDepths()),
  82. ...selection,
  83. };
  84. const dashboard = await createDashboard(api, organization.slug, newDashboard);
  85. navigate(
  86. normalizeUrl({
  87. pathname: `/organizations/${organization.slug}/dashboards/${dashboard.id}/`,
  88. })
  89. );
  90. }, [formState.importResult, selection, organization, api, navigate]);
  91. return (
  92. <Fragment>
  93. <Header>
  94. <h4>{t('Import dashboard')}</h4>
  95. </Header>
  96. <Body>
  97. <ContentWrapper>
  98. {formState.step === 'initial' && (
  99. <JSONTextArea
  100. rows={4}
  101. maxRows={20}
  102. name="dashboard"
  103. placeholder={t('Paste dashboard JSON ')}
  104. value={formState.dashboard}
  105. onChange={e => {
  106. const isValid = isValidJson(e.target.value);
  107. setFormState(curr => ({...curr, dashboard: e.target.value, isValid}));
  108. }}
  109. />
  110. )}
  111. {formState.step === 'importing' && <LoadingIndicator />}
  112. {formState.step === 'add-widgets' && (
  113. <Fragment>
  114. <div>
  115. {t(
  116. 'Processed %s widgets from the dashboard',
  117. formState.importResult?.report.length
  118. )}
  119. </div>
  120. <PanelTable headers={['Title', 'Outcome', 'Errors']}>
  121. {formState.importResult?.report.map(widget => {
  122. return (
  123. <Fragment key={widget.id}>
  124. <div>{widget.title}</div>
  125. <div>
  126. <Tag type={widget.outcome}>{widget.outcome}</Tag>
  127. </div>
  128. <div>{widget.errors.join(', ')}</div>
  129. </Fragment>
  130. );
  131. })}
  132. </PanelTable>
  133. <div>
  134. {t(
  135. 'Found %s widgets that can be imported',
  136. formState.importResult?.widgets.length
  137. )}
  138. </div>
  139. </Fragment>
  140. )}
  141. </ContentWrapper>
  142. </Body>
  143. <Footer>
  144. <Tooltip
  145. disabled={formState.isValid}
  146. title={t('Please input valid dashboard JSON')}
  147. >
  148. <Button
  149. priority="primary"
  150. disabled={!formState.isValid}
  151. onClick={
  152. formState.step === 'initial' ? handleImportDashboard : handleCreateDashboard
  153. }
  154. >
  155. {formState.step === 'initial' ? t('Import') : t('Create Dashboard')}
  156. </Button>
  157. </Tooltip>
  158. </Footer>
  159. </Fragment>
  160. );
  161. }
  162. const ContentWrapper = styled('div')`
  163. display: grid;
  164. grid-template-columns: 1fr;
  165. gap: ${space(2)};
  166. max-height: 70vh;
  167. overflow-y: scroll;
  168. `;
  169. const JSONTextArea = styled(TextArea)`
  170. min-height: 200px;
  171. `;
  172. const modalCss = css`
  173. width: 80%;
  174. `;
  175. const isValidJson = (str: string) => {
  176. try {
  177. JSON.parse(str);
  178. } catch (e) {
  179. return false;
  180. }
  181. return true;
  182. };