DashboardImportPage.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. import { css } from '@emotion/css';
  2. import React, { FormEvent, PureComponent } from 'react';
  3. import { connect, ConnectedProps } from 'react-redux';
  4. import { AppEvents, GrafanaTheme2, LoadingState } from '@grafana/data';
  5. import { selectors } from '@grafana/e2e-selectors';
  6. import { reportInteraction } from '@grafana/runtime';
  7. import {
  8. Button,
  9. Field,
  10. FileUpload,
  11. Form,
  12. HorizontalGroup,
  13. Input,
  14. Spinner,
  15. stylesFactory,
  16. TextArea,
  17. Themeable2,
  18. VerticalGroup,
  19. withTheme2,
  20. } from '@grafana/ui';
  21. import appEvents from 'app/core/app_events';
  22. import Page from 'app/core/components/Page/Page';
  23. import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
  24. import { getNavModel } from 'app/core/selectors/navModel';
  25. import { StoreState } from 'app/types';
  26. import { cleanUpAction } from '../../core/actions/cleanUp';
  27. import { ImportDashboardOverview } from './components/ImportDashboardOverview';
  28. import { fetchGcomDashboard, importDashboardJson } from './state/actions';
  29. import { validateDashboardJson, validateGcomDashboard } from './utils/validation';
  30. type DashboardImportPageRouteSearchParams = {
  31. gcomDashboardId?: string;
  32. };
  33. type OwnProps = Themeable2 & GrafanaRouteComponentProps<{}, DashboardImportPageRouteSearchParams>;
  34. const IMPORT_STARTED_EVENT_NAME = 'dashboard_import_loaded';
  35. const mapStateToProps = (state: StoreState) => ({
  36. navModel: getNavModel(state.navIndex, 'import', undefined, true),
  37. loadingState: state.importDashboard.state,
  38. });
  39. const mapDispatchToProps = {
  40. fetchGcomDashboard,
  41. importDashboardJson,
  42. cleanUpAction,
  43. };
  44. const connector = connect(mapStateToProps, mapDispatchToProps);
  45. type Props = OwnProps & ConnectedProps<typeof connector>;
  46. class UnthemedDashboardImport extends PureComponent<Props> {
  47. constructor(props: Props) {
  48. super(props);
  49. const { gcomDashboardId } = this.props.queryParams;
  50. if (gcomDashboardId) {
  51. this.getGcomDashboard({ gcomDashboard: gcomDashboardId });
  52. return;
  53. }
  54. }
  55. componentWillUnmount() {
  56. this.props.cleanUpAction({ stateSelector: (state: StoreState) => state.importDashboard });
  57. }
  58. onFileUpload = (event: FormEvent<HTMLInputElement>) => {
  59. reportInteraction(IMPORT_STARTED_EVENT_NAME, {
  60. import_source: 'json_uploaded',
  61. });
  62. const { importDashboardJson } = this.props;
  63. const file = event.currentTarget.files && event.currentTarget.files.length > 0 && event.currentTarget.files[0];
  64. if (file) {
  65. const reader = new FileReader();
  66. const readerOnLoad = () => {
  67. return (e: any) => {
  68. let dashboard: any;
  69. try {
  70. dashboard = JSON.parse(e.target.result);
  71. } catch (error) {
  72. appEvents.emit(AppEvents.alertError, [
  73. 'Import failed',
  74. 'JSON -> JS Serialization failed: ' + error.message,
  75. ]);
  76. return;
  77. }
  78. importDashboardJson(dashboard);
  79. };
  80. };
  81. reader.onload = readerOnLoad();
  82. reader.readAsText(file);
  83. }
  84. };
  85. getDashboardFromJson = (formData: { dashboardJson: string }) => {
  86. reportInteraction(IMPORT_STARTED_EVENT_NAME, {
  87. import_source: 'json_pasted',
  88. });
  89. this.props.importDashboardJson(JSON.parse(formData.dashboardJson));
  90. };
  91. getGcomDashboard = (formData: { gcomDashboard: string }) => {
  92. reportInteraction(IMPORT_STARTED_EVENT_NAME, {
  93. import_source: 'gcom',
  94. });
  95. let dashboardId;
  96. const match = /(^\d+$)|dashboards\/(\d+)/.exec(formData.gcomDashboard);
  97. if (match && match[1]) {
  98. dashboardId = match[1];
  99. } else if (match && match[2]) {
  100. dashboardId = match[2];
  101. }
  102. if (dashboardId) {
  103. this.props.fetchGcomDashboard(dashboardId);
  104. }
  105. };
  106. renderImportForm() {
  107. const styles = importStyles(this.props.theme);
  108. return (
  109. <>
  110. <div className={styles.option}>
  111. <FileUpload accept="application/json" onFileUpload={this.onFileUpload}>
  112. Upload JSON file
  113. </FileUpload>
  114. </div>
  115. <div className={styles.option}>
  116. <Form onSubmit={this.getGcomDashboard} defaultValues={{ gcomDashboard: '' }}>
  117. {({ register, errors }) => (
  118. <Field
  119. label="Import via grafana.com"
  120. invalid={!!errors.gcomDashboard}
  121. error={errors.gcomDashboard && errors.gcomDashboard.message}
  122. >
  123. <Input
  124. id="url-input"
  125. placeholder="Grafana.com dashboard URL or ID"
  126. type="text"
  127. {...register('gcomDashboard', {
  128. required: 'A Grafana dashboard URL or ID is required',
  129. validate: validateGcomDashboard,
  130. })}
  131. addonAfter={<Button type="submit">Load</Button>}
  132. />
  133. </Field>
  134. )}
  135. </Form>
  136. </div>
  137. <div className={styles.option}>
  138. <Form onSubmit={this.getDashboardFromJson} defaultValues={{ dashboardJson: '' }}>
  139. {({ register, errors }) => (
  140. <>
  141. <Field
  142. label="Import via panel json"
  143. invalid={!!errors.dashboardJson}
  144. error={errors.dashboardJson && errors.dashboardJson.message}
  145. >
  146. <TextArea
  147. {...register('dashboardJson', {
  148. required: 'Need a dashboard JSON model',
  149. validate: validateDashboardJson,
  150. })}
  151. data-testid={selectors.components.DashboardImportPage.textarea}
  152. id="dashboard-json-textarea"
  153. rows={10}
  154. />
  155. </Field>
  156. <Button type="submit" data-testid={selectors.components.DashboardImportPage.submit}>
  157. Load
  158. </Button>
  159. </>
  160. )}
  161. </Form>
  162. </div>
  163. </>
  164. );
  165. }
  166. render() {
  167. const { loadingState, navModel } = this.props;
  168. return (
  169. <Page navModel={navModel}>
  170. <Page.Contents>
  171. {loadingState === LoadingState.Loading && (
  172. <VerticalGroup justify="center">
  173. <HorizontalGroup justify="center">
  174. <Spinner size={32} />
  175. </HorizontalGroup>
  176. </VerticalGroup>
  177. )}
  178. {[LoadingState.Error, LoadingState.NotStarted].includes(loadingState) && this.renderImportForm()}
  179. {loadingState === LoadingState.Done && <ImportDashboardOverview />}
  180. </Page.Contents>
  181. </Page>
  182. );
  183. }
  184. }
  185. const DashboardImportUnConnected = withTheme2(UnthemedDashboardImport);
  186. const DashboardImport = connector(DashboardImportUnConnected);
  187. DashboardImport.displayName = 'DashboardImport';
  188. export default DashboardImport;
  189. const importStyles = stylesFactory((theme: GrafanaTheme2) => {
  190. return {
  191. option: css`
  192. margin-bottom: ${theme.spacing(4)};
  193. `,
  194. };
  195. });