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