diff --git a/packages/grafana-ui/src/components/Forms/Form.tsx b/packages/grafana-ui/src/components/Forms/Form.tsx index f6690b4ced0..50b17eef43e 100644 --- a/packages/grafana-ui/src/components/Forms/Form.tsx +++ b/packages/grafana-ui/src/components/Forms/Form.tsx @@ -1,24 +1,34 @@ import React, { useEffect } from 'react'; -import { useForm, Mode, OnSubmit, DeepPartial, FormContextValues } from 'react-hook-form'; - -type FormAPI = Pick, 'register' | 'errors' | 'control' | 'formState'>; +import { useForm, Mode, OnSubmit, DeepPartial } from 'react-hook-form'; +import { FormAPI } from '../../types'; interface FormProps { validateOn?: Mode; + validateOnMount?: boolean; + validateFieldsOnMount?: string[]; defaultValues?: DeepPartial; onSubmit: OnSubmit; children: (api: FormAPI) => React.ReactNode; } -export function Form({ defaultValues, onSubmit, children, validateOn = 'onSubmit' }: FormProps) { - const { handleSubmit, register, errors, control, reset, getValues, formState } = useForm({ +export function Form({ + defaultValues, + onSubmit, + validateOnMount = false, + validateFieldsOnMount, + children, + validateOn = 'onSubmit', +}: FormProps) { + const { handleSubmit, register, errors, control, triggerValidation, getValues, formState } = useForm({ mode: validateOn, defaultValues, }); useEffect(() => { - reset({ ...getValues(), ...defaultValues }); - }, [defaultValues]); + if (validateOnMount) { + triggerValidation(validateFieldsOnMount); + } + }, []); - return
{children({ register, errors, control, formState })}
; + return
{children({ register, errors, control, getValues, formState })}
; } diff --git a/packages/grafana-ui/src/components/Forms/Legend.tsx b/packages/grafana-ui/src/components/Forms/Legend.tsx index e42a768a61b..89077a523a7 100644 --- a/packages/grafana-ui/src/components/Forms/Legend.tsx +++ b/packages/grafana-ui/src/components/Forms/Legend.tsx @@ -1,10 +1,10 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import { useTheme, stylesFactory } from '../../themes'; import { GrafanaTheme } from '@grafana/data'; import { css, cx } from 'emotion'; export interface LabelProps extends React.HTMLAttributes { - children: string; + children: string | ReactNode; description?: string; } diff --git a/packages/grafana-ui/src/components/Forms/index.ts b/packages/grafana-ui/src/components/Forms/index.ts index 77e3d106ced..b8e7450c654 100644 --- a/packages/grafana-ui/src/components/Forms/index.ts +++ b/packages/grafana-ui/src/components/Forms/index.ts @@ -8,6 +8,7 @@ import { AsyncSelect, Select } from './Select/Select'; import { Form } from './Form'; import { Field } from './Field'; import { Switch } from './Switch'; +import { Legend } from './Legend'; import { TextArea } from './TextArea/TextArea'; import { Checkbox } from './Checkbox'; @@ -25,6 +26,7 @@ const Forms = { AsyncSelect, TextArea, Checkbox, + Legend, }; export default Forms; diff --git a/packages/grafana-ui/src/components/Select/Select.tsx b/packages/grafana-ui/src/components/Select/Select.tsx index 594972b64d0..e79652425bf 100644 --- a/packages/grafana-ui/src/components/Select/Select.tsx +++ b/packages/grafana-ui/src/components/Select/Select.tsx @@ -154,7 +154,7 @@ export class Select extends PureComponent> { onBlur={onBlur} openMenuOnFocus={openMenuOnFocus} maxMenuHeight={maxMenuHeight} - noOptionsMessage={() => noOptionsMessage} + noOptionsMessage={noOptionsMessage} isMulti={isMulti} backspaceRemovesValue={backspaceRemovesValue} menuIsOpen={isOpen} diff --git a/packages/grafana-ui/src/types/forms.ts b/packages/grafana-ui/src/types/forms.ts new file mode 100644 index 00000000000..edf931127a8 --- /dev/null +++ b/packages/grafana-ui/src/types/forms.ts @@ -0,0 +1,4 @@ +import { FormContextValues } from 'react-hook-form'; +export { OnSubmit as FormsOnSubmit, FieldErrors as FormFieldErrors } from 'react-hook-form'; + +export type FormAPI = Pick, 'register' | 'errors' | 'control' | 'formState' | 'getValues'>; diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts index f38c1975ae8..cdbfd6d82e2 100644 --- a/packages/grafana-ui/src/types/index.ts +++ b/packages/grafana-ui/src/types/index.ts @@ -2,3 +2,4 @@ export * from './theme'; export * from './input'; export * from './completion'; export * from './storybook'; +export * from './forms'; diff --git a/public/app/core/components/Select/DataSourcePicker.tsx b/public/app/core/components/Select/DataSourcePicker.tsx index 466adeb0ea6..42e8e426ee3 100644 --- a/public/app/core/components/Select/DataSourcePicker.tsx +++ b/public/app/core/components/Select/DataSourcePicker.tsx @@ -8,18 +8,20 @@ import { SelectableValue, DataSourceSelectItem } from '@grafana/data'; export interface Props { onChange: (ds: DataSourceSelectItem) => void; datasources: DataSourceSelectItem[]; - current: DataSourceSelectItem; + current?: DataSourceSelectItem; hideTextValue?: boolean; onBlur?: () => void; autoFocus?: boolean; openMenuOnFocus?: boolean; showLoading?: boolean; + placeholder?: string; } export class DataSourcePicker extends PureComponent { static defaultProps: Partial = { autoFocus: false, openMenuOnFocus: false, + placeholder: 'Select datasource', }; searchInput: HTMLElement; @@ -30,11 +32,23 @@ export class DataSourcePicker extends PureComponent { onChange = (item: SelectableValue) => { const ds = this.props.datasources.find(ds => ds.name === item.value); - this.props.onChange(ds); + + if (ds) { + this.props.onChange(ds); + } }; render() { - const { datasources, current, autoFocus, hideTextValue, onBlur, openMenuOnFocus, showLoading } = this.props; + const { + datasources, + current, + autoFocus, + hideTextValue, + onBlur, + openMenuOnFocus, + showLoading, + placeholder, + } = this.props; const options = datasources.map(ds => ({ value: ds.name, @@ -63,7 +77,7 @@ export class DataSourcePicker extends PureComponent { onBlur={onBlur} openMenuOnFocus={openMenuOnFocus} maxMenuHeight={500} - placeholder="Select datasource" + placeholder={placeholder} noOptionsMessage={() => 'No datasources found'} value={value} /> diff --git a/public/app/core/components/Select/FolderPicker.tsx b/public/app/core/components/Select/FolderPicker.tsx index bbc00f36461..ff3a3decb64 100644 --- a/public/app/core/components/Select/FolderPicker.tsx +++ b/public/app/core/components/Select/FolderPicker.tsx @@ -9,7 +9,7 @@ import { DashboardSearchHit } from '../../../types'; export interface Props { onChange: ($folder: { title: string; id: number }) => void; - enableCreateNew: boolean; + enableCreateNew?: boolean; rootName?: string; enableReset?: boolean; dashboardId?: any; @@ -43,6 +43,7 @@ export class FolderPicker extends PureComponent { enableReset: false, initialTitle: '', enableCreateNew: false, + useInNextGenForms: false, }; componentDidMount = async () => { @@ -115,7 +116,7 @@ export class FolderPicker extends PureComponent { folder = resetFolder; } - if (!folder) { + if (folder.value === -1) { if (contextSrv.isEditor) { folder = rootFolder; } else { diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index 3914e14a412..503583e01bf 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -14,6 +14,7 @@ import userReducers from 'app/features/profile/state/reducers'; import organizationReducers from 'app/features/org/state/reducers'; import ldapReducers from 'app/features/admin/state/reducers'; import templatingReducers from 'app/features/variables/state/reducers'; +import importDashboardReducers from 'app/features/manage-dashboards/state/reducers'; const rootReducers = { ...sharedReducers, @@ -30,6 +31,7 @@ const rootReducers = { ...organizationReducers, ...ldapReducers, ...templatingReducers, + ...importDashboardReducers, }; const addedReducers = {}; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index e2a864c4fe7..ff7c3d43c5c 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -15,19 +15,25 @@ function getNotFoundModel(): NavModel { }; } -export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel): NavModel { +export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel, onlyChild = false): NavModel { if (navIndex[id]) { const node = navIndex[id]; - const main = { - ...node.parentItem, - }; - main.children = main.children.map(item => { - return { - ...item, - active: item.url === node.url, - }; - }); + let main: NavModelItem; + if (!onlyChild && node.parentItem) { + main = { ...node.parentItem }; + + main.children = + main.children && + main.children.map(item => { + return { + ...item, + active: item.url === node.url, + }; + }); + } else { + main = node; + } return { node: node, diff --git a/public/app/features/manage-dashboards/DashboardImportCtrl.test.ts b/public/app/features/manage-dashboards/DashboardImportCtrl.test.ts deleted file mode 100644 index 07a1f47252f..00000000000 --- a/public/app/features/manage-dashboards/DashboardImportCtrl.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { DashboardImportCtrl } from './DashboardImportCtrl'; -import config from 'app/core/config'; -import { backendSrv } from 'app/core/services/backend_srv'; -import { IScope } from 'angular'; - -describe('DashboardImportCtrl', () => { - const ctx: any = {}; - jest.spyOn(backendSrv, 'getDashboardByUid').mockImplementation(() => Promise.resolve([])); - jest.spyOn(backendSrv, 'search').mockImplementation(() => Promise.resolve([])); - const getMock = jest.spyOn(backendSrv, 'get'); - const $scope = ({ $evalAsync: jest.fn() } as any) as IScope; - - let navModelSrv: any; - let validationSrv: any; - - beforeEach(() => { - navModelSrv = { - getNav: () => {}, - }; - - validationSrv = { - validateNewDashboardName: jest.fn().mockReturnValue(Promise.resolve()), - }; - - ctx.ctrl = new DashboardImportCtrl($scope, validationSrv, navModelSrv, {} as any, {} as any); - - jest.clearAllMocks(); - }); - - describe('when uploading json', () => { - beforeEach(() => { - config.datasources = { - ds: { - type: 'test-db', - } as any, - }; - - ctx.ctrl.onUpload({ - __inputs: [ - { - name: 'ds', - pluginId: 'test-db', - type: 'datasource', - pluginName: 'Test DB', - }, - ], - }); - }); - - it('should build input model', () => { - expect(ctx.ctrl.inputs.length).toBe(1); - expect(ctx.ctrl.inputs[0].name).toBe('ds'); - expect(ctx.ctrl.inputs[0].info).toBe('Select a Test DB data source'); - }); - - it('should set inputValid to false', () => { - expect(ctx.ctrl.inputsValid).toBe(false); - }); - }); - - describe('when specifying grafana.com url', () => { - beforeEach(() => { - ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123'; - // setup api mock - getMock.mockImplementation(() => Promise.resolve({ json: {} })); - return ctx.ctrl.checkGnetDashboard(); - }); - - it('should call gnet api with correct dashboard id', () => { - expect(getMock.mock.calls[0][0]).toBe('api/gnet/dashboards/123'); - }); - }); - - describe('when specifying dashboard id', () => { - beforeEach(() => { - ctx.ctrl.gnetUrl = '2342'; - // setup api mock - getMock.mockImplementation(() => Promise.resolve({ json: {} })); - return ctx.ctrl.checkGnetDashboard(); - }); - - it('should call gnet api with correct dashboard id', () => { - expect(getMock.mock.calls[0][0]).toBe('api/gnet/dashboards/2342'); - }); - }); -}); diff --git a/public/app/features/manage-dashboards/DashboardImportCtrl.ts b/public/app/features/manage-dashboards/DashboardImportCtrl.ts deleted file mode 100644 index 9d4b5cbfe58..00000000000 --- a/public/app/features/manage-dashboards/DashboardImportCtrl.ts +++ /dev/null @@ -1,258 +0,0 @@ -import _ from 'lodash'; -import config from 'app/core/config'; -import locationUtil from 'app/core/utils/location_util'; -import { ValidationSrv } from './services/ValidationSrv'; -import { NavModelSrv } from 'app/core/core'; -import { ILocationService, IScope } from 'angular'; -import { backendSrv } from 'app/core/services/backend_srv'; -import { promiseToDigest } from 'app/core/utils/promiseToDigest'; - -export class DashboardImportCtrl { - navModel: any; - step: number; - jsonText: string; - parseError: string; - nameExists: boolean; - uidExists: boolean; - dash: any; - inputs: any[]; - inputsValid: boolean; - gnetUrl: string; - gnetError: string; - gnetInfo: any; - titleTouched: boolean; - hasNameValidationError: boolean; - nameValidationError: any; - hasUidValidationError: boolean; - uidValidationError: any; - autoGenerateUid: boolean; - autoGenerateUidValue: string; - folderId: number; - initialFolderTitle: string; - isValidFolderSelection: boolean; - - /** @ngInject */ - constructor( - private $scope: IScope, - private validationSrv: ValidationSrv, - navModelSrv: NavModelSrv, - private $location: ILocationService, - $routeParams: any - ) { - this.navModel = navModelSrv.getNav('create', 'import'); - - this.step = 1; - this.nameExists = false; - this.uidExists = false; - this.autoGenerateUid = true; - this.autoGenerateUidValue = 'auto-generated'; - this.folderId = $routeParams.folderId ? Number($routeParams.folderId) || 0 : null; - this.initialFolderTitle = 'Select a folder'; - - // check gnetId in url - if ($routeParams.gnetId) { - this.gnetUrl = $routeParams.gnetId; - this.checkGnetDashboard(); - } - } - - onUpload(dash: any) { - this.dash = dash; - this.dash.id = null; - this.step = 2; - this.inputs = []; - - if (this.dash.__inputs) { - for (const input of this.dash.__inputs) { - const inputModel: any = { - name: input.name, - label: input.label, - info: input.description, - value: input.value, - type: input.type, - pluginId: input.pluginId, - options: [], - }; - - if (input.type === 'datasource') { - this.setDatasourceOptions(input, inputModel); - } else if (!inputModel.info) { - inputModel.info = 'Specify a string constant'; - } - - this.inputs.push(inputModel); - } - } - - this.inputsValid = this.inputs.length === 0; - this.titleChanged(); - this.uidChanged(true); - } - - setDatasourceOptions(input: { pluginId: string; pluginName: string }, inputModel: any) { - const sources = _.filter(config.datasources, val => { - return val.type === input.pluginId; - }); - - if (sources.length === 0) { - inputModel.info = 'No data sources of type ' + input.pluginName + ' found'; - } else if (!inputModel.info) { - inputModel.info = 'Select a ' + input.pluginName + ' data source'; - } - - inputModel.options = sources.map(val => { - return { text: val.name, value: val.name }; - }); - } - - inputValueChanged() { - this.inputsValid = true; - for (const input of this.inputs) { - if (!input.value) { - this.inputsValid = false; - } - } - } - - titleChanged() { - this.titleTouched = true; - this.nameExists = false; - - promiseToDigest(this.$scope)( - this.validationSrv - .validateNewDashboardName(this.folderId, this.dash.title) - .then(() => { - this.nameExists = false; - this.hasNameValidationError = false; - }) - .catch(err => { - if (err.type === 'EXISTING') { - this.nameExists = true; - } - - this.hasNameValidationError = true; - this.nameValidationError = err.message; - }) - ); - } - - uidChanged(initial: boolean) { - this.uidExists = false; - this.hasUidValidationError = false; - - if (initial === true && this.dash.uid) { - this.autoGenerateUidValue = 'value set'; - } - - if (!this.dash.uid) { - return; - } - - promiseToDigest(this.$scope)( - backendSrv - // @ts-ignore - .getDashboardByUid(this.dash.uid) - .then((res: any) => { - this.uidExists = true; - this.hasUidValidationError = true; - this.uidValidationError = `Dashboard named '${res.dashboard.title}' in folder '${res.meta.folderTitle}' has the same uid`; - }) - .catch((err: any) => { - err.isHandled = true; - }) - ); - } - - onFolderChange = (folder: any) => { - this.folderId = folder.id; - this.titleChanged(); - }; - - onEnterFolderCreation = () => { - this.inputsValid = false; - }; - - onExitFolderCreation = () => { - this.inputValueChanged(); - }; - - isValid() { - return this.inputsValid && this.folderId !== null; - } - - saveDashboard() { - const inputs = this.inputs.map(input => { - return { - name: input.name, - type: input.type, - pluginId: input.pluginId, - value: input.value, - }; - }); - - return promiseToDigest(this.$scope)( - backendSrv - .post('api/dashboards/import', { - dashboard: this.dash, - overwrite: true, - inputs: inputs, - folderId: this.folderId, - }) - .then(res => { - const dashUrl = locationUtil.stripBaseFromUrl(res.importedUrl); - this.$location.url(dashUrl); - }) - ); - } - - loadJsonText() { - try { - this.parseError = ''; - const dash = JSON.parse(this.jsonText); - this.onUpload(dash); - } catch (err) { - console.log(err); - this.parseError = err.message; - return; - } - } - - checkGnetDashboard() { - this.gnetError = ''; - - const match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); - let dashboardId; - - if (match && match[1]) { - dashboardId = match[1]; - } else if (match && match[2]) { - dashboardId = match[2]; - } else { - this.gnetError = 'Could not find dashboard'; - } - - return promiseToDigest(this.$scope)( - backendSrv - .get('api/gnet/dashboards/' + dashboardId) - .then(res => { - this.gnetInfo = res; - // store reference to grafana.com - res.json.gnetId = res.id; - this.onUpload(res.json); - }) - .catch(err => { - err.isHandled = true; - this.gnetError = err.data.message || err; - }) - ); - } - - back() { - this.gnetUrl = ''; - this.step = 1; - this.gnetError = ''; - this.gnetInfo = ''; - } -} - -export default DashboardImportCtrl; diff --git a/public/app/features/manage-dashboards/DashboardImportPage.tsx b/public/app/features/manage-dashboards/DashboardImportPage.tsx new file mode 100644 index 00000000000..3b1e13a3c51 --- /dev/null +++ b/public/app/features/manage-dashboards/DashboardImportPage.tsx @@ -0,0 +1,162 @@ +import React, { FormEvent, PureComponent } from 'react'; +import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; +import { css } from 'emotion'; +import { AppEvents, NavModel } from '@grafana/data'; +import { Button, Forms, stylesFactory } from '@grafana/ui'; +import Page from 'app/core/components/Page/Page'; +import { ImportDashboardOverview } from './components/ImportDashboardOverview'; +import { DashboardFileUpload } from './components/DashboardFileUpload'; +import { validateDashboardJson, validateGcomDashboard } from './utils/validation'; +import { fetchGcomDashboard, importDashboardJson } from './state/actions'; +import appEvents from 'app/core/app_events'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { StoreState } from 'app/types'; + +interface OwnProps {} + +interface ConnectedProps { + navModel: NavModel; + isLoaded: boolean; +} + +interface DispatchProps { + fetchGcomDashboard: typeof fetchGcomDashboard; + importDashboardJson: typeof importDashboardJson; +} + +type Props = OwnProps & ConnectedProps & DispatchProps; + +class DashboardImportUnConnected extends PureComponent { + onFileUpload = (event: FormEvent) => { + const { importDashboardJson } = this.props; + const file = event.currentTarget.files && event.currentTarget.files.length > 0 && event.currentTarget.files[0]; + + if (file) { + const reader = new FileReader(); + const readerOnLoad = () => { + return (e: any) => { + let dashboard: any; + try { + dashboard = JSON.parse(e.target.result); + } catch (error) { + appEvents.emit(AppEvents.alertError, [ + 'Import failed', + 'JSON -> JS Serialization failed: ' + error.message, + ]); + return; + } + importDashboardJson(dashboard); + }; + }; + reader.onload = readerOnLoad(); + reader.readAsText(file); + } + }; + + getDashboardFromJson = (formData: { dashboardJson: string }) => { + this.props.importDashboardJson(JSON.parse(formData.dashboardJson)); + }; + + getGcomDashboard = (formData: { gcomDashboard: string }) => { + let dashboardId; + const match = /(^\d+$)|dashboards\/(\d+)/.exec(formData.gcomDashboard); + if (match && match[1]) { + dashboardId = match[1]; + } else if (match && match[2]) { + dashboardId = match[2]; + } + + if (dashboardId) { + this.props.fetchGcomDashboard(dashboardId); + } + }; + + renderImportForm() { + const styles = importStyles(); + + return ( + <> +
+ +
+
+ Import via grafana.com + + {({ register, errors }) => ( + + Load} + /> + + )} + +
+
+ Import via panel json + + {({ register, errors }) => ( + <> + + + + + + )} + +
+ + ); + } + + render() { + const { isLoaded, navModel } = this.props; + return ( + + {isLoaded ? : this.renderImportForm()} + + ); + } +} + +const mapStateToProps: MapStateToProps = (state: StoreState) => ({ + navModel: getNavModel(state.navIndex, 'import', undefined, true), + isLoaded: state.importDashboard.isLoaded, +}); + +const mapDispatchToProps: MapDispatchToProps = { + fetchGcomDashboard, + importDashboardJson, +}; + +export const DashboardImportPage = connect(mapStateToProps, mapDispatchToProps)(DashboardImportUnConnected); +export default DashboardImportPage; +DashboardImportPage.displayName = 'DashboardImport'; + +const importStyles = stylesFactory(() => { + return { + option: css` + margin-bottom: 32px; + `, + }; +}); diff --git a/public/app/features/manage-dashboards/components/DashboardFileUpload.tsx b/public/app/features/manage-dashboards/components/DashboardFileUpload.tsx new file mode 100644 index 00000000000..acedd4eb228 --- /dev/null +++ b/public/app/features/manage-dashboards/components/DashboardFileUpload.tsx @@ -0,0 +1,39 @@ +import React, { FC, FormEvent } from 'react'; +import { Forms, stylesFactory, useTheme } from '@grafana/ui'; +import { GrafanaTheme } from '@grafana/data'; +import { css } from 'emotion'; + +interface Props { + onFileUpload: (event: FormEvent) => void; +} + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + const buttonFormStyle = Forms.getFormStyles(theme, { variant: 'primary', invalid: false, size: 'md' }).button.button; + return { + fileUpload: css` + display: none; + `, + button: css` + ${buttonFormStyle} + `, + }; +}); + +export const DashboardFileUpload: FC = ({ onFileUpload }) => { + const theme = useTheme(); + const style = getStyles(theme); + + return ( + + ); +}; diff --git a/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx new file mode 100644 index 00000000000..b1857cf6d2f --- /dev/null +++ b/public/app/features/manage-dashboards/components/ImportDashboardForm.tsx @@ -0,0 +1,155 @@ +import React, { FC, useEffect, useState } from 'react'; +import { Button, Forms, FormAPI, FormsOnSubmit, HorizontalGroup, FormFieldErrors } from '@grafana/ui'; +import { FolderPicker } from 'app/core/components/Select/FolderPicker'; +import DataSourcePicker from 'app/core/components/Select/DataSourcePicker'; +import { DashboardInput, DashboardInputs, DataSourceInput, ImportDashboardDTO } from '../state/reducers'; +import { validateTitle, validateUid } from '../utils/validation'; + +interface Props extends Omit, 'formState'> { + uidReset: boolean; + inputs: DashboardInputs; + initialFolderId: number; + + onCancel: () => void; + onUidReset: () => void; + onSubmit: FormsOnSubmit; +} + +export const ImportDashboardForm: FC = ({ + register, + errors, + control, + getValues, + uidReset, + inputs, + initialFolderId, + onUidReset, + onCancel, + onSubmit, +}) => { + const [isSubmitted, setSubmitted] = useState(false); + + /* + This useEffect is needed for overwriting a dashboard. It + submits the form even if there's validation errors on title or uid. + */ + useEffect(() => { + if (isSubmitted && (errors.title || errors.uid)) { + onSubmit(getValues({ nest: true }), {} as any); + } + }, [errors]); + + return ( + <> + Options + + await validateTitle(v, getValues().folderId), + })} + /> + + + + + + <> + {!uidReset ? ( + await validateUid(v) })} + addonAfter={!uidReset && } + /> + ) : ( + await validateUid(v) })} + /> + )} + + + {inputs.dataSources && + inputs.dataSources.map((input: DataSourceInput, index: number) => { + const dataSourceOption = `dataSources[${index}]`; + return ( + + + + ); + })} + {inputs.constants && + inputs.constants.map((input: DashboardInput, index) => { + const constantIndex = `constants[${index}]`; + return ( + + + + ); + })} + + + + + + ); +}; + +function getButtonVariant(errors: FormFieldErrors) { + return errors && (errors.title || errors.uid) ? 'destructive' : 'primary'; +} + +function getButtonText(errors: FormFieldErrors) { + return errors && (errors.title || errors.uid) ? 'Import (Overwrite)' : 'Import'; +} diff --git a/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx new file mode 100644 index 00000000000..f83114a6153 --- /dev/null +++ b/public/app/features/manage-dashboards/components/ImportDashboardOverview.tsx @@ -0,0 +1,123 @@ +import React, { PureComponent } from 'react'; +import { dateTime } from '@grafana/data'; +import { Forms } from '@grafana/ui'; +import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; +import { ImportDashboardForm } from './ImportDashboardForm'; +import { resetDashboard, saveDashboard } from '../state/actions'; +import { DashboardInputs, DashboardSource, ImportDashboardDTO } from '../state/reducers'; +import { StoreState } from 'app/types'; + +interface OwnProps {} + +interface ConnectedProps { + dashboard: ImportDashboardDTO; + inputs: DashboardInputs; + source: DashboardSource; + meta?: any; + folderId: number; +} + +interface DispatchProps { + resetDashboard: typeof resetDashboard; + saveDashboard: typeof saveDashboard; +} + +type Props = OwnProps & ConnectedProps & DispatchProps; + +interface State { + uidReset: boolean; +} + +class ImportDashboardOverviewUnConnected extends PureComponent { + state: State = { + uidReset: false, + }; + + onSubmit = (form: ImportDashboardDTO) => { + this.props.saveDashboard(form); + }; + + onCancel = () => { + this.props.resetDashboard(); + }; + + onUidReset = () => { + this.setState({ uidReset: true }); + }; + + render() { + const { dashboard, inputs, meta, source, folderId } = this.props; + const { uidReset } = this.state; + + return ( + <> + {source === DashboardSource.Gcom && ( +
+
+ + Importing Dashboard from{' '} + + Grafana.com + + +
+ + + + + + + + + + + +
Published by{meta.orgName}
Updated on{dateTime(meta.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
+
+ )} + + {({ register, errors, control, getValues }) => ( + + )} + + + ); + } +} + +const mapStateToProps: MapStateToProps = (state: StoreState) => ({ + dashboard: state.importDashboard.dashboard, + meta: state.importDashboard.meta, + source: state.importDashboard.source, + inputs: state.importDashboard.inputs, + folderId: state.location.routeParams.folderId ? Number(state.location.routeParams.folderId) : 0, +}); + +const mapDispatchToProps: MapDispatchToProps = { + resetDashboard, + saveDashboard, +}; + +export const ImportDashboardOverview = connect(mapStateToProps, mapDispatchToProps)(ImportDashboardOverviewUnConnected); +ImportDashboardOverview.displayName = 'ImportDashboardOverview'; diff --git a/public/app/features/manage-dashboards/partials/dashboard_import.html b/public/app/features/manage-dashboards/partials/dashboard_import.html deleted file mode 100644 index 3d057ea0372..00000000000 --- a/public/app/features/manage-dashboards/partials/dashboard_import.html +++ /dev/null @@ -1,170 +0,0 @@ - - -
-
- -
-
- -
- -
Grafana.com Dashboard
- -
-
- -
-
- -
-
- -
Or paste JSON
- -
-
- -
- - - - {{ctrl.parseError}} - -
-
- -
-
-

- Importing Dashboard from - Grafana.com -

- -
- - -
-
- - -
-
- -

- Options -

- -
-
-
- - - -
-
- -
-
- -
-
- -
-
- - -
-
- -
-
- - Unique identifier (uid) - - The unique identifier (uid) of a dashboard can be used for uniquely identify a dashboard between multiple - Grafana installs. - The uid allows having consistent URL’s for accessing dashboards so changing the title of a dashboard will - not break any - bookmarked links to that dashboard. - - - - change - - -
-
- -
-
- -
-
- -
-
- - -
- -
- - - -
-
-
- -
- - - Cancel -
- -
-
- -