Grafana-UI: Add FileUpload (#25835)

* Grafana UI: Setup component

* Grafana UI: DashboardFileUpload => FileUpload

* Grafana UI: Expand docs

* Grafana UI: Add className

* Grafana UI: Update import

* Grafana UI: Clarify props

* Update packages/grafana-ui/src/components/FileUpload/FileUpload.tsx

Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com>

* Grafana UI: Style icon

* Grafana UI: Show uploaded file name

* Grafana UI: Add tests

* Grafana UI: Add useStyles + useCallback

* Grafana UI: Remove stylesFactory

* Grafana UI: Revert to useTheme

Co-authored-by: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com>
This commit is contained in:
Alex Khomenko 2020-06-30 17:48:38 +03:00 committed by GitHub
parent e3bbc14feb
commit 275c37503a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 156 additions and 42 deletions

View File

@ -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';
<FileUpload onFileUpload={({ currentTarget }) => console.log('file', currentTarget?.files && currentTarget.files[0])}/>
```
### Props
<Props of={FileUpload} />

View File

@ -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 (
<FileUpload
onFileUpload={({ currentTarget }) => console.log('file', currentTarget?.files && currentTarget.files[0])}
/>
);
};

View File

@ -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(<FileUpload onFileUpload={() => {}} />);
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(<FileUpload onFileUpload={() => {}} />);
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');
});
});

View File

@ -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<HTMLInputElement>) => 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<Props> = ({ onFileUpload, className, children = 'Upload file', accept = '*' }) => {
const theme = useTheme();
const style = getStyles(theme);
const [fileName, setFileName] = useState('');
const onChange = useCallback((event: FormEvent<HTMLInputElement>) => {
const file = event.currentTarget?.files?.[0];
if (file) {
setFileName(file.name ?? '');
}
onFileUpload(event);
}, []);
return (
<>
<label className={cx(style.button, className)}>
<Icon name="upload" className={style.icon} />
{children}
<input
type="file"
id="fileUpload"
className={style.fileUpload}
onChange={onChange}
multiple={false}
accept={accept}
/>
</label>
{fileName && (
<span aria-label="File name" className={style.fileName}>
{trimFileName(fileName)}
</span>
)}
</>
);
};
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};
`,
};
});

View File

@ -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

View File

@ -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<Props> {
return (
<>
<div className={styles.option}>
<DashboardFileUpload onFileUpload={this.onFileUpload} />
<FileUpload accept="application/json" onFileUpload={this.onFileUpload}>
Upload JSON file
</FileUpload>
</div>
<div className={styles.option}>
<Legend>Import via grafana.com</Legend>

View File

@ -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<HTMLInputElement>) => 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<Props> = ({ onFileUpload }) => {
const theme = useTheme();
const style = getStyles(theme);
return (
<label className={style.button}>
Upload .json file
<input
type="file"
id="fileUpload"
className={style.fileUpload}
onChange={onFileUpload}
multiple={false}
accept="application/json"
/>
</label>
);
};