mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Storage: Upload button (#52346)
This commit is contained in:
parent
4aae9d1567
commit
524948515c
@ -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)};
|
||||
`,
|
||||
});
|
||||
|
118
public/app/features/storage/UploadButton.tsx
Normal file
118
public/app/features/storage/UploadButton.tsx
Normal file
@ -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…
Reference in New Issue
Block a user