diff --git a/packages/grafana-ui/src/components/FileUpload/FileUpload.mdx b/packages/grafana-ui/src/components/FileUpload/FileUpload.mdx new file mode 100644 index 00000000000..141551de8d4 --- /dev/null +++ b/packages/grafana-ui/src/components/FileUpload/FileUpload.mdx @@ -0,0 +1,17 @@ +import { Story, Preview, Props } from '@storybook/addon-docs/blocks'; +import { FileUpload } from './FileUpload'; + +# FileUpload + +A button-styled input that triggers file upload popup. Button text and accepted file extensions can be customized via `label` and `accepted` props respectively. + +### Usage + +```jsx +import { FileUpload } from '@grafana/ui'; + + console.log('file', currentTarget?.files && currentTarget.files[0])}/> +``` + +### Props + diff --git a/packages/grafana-ui/src/components/FileUpload/FileUpload.story.tsx b/packages/grafana-ui/src/components/FileUpload/FileUpload.story.tsx new file mode 100644 index 00000000000..d3e44096404 --- /dev/null +++ b/packages/grafana-ui/src/components/FileUpload/FileUpload.story.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { FileUpload } from './FileUpload'; +import mdx from './FileUpload.mdx'; + +export default { + title: 'Forms/FileUpload', + component: FileUpload, + decorators: [withCenteredStory], + parameters: { + docs: { + page: mdx, + }, + }, +}; + +export const single = () => { + return ( + console.log('file', currentTarget?.files && currentTarget.files[0])} + /> + ); +}; diff --git a/packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx b/packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx new file mode 100644 index 00000000000..ad5274f9423 --- /dev/null +++ b/packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { FileUpload } from './FileUpload'; + +describe('FileUpload', () => { + it('should render upload button with default text and no file name', () => { + const wrapper = shallow( {}} />); + expect(wrapper.findWhere(comp => comp.text() === 'Upload file').exists()).toBeTruthy(); + expect(wrapper.find({ 'aria-label': 'File name' }).exists()).toBeFalsy(); + }); + + it("should trim uploaded file's name", () => { + const wrapper = shallow( {}} />); + + wrapper.find('input').simulate('change', { + currentTarget: { + files: [{ name: 'longFileName.something.png' }], + }, + }); + expect(wrapper.find({ 'aria-label': 'File name' }).exists()).toBeTruthy(); + // Trim file name longer than 16 chars + expect(wrapper.find({ 'aria-label': 'File name' }).text()).toEqual('longFileName.som....png'); + + // Keep the name below the length limit intact + wrapper.find('input').simulate('change', { + currentTarget: { + files: [{ name: 'longFileName.png' }], + }, + }); + expect(wrapper.find({ 'aria-label': 'File name' }).text()).toEqual('longFileName.png'); + }); +}); diff --git a/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx b/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx new file mode 100644 index 00000000000..776fdec5f45 --- /dev/null +++ b/packages/grafana-ui/src/components/FileUpload/FileUpload.tsx @@ -0,0 +1,79 @@ +import React, { FC, FormEvent, useCallback, useState } from 'react'; +import { GrafanaTheme } from '@grafana/data'; +import { css, cx } from 'emotion'; +import { getFormStyles, Icon } from '../index'; +import { stylesFactory, useTheme } from '../../themes'; + +export interface Props { + onFileUpload: (event: FormEvent) => void; + /** Accepted file extensions */ + accept?: string; + className?: string; +} + +function trimFileName(fileName: string) { + const nameLength = 16; + const delimiter = fileName.lastIndexOf('.'); + const extension = fileName.substring(delimiter); + const file = fileName.substring(0, delimiter); + + if (file.length < nameLength) { + return fileName; + } + + return `${file.substring(0, nameLength)}...${extension}`; +} + +export const FileUpload: FC = ({ onFileUpload, className, children = 'Upload file', accept = '*' }) => { + const theme = useTheme(); + const style = getStyles(theme); + const [fileName, setFileName] = useState(''); + + const onChange = useCallback((event: FormEvent) => { + const file = event.currentTarget?.files?.[0]; + if (file) { + setFileName(file.name ?? ''); + } + onFileUpload(event); + }, []); + + return ( + <> + + {fileName && ( + + {trimFileName(fileName)} + + )} + + ); +}; + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + const buttonFormStyle = getFormStyles(theme, { variant: 'primary', invalid: false, size: 'md' }).button.button; + return { + fileUpload: css` + display: none; + `, + button: css` + ${buttonFormStyle} + `, + icon: css` + margin-right: ${theme.spacing.xs}; + `, + fileName: css` + margin-left: ${theme.spacing.xs}; + `, + }; +}); diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index f5d9f95c329..ecd4b303909 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -153,6 +153,7 @@ export { Switch } from './Switch/Switch'; export { Checkbox } from './Forms/Checkbox'; export { TextArea } from './TextArea/TextArea'; +export { FileUpload } from './FileUpload/FileUpload'; // Legacy forms diff --git a/public/app/features/manage-dashboards/DashboardImportPage.tsx b/public/app/features/manage-dashboards/DashboardImportPage.tsx index d0cdf712945..0e2afbe6195 100644 --- a/public/app/features/manage-dashboards/DashboardImportPage.tsx +++ b/public/app/features/manage-dashboards/DashboardImportPage.tsx @@ -2,11 +2,10 @@ import React, { FormEvent, PureComponent } from 'react'; import { MapDispatchToProps, MapStateToProps } from 'react-redux'; import { css } from 'emotion'; import { AppEvents, NavModel } from '@grafana/data'; -import { Button, stylesFactory, Input, TextArea, Field, Form, Legend } from '@grafana/ui'; +import { Button, stylesFactory, Input, TextArea, Field, Form, Legend, FileUpload } from '@grafana/ui'; import Page from 'app/core/components/Page/Page'; import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp'; 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'; @@ -78,7 +77,9 @@ class DashboardImportUnConnected extends PureComponent { return ( <>
- + + Upload JSON file +
Import via grafana.com diff --git a/public/app/features/manage-dashboards/components/DashboardFileUpload.tsx b/public/app/features/manage-dashboards/components/DashboardFileUpload.tsx deleted file mode 100644 index 76636175921..00000000000 --- a/public/app/features/manage-dashboards/components/DashboardFileUpload.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { FC, FormEvent } from 'react'; -import { getFormStyles, 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 = 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 ( - - ); -};