Storage: Upload button (#52346)

pull/52425/head
Adela Almasan 3 years ago committed by GitHub
parent 4aae9d1567
commit 524948515c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      packages/grafana-ui/src/components/FileUpload/FileUpload.tsx
  2. 21
      public/app/features/storage/FolderView.tsx
  3. 48
      public/app/features/storage/StoragePage.tsx
  4. 118
      public/app/features/storage/UploadButton.tsx
  5. 156
      public/app/features/storage/UploadView.tsx
  6. 1
      public/app/features/storage/types.ts

@ -21,6 +21,8 @@ export interface Props {
className?: string;
/** Button size */
size?: ComponentSize;
/** Show the file name */
showFileName?: boolean;
}
export const FileUpload: FC<Props> = ({

@ -5,18 +5,14 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
import { Table, useStyles2 } from '@grafana/ui';
import { UploadView } from './UploadView';
import { StorageView } from './types';
interface Props {
listing: DataFrame;
path: string;
onPathChange: (p: string, view?: StorageView) => void;
view: StorageView;
fileNames: string[];
}
export function FolderView({ listing, path, onPathChange, view, fileNames }: Props) {
export function FolderView({ listing, view }: Props) {
const styles = useStyles2(getStyles);
switch (view) {
@ -24,21 +20,6 @@ export function FolderView({ listing, path, onPathChange, view, fileNames }: Pro
return <div>CONFIGURE?</div>;
case StorageView.Perms:
return <div>Permissions</div>;
case StorageView.Upload:
return (
<UploadView
folder={path}
onUpload={(rsp) => {
console.log('Uploaded: ' + path);
if (rsp.path) {
onPathChange(rsp.path);
} else {
onPathChange(path); // back to data
}
}}
fileNames={fileNames}
/>
);
}
return (

@ -4,7 +4,7 @@ import { useAsync } from 'react-use';
import { DataFrame, GrafanaTheme2, isDataFrame, ValueLinkConfig } from '@grafana/data';
import { config, locationService } from '@grafana/runtime';
import { useStyles2, IconName, Spinner, TabsBar, Tab, Button, HorizontalGroup, LinkButton } from '@grafana/ui';
import { useStyles2, IconName, Spinner, TabsBar, Tab, Button, HorizontalGroup, LinkButton, Alert } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
@ -18,6 +18,7 @@ import { ExportView } from './ExportView';
import { FileView } from './FileView';
import { FolderView } from './FolderView';
import { RootView } from './RootView';
import { UploadButton } from './UploadButton';
import { getGrafanaStorage, filenameAlreadyExists } from './storage';
import { StorageView } from './types';
@ -57,6 +58,7 @@ export default function StoragePage(props: Props) {
};
const [isAddingNewFolder, setIsAddingNewFolder] = useState(false);
const [errorMessages, setErrorMessages] = useState<string[]>([]);
const listing = useAsync((): Promise<DataFrame | undefined> => {
return getGrafanaStorage()
@ -153,18 +155,27 @@ export default function StoragePage(props: Props) {
opts.push({ what: StorageView.History, text: 'History' });
}
// Hardcode the uploadable folder :)
if (isFolder && path.startsWith('resources')) {
opts.push({
what: StorageView.Upload,
text: 'Upload',
});
}
const canAddFolder = isFolder && path.startsWith('resources');
const canDelete = path.startsWith('resources/');
const canViewDashboard =
path.startsWith('devenv/') && config.featureToggles.dashboardsFromStorage && (isFolder || path.endsWith('.json'));
const getErrorMessages = () => {
return (
<div className={styles.errorAlert}>
<Alert title="Upload failed" severity="error" onRemove={clearAlert}>
{errorMessages.map((error) => {
return <div key={error}>{error}</div>;
})}
</Alert>
</div>
);
};
const clearAlert = () => {
setErrorMessages([]);
};
return (
<div className={styles.wrapper}>
<HorizontalGroup width="100%" justify="space-between" spacing={'md'} height={25}>
@ -175,7 +186,13 @@ export default function StoragePage(props: Props) {
Dashboard
</LinkButton>
)}
{canAddFolder && <Button onClick={() => setIsAddingNewFolder(true)}>New Folder</Button>}
{canAddFolder && (
<>
<UploadButton path={path} setErrorMessages={setErrorMessages} fileNames={fileNames} setPath={setPath} />
<Button onClick={() => setIsAddingNewFolder(true)}>New Folder</Button>
</>
)}
{canDelete && (
<Button
variant="destructive"
@ -207,6 +224,8 @@ export default function StoragePage(props: Props) {
</HorizontalGroup>
</HorizontalGroup>
{errorMessages.length > 0 && getErrorMessages()}
<TabsBar>
{opts.map((opt) => (
<Tab
@ -218,7 +237,7 @@ export default function StoragePage(props: Props) {
))}
</TabsBar>
{isFolder ? (
<FolderView path={path} listing={frame} onPathChange={setPath} view={view} fileNames={fileNames} />
<FolderView listing={frame} view={view} />
) : (
<FileView path={path} listing={frame} onPathChange={setPath} view={view} />
)}
@ -284,11 +303,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
border: 1px solid ${theme.colors.border.medium};
height: 100%;
`,
uploadSpot: css`
margin-left: ${theme.spacing(2)};
`,
border: css`
border: 1px solid ${theme.colors.border.medium};
padding: ${theme.spacing(2)};
`,
errorAlert: css`
padding-top: 20px;
`,
uploadButton: css`
margin-right: ${theme.spacing(2)};
`,
});

@ -0,0 +1,118 @@
import { css } from '@emotion/css';
import React, { FormEvent, useEffect, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { ConfirmModal, FileUpload, useStyles2 } from '@grafana/ui';
import { filenameAlreadyExists, getGrafanaStorage } from './storage';
import { StorageView, UploadReponse } from './types';
interface Props {
setErrorMessages: (errors: string[]) => void;
setPath: (p: string, view?: StorageView) => void;
path: string;
fileNames: string[];
}
const fileFormats = 'image/jpg, image/jpeg, image/png, image/gif, image/webp';
export function UploadButton({ setErrorMessages, setPath, path, fileNames }: Props) {
const styles = useStyles2(getStyles);
const [file, setFile] = useState<File | undefined>(undefined);
const [filenameExists, setFilenameExists] = useState(false);
const [fileUploadKey, setFileUploadKey] = useState(1);
const [isConfirmOpen, setIsConfirmOpen] = useState(true);
useEffect(() => {
setFileUploadKey((prev) => prev + 1);
}, [file]);
const onUpload = (rsp: UploadReponse) => {
console.log('Uploaded: ' + path);
if (rsp.path) {
setPath(rsp.path);
} else {
setPath(path); // back to data
}
};
const doUpload = async (fileToUpload: File, overwriteExistingFile: boolean) => {
if (!fileToUpload) {
setErrorMessages(['Please select a file.']);
return;
}
const rsp = await getGrafanaStorage().upload(path, fileToUpload, overwriteExistingFile);
if (rsp.status !== 200) {
setErrorMessages([rsp.message]);
} else {
onUpload(rsp);
}
};
const onFileUpload = (event: FormEvent<HTMLInputElement>) => {
setErrorMessages([]);
const fileToUpload =
event.currentTarget.files && event.currentTarget.files.length > 0 && event.currentTarget.files[0]
? event.currentTarget.files[0]
: undefined;
if (fileToUpload) {
setFile(fileToUpload);
const fileExists = filenameAlreadyExists(fileToUpload.name, fileNames);
if (!fileExists) {
setFilenameExists(false);
doUpload(fileToUpload, false).then((r) => {});
} else {
setFilenameExists(true);
setIsConfirmOpen(true);
}
}
};
const onOverwriteConfirm = () => {
if (file) {
doUpload(file, true).then((r) => {});
setIsConfirmOpen(false);
}
};
const onOverwriteDismiss = () => {
setFile(undefined);
setFilenameExists(false);
setIsConfirmOpen(false);
};
return (
<>
<FileUpload accept={fileFormats} onFileUpload={onFileUpload} key={fileUploadKey} className={styles.uploadButton}>
Upload
</FileUpload>
{file && filenameExists && (
<ConfirmModal
isOpen={isConfirmOpen}
body={
<div>
<p>{file?.name}</p>
<p>A file with this name already exists.</p>
<p>What would you like to do?</p>
</div>
}
title={'This file already exists'}
confirmText={'Replace'}
onConfirm={onOverwriteConfirm}
onDismiss={onOverwriteDismiss}
/>
)}
</>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
uploadButton: css`
margin-right: ${theme.spacing(2)};
`,
});

@ -1,156 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import SVG from 'react-inlinesvg';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, Button, ButtonGroup, Checkbox, Field, FileDropzone, useStyles2 } from '@grafana/ui';
import { filenameAlreadyExists, getGrafanaStorage } from './storage';
import { UploadReponse } from './types';
interface Props {
folder: string;
onUpload: (rsp: UploadReponse) => void;
fileNames: string[];
}
interface ErrorResponse {
message: string;
}
const FileDropzoneCustomChildren = ({ secondaryText = 'Drag and drop here or browse' }) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.iconWrapper}>
<small className={styles.small}>{secondaryText}</small>
</div>
);
};
export const UploadView = ({ folder, onUpload, fileNames }: Props) => {
const [file, setFile] = useState<File | undefined>(undefined);
const styles = useStyles2(getStyles);
const [error, setError] = useState<ErrorResponse>({ message: '' });
const [overwriteExistingFile, setOverwriteExistingFile] = useState(false);
const Preview = () => {
if (!file) {
return <></>;
}
const isImage = file.type?.startsWith('image/');
const isSvg = file.name?.endsWith('.svg');
const src = URL.createObjectURL(file);
return (
<Field label="Preview">
<div className={styles.iconPreview}>
{isSvg && <SVG src={src} className={styles.img} />}
{isImage && !isSvg && <img src={src} className={styles.img} />}
</div>
</Field>
);
};
const doUpload = async () => {
if (!file) {
setError({ message: 'please select a file' });
return;
}
const rsp = await getGrafanaStorage().upload(folder, file, overwriteExistingFile);
if (rsp.status !== 200) {
setError(rsp);
} else {
onUpload(rsp);
}
};
const filenameExists = file ? filenameAlreadyExists(file.name, fileNames) : false;
const isUploadDisabled = !file || (filenameExists && !overwriteExistingFile);
return (
<div>
<FileDropzone
readAs="readAsBinaryString"
onFileRemove={() => {
setFile(undefined);
}}
options={{
accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.gif', '.webp'] },
multiple: false,
onDrop: (acceptedFiles: File[]) => {
setFile(acceptedFiles[0]);
},
}}
>
{error.message !== '' ? <p>{error.message}</p> : Boolean(file) ? <Preview /> : <FileDropzoneCustomChildren />}
</FileDropzone>
{file && filenameExists && (
<div className={styles.alert}>
<Alert title={`${file.name} already exists`} severity="error">
<Checkbox
value={overwriteExistingFile}
onChange={() => setOverwriteExistingFile(!overwriteExistingFile)}
label="Overwrite existing file"
/>
</Alert>
</div>
)}
<ButtonGroup>
<Button className={styles.button} variant={'primary'} disabled={isUploadDisabled} onClick={doUpload}>
Upload
</Button>
</ButtonGroup>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
resourcePickerPopover: css`
border-radius: ${theme.shape.borderRadius()};
box-shadow: ${theme.shadows.z3};
background: ${theme.colors.background.primary};
border: 1px solid ${theme.colors.border.medium};
`,
resourcePickerPopoverContent: css`
width: 315px;
font-size: ${theme.typography.bodySmall.fontSize};
min-height: 184px;
padding: ${theme.spacing(1)};
display: flex;
flex-direction: column;
`,
button: css`
margin: 12px 20px 5px;
`,
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};
`,
iconWrapper: css`
display: flex;
flex-direction: column;
align-items: center;
`,
small: css`
color: ${theme.colors.text.secondary};
margin-bottom: ${theme.spacing(2)};
`,
alert: css`
padding-top: 10px;
`,
});

@ -2,7 +2,6 @@ export enum StorageView {
Data = 'data',
Config = 'config',
Perms = 'perms',
Upload = 'upload',
Export = 'export',
History = 'history',
AddRoot = 'add',

Loading…
Cancel
Save