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;
|
className?: string;
|
||||||
/** Button size */
|
/** Button size */
|
||||||
size?: ComponentSize;
|
size?: ComponentSize;
|
||||||
|
/** Show the file name */
|
||||||
|
showFileName?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUpload: FC<Props> = ({
|
export const FileUpload: FC<Props> = ({
|
||||||
|
@ -5,18 +5,14 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
|||||||
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Table, useStyles2 } from '@grafana/ui';
|
import { Table, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { UploadView } from './UploadView';
|
|
||||||
import { StorageView } from './types';
|
import { StorageView } from './types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
listing: DataFrame;
|
listing: DataFrame;
|
||||||
path: string;
|
|
||||||
onPathChange: (p: string, view?: StorageView) => void;
|
|
||||||
view: StorageView;
|
view: StorageView;
|
||||||
fileNames: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FolderView({ listing, path, onPathChange, view, fileNames }: Props) {
|
export function FolderView({ listing, view }: Props) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
switch (view) {
|
switch (view) {
|
||||||
@ -24,21 +20,6 @@ export function FolderView({ listing, path, onPathChange, view, fileNames }: Pro
|
|||||||
return <div>CONFIGURE?</div>;
|
return <div>CONFIGURE?</div>;
|
||||||
case StorageView.Perms:
|
case StorageView.Perms:
|
||||||
return <div>Permissions</div>;
|
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 (
|
return (
|
||||||
|
@ -4,7 +4,7 @@ import { useAsync } from 'react-use';
|
|||||||
|
|
||||||
import { DataFrame, GrafanaTheme2, isDataFrame, ValueLinkConfig } from '@grafana/data';
|
import { DataFrame, GrafanaTheme2, isDataFrame, ValueLinkConfig } from '@grafana/data';
|
||||||
import { config, locationService } from '@grafana/runtime';
|
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 appEvents from 'app/core/app_events';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { useNavModel } from 'app/core/hooks/useNavModel';
|
import { useNavModel } from 'app/core/hooks/useNavModel';
|
||||||
@ -18,6 +18,7 @@ import { ExportView } from './ExportView';
|
|||||||
import { FileView } from './FileView';
|
import { FileView } from './FileView';
|
||||||
import { FolderView } from './FolderView';
|
import { FolderView } from './FolderView';
|
||||||
import { RootView } from './RootView';
|
import { RootView } from './RootView';
|
||||||
|
import { UploadButton } from './UploadButton';
|
||||||
import { getGrafanaStorage, filenameAlreadyExists } from './storage';
|
import { getGrafanaStorage, filenameAlreadyExists } from './storage';
|
||||||
import { StorageView } from './types';
|
import { StorageView } from './types';
|
||||||
|
|
||||||
@ -57,6 +58,7 @@ export default function StoragePage(props: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [isAddingNewFolder, setIsAddingNewFolder] = useState(false);
|
const [isAddingNewFolder, setIsAddingNewFolder] = useState(false);
|
||||||
|
const [errorMessages, setErrorMessages] = useState<string[]>([]);
|
||||||
|
|
||||||
const listing = useAsync((): Promise<DataFrame | undefined> => {
|
const listing = useAsync((): Promise<DataFrame | undefined> => {
|
||||||
return getGrafanaStorage()
|
return getGrafanaStorage()
|
||||||
@ -153,18 +155,27 @@ export default function StoragePage(props: Props) {
|
|||||||
opts.push({ what: StorageView.History, text: 'History' });
|
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 canAddFolder = isFolder && path.startsWith('resources');
|
||||||
const canDelete = path.startsWith('resources/');
|
const canDelete = path.startsWith('resources/');
|
||||||
const canViewDashboard =
|
const canViewDashboard =
|
||||||
path.startsWith('devenv/') && config.featureToggles.dashboardsFromStorage && (isFolder || path.endsWith('.json'));
|
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 (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<HorizontalGroup width="100%" justify="space-between" spacing={'md'} height={25}>
|
<HorizontalGroup width="100%" justify="space-between" spacing={'md'} height={25}>
|
||||||
@ -175,7 +186,13 @@ export default function StoragePage(props: Props) {
|
|||||||
Dashboard
|
Dashboard
|
||||||
</LinkButton>
|
</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 && (
|
{canDelete && (
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
@ -207,6 +224,8 @@ export default function StoragePage(props: Props) {
|
|||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
</HorizontalGroup>
|
</HorizontalGroup>
|
||||||
|
|
||||||
|
{errorMessages.length > 0 && getErrorMessages()}
|
||||||
|
|
||||||
<TabsBar>
|
<TabsBar>
|
||||||
{opts.map((opt) => (
|
{opts.map((opt) => (
|
||||||
<Tab
|
<Tab
|
||||||
@ -218,7 +237,7 @@ export default function StoragePage(props: Props) {
|
|||||||
))}
|
))}
|
||||||
</TabsBar>
|
</TabsBar>
|
||||||
{isFolder ? (
|
{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} />
|
<FileView path={path} listing={frame} onPathChange={setPath} view={view} />
|
||||||
)}
|
)}
|
||||||
@ -284,11 +303,14 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
border: 1px solid ${theme.colors.border.medium};
|
border: 1px solid ${theme.colors.border.medium};
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`,
|
`,
|
||||||
uploadSpot: css`
|
|
||||||
margin-left: ${theme.spacing(2)};
|
|
||||||
`,
|
|
||||||
border: css`
|
border: css`
|
||||||
border: 1px solid ${theme.colors.border.medium};
|
border: 1px solid ${theme.colors.border.medium};
|
||||||
padding: ${theme.spacing(2)};
|
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',
|
Data = 'data',
|
||||||
Config = 'config',
|
Config = 'config',
|
||||||
Perms = 'perms',
|
Perms = 'perms',
|
||||||
Upload = 'upload',
|
|
||||||
Export = 'export',
|
Export = 'export',
|
||||||
History = 'history',
|
History = 'history',
|
||||||
AddRoot = 'add',
|
AddRoot = 'add',
|
||||||
|
Loading…
Reference in New Issue
Block a user