mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05:00
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:
co-authored by
Ryan McKinley
Artur Wierzbicki
parent
4b417c8f3e
commit
900d9bf9a1
@@ -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>
|
||||
|
||||
@@ -126,6 +126,7 @@ export enum MediaType {
|
||||
export enum PickerTabType {
|
||||
Folder = 'folder',
|
||||
URL = 'url',
|
||||
Upload = 'upload',
|
||||
}
|
||||
|
||||
export enum ResourcePickerSize {
|
||||
|
||||
Reference in New Issue
Block a user