dashboardImportModal.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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} = 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. return (
  61. <Fragment>
  62. <Header>
  63. <h4>{t('Import dashboard')}</h4>
  64. </Header>
  65. <Body>
  66. <ContentWrapper>
  67. {formState.step === 'initial' && (
  68. <JSONTextArea
  69. rows={4}
  70. maxRows={20}
  71. name="dashboard"
  72. placeholder={t('Paste dashboard JSON ')}
  73. value={formState.dashboard}
  74. onChange={e => {
  75. const isValid = isValidJson(e.target.value);
  76. setFormState(curr => ({...curr, dashboard: e.target.value, isValid}));
  77. }}
  78. />
  79. )}
  80. {formState.step === 'importing' && <LoadingIndicator />}
  81. {formState.step === 'add-widgets' && (
  82. <Fragment>
  83. <div>
  84. {t(
  85. 'Processed %s widgets from the dashboard',
  86. formState.importResult?.report.length
  87. )}
  88. </div>
  89. <PanelTable headers={['Title', 'Outcome', 'Errors', 'Notes']}>
  90. {formState.importResult?.report.map(widget => {
  91. return (
  92. <Fragment key={widget.id}>
  93. <div>{widget.title}</div>
  94. <div>
  95. <Tag type={widget.outcome}>{widget.outcome}</Tag>
  96. </div>
  97. <div>{widget.errors.join(', ')}</div>
  98. <div>{widget.notes.join(', ')}</div>
  99. </Fragment>
  100. );
  101. })}
  102. </PanelTable>
  103. <div>
  104. {t(
  105. 'Found %s widgets that can be imported',
  106. formState.importResult?.widgets.length
  107. )}
  108. </div>
  109. </Fragment>
  110. )}
  111. </ContentWrapper>
  112. </Body>
  113. <Footer>
  114. <Tooltip
  115. disabled={formState.isValid}
  116. title={t('Please input valid dashboard JSON')}
  117. >
  118. <Button
  119. priority="primary"
  120. disabled={!formState.isValid}
  121. onClick={handleImportDashboard}
  122. >
  123. {formState.step === 'initial' ? t('Import') : t('Add Widgets')}
  124. </Button>
  125. </Tooltip>
  126. </Footer>
  127. </Fragment>
  128. );
  129. }
  130. const ContentWrapper = styled('div')`
  131. display: grid;
  132. grid-template-columns: 1fr;
  133. gap: ${space(2)};
  134. max-height: 70vh;
  135. overflow-y: scroll;
  136. `;
  137. const JSONTextArea = styled(TextArea)`
  138. min-height: 200px;
  139. `;
  140. const modalCss = css`
  141. width: 80%;
  142. `;
  143. const isValidJson = (str: string) => {
  144. try {
  145. JSON.parse(str);
  146. } catch (e) {
  147. return false;
  148. }
  149. return true;
  150. };