mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
e3bbc14feb
commit
275c37503a
17
packages/grafana-ui/src/components/FileUpload/FileUpload.mdx
Normal file
17
packages/grafana-ui/src/components/FileUpload/FileUpload.mdx
Normal 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} />
|
@ -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])}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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');
|
||||
});
|
||||
});
|
79
packages/grafana-ui/src/components/FileUpload/FileUpload.tsx
Normal file
79
packages/grafana-ui/src/components/FileUpload/FileUpload.tsx
Normal 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};
|
||||
`,
|
||||
};
|
||||
});
|
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user