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