FileStorage: Add upload form (#46749)

* move upload to http

* use storage from grafanads

* rever gomod changes

* fix test

* wip

* add upload func

* update upload func

* writing to uploads

* edit response from service

* use dropzone for UI

* modify response struct in service

* better read file

* set content type for svg

* restrict file types upload

* add test and clean up errors

* pass test

* fix backend lint errors

* limit type of files on FE

* add TODO for after merge

* rebase with storage changes

* comment out unused function

* update UI to not have 2 uploads

* only call upload on select

* use utils function to find * in path

* show preview on drag over

* not allowing upload of svg

* add preview to upload tab

* no console.log

* resolve conflicts

* refactor log line

* fix failing BE test

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: Artur Wierzbicki <artur.wierzbicki@grafana.com>
This commit is contained in:
An
2022-04-27 15:12:48 -04:00
committed by GitHub
co-authored by Ryan McKinley Artur Wierzbicki
parent 4b417c8f3e
commit 900d9bf9a1
7 changed files with 361 additions and 27 deletions
@@ -0,0 +1,132 @@
import { css } from '@emotion/css';
import React, { Dispatch, SetStateAction, useState } from 'react';
import SVG from 'react-inlinesvg';
import { GrafanaTheme2 } from '@grafana/data';
import { FileDropzone, useStyles2, Button, DropzoneFile, Field } from '@grafana/ui';
import { MediaType } from '../types';
interface Props {
setFormData: Dispatch<SetStateAction<FormData>>;
mediaType: MediaType;
setUpload: Dispatch<SetStateAction<boolean>>;
newValue: string;
error: ErrorResponse;
}
interface ErrorResponse {
message: string;
}
export function FileDropzoneCustomChildren({ secondaryText = 'Drag and drop here or browse' }) {
const styles = useStyles2(getStyles);
return (
<div className={styles.iconWrapper}>
<small className={styles.small}>{secondaryText}</small>
<Button type="button" icon="upload">
Upload
</Button>
</div>
);
}
export const FileUploader = ({ mediaType, setFormData, setUpload, error }: Props) => {
const styles = useStyles2(getStyles);
const [dropped, setDropped] = useState<boolean>(false);
const [file, setFile] = useState<string>('');
const Preview = () => (
<Field label="Preview">
<div className={styles.iconPreview}>
{mediaType === MediaType.Icon && <SVG src={file} className={styles.img} />}
{mediaType === MediaType.Image && <img src={file} className={styles.img} />}
</div>
</Field>
);
const onFileRemove = (file: DropzoneFile) => {
fetch(`/api/storage/delete/upload/${file.file.name}`, {
method: 'DELETE',
}).catch((error) => console.error('cannot delete file', error));
};
const acceptableFiles =
mediaType === 'icon' ? 'image/svg+xml' : 'image/jpeg,image/png,image/gif,image/png, image/webp';
return (
<FileDropzone
readAs="readAsBinaryString"
onFileRemove={onFileRemove}
options={{
accept: acceptableFiles,
multiple: false,
onDrop: (acceptedFiles: File[]) => {
let formData = new FormData();
formData.append('file', acceptedFiles[0]);
setFile(URL.createObjectURL(acceptedFiles[0]));
setDropped(true);
setFormData(formData);
setUpload(true);
},
}}
>
{error.message !== '' && dropped ? (
<p>{error.message}</p>
) : dropped ? (
<Preview />
) : (
<FileDropzoneCustomChildren />
)}
</FileDropzone>
);
};
function getStyles(theme: GrafanaTheme2, isDragActive?: boolean) {
return {
container: css`
display: flex;
flex-direction: column;
width: 100%;
`,
dropzone: css`
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
padding: ${theme.spacing(6)};
border-radius: 2px;
border: 2px dashed ${theme.colors.border.medium};
background-color: ${isDragActive ? theme.colors.background.secondary : theme.colors.background.primary};
cursor: pointer;
`,
iconWrapper: css`
display: flex;
flex-direction: column;
align-items: center;
`,
acceptMargin: css`
margin: ${theme.spacing(2, 0, 1)};
`,
small: css`
color: ${theme.colors.text.secondary};
margin-bottom: ${theme.spacing(2)};
`,
iconContainer: css`
display: flex;
flex-direction: column;
width: 80%;
align-items: center;
align-self: center;
`,
iconPreview: css`
width: 238px;
height: 198px;
border: 1px solid ${theme.colors.border.medium};
display: flex;
align-items: center;
justify-content: center;
`,
img: css`
width: 147px;
height: 147px;
fill: ${theme.colors.text.primary};
`,
};
}
@@ -5,10 +5,13 @@ import { useOverlay } from '@react-aria/overlays';
import React, { createRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { Button, ButtonGroup, useStyles2 } from '@grafana/ui';
import { config } from 'app/core/config';
import { MediaType, PickerTabType, ResourceFolderName } from '../types';
import { FileUploader } from './FileUploader';
import { FolderPickerTab } from './FolderPickerTab';
import { URLPickerTab } from './URLPickerTab';
@@ -19,6 +22,9 @@ interface Props {
folderName: ResourceFolderName;
}
interface ErrorResponse {
message: string;
}
export const ResourcePickerPopover = (props: Props) => {
const { value, onChange, mediaType, folderName } = props;
const styles = useStyles2(getStyles);
@@ -33,6 +39,9 @@ export const ResourcePickerPopover = (props: Props) => {
const [newValue, setNewValue] = useState<string>(value ?? '');
const [activePicker, setActivePicker] = useState<PickerTabType>(PickerTabType.Folder);
const [formData, setFormData] = useState<FormData>(new FormData());
const [upload, setUpload] = useState<boolean>(false);
const [error, setError] = useState<ErrorResponse>({ message: '' });
const getTabClassName = (tabName: PickerTabType) => {
return `${styles.resourcePickerPopoverTab} ${activePicker === tabName && styles.resourcePickerPopoverActiveTab}`;
@@ -49,13 +58,23 @@ export const ResourcePickerPopover = (props: Props) => {
);
const renderURLPicker = () => <URLPickerTab newValue={newValue} setNewValue={setNewValue} mediaType={mediaType} />;
const renderUploader = () => (
<FileUploader
mediaType={mediaType}
setFormData={setFormData}
setUpload={setUpload}
newValue={newValue}
error={error}
/>
);
const renderPicker = () => {
switch (activePicker) {
case PickerTabType.Folder:
return renderFolderPicker();
case PickerTabType.URL:
return renderURLPicker();
case PickerTabType.Upload:
return renderUploader();
default:
return renderFolderPicker();
}
@@ -75,6 +94,16 @@ export const ResourcePickerPopover = (props: Props) => {
<button className={getTabClassName(PickerTabType.URL)} onClick={() => setActivePicker(PickerTabType.URL)}>
URL
</button>
{config.featureToggles['storageLocalUpload'] ? (
<button
className={getTabClassName(PickerTabType.Upload)}
onClick={() => setActivePicker(PickerTabType.Upload)}
>
Upload
</button>
) : (
''
)}
</div>
<div className={styles.resourcePickerPopoverContent}>
{renderPicker()}
@@ -85,7 +114,31 @@ export const ResourcePickerPopover = (props: Props) => {
<Button
className={styles.button}
variant={newValue && newValue !== value ? 'primary' : 'secondary'}
onClick={() => onChange(newValue)}
onClick={() => {
if (upload) {
fetch('/api/storage/upload', {
method: 'POST',
body: formData,
})
.then((res) => {
if (res.status >= 400) {
res.json().then((data) => setError(data));
return;
} else {
return res.json();
}
})
.then((data) => {
getBackendSrv()
.get(`api/storage/read/${data.path}`)
.then(() => setNewValue(`${config.appUrl}api/storage/read/${data.path}`))
.then(() => onChange(`${config.appUrl}api/storage/read/${data.path}`));
})
.catch((err) => console.error(err));
} else {
onChange(newValue);
}
}}
>
Select
</Button>
+1
View File
@@ -126,6 +126,7 @@ export enum MediaType {
export enum PickerTabType {
Folder = 'folder',
URL = 'url',
Upload = 'upload',
}
export enum ResourcePickerSize {