dashboardImportModal.tsx 5.1 KB

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