feat: 完成了 新建桶; 上传文件(基本功能)

todo: 上传 rename, 上传 public 权限选择
bug: 首次加载 conns list; 上传的时候前缀过滤失败
This commit is contained in:
zhaoyupeng
2024-10-12 17:35:59 +08:00
parent 1c818daf16
commit 777253063b
28 changed files with 791 additions and 96 deletions

View File

@ -1 +1 @@
b20ef5a27687e07e09878451f9a2e1aa
674272f31b23a798d77a17321c6a8785

View File

@ -1,5 +1,5 @@
import {makeStyles, MenuItem, MenuList, Text, tokens} from "@fluentui/react-components";
import {ArchiveRegular, DocumentBulletListRegular} from "@fluentui/react-icons";
import {ArchiveRegular} from "@fluentui/react-icons";
import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
import React from "react";
import {useStoreBucket} from "../../store/bucket";
@ -50,7 +50,7 @@ export function ListBucketComponent() {
return <MenuList className={styles.container}>
<VirtualizerScrollView
numItems={bucket_list.length}
numItems={bucket_list?bucket_list.length:0}
itemSize={32}
container={{role: 'list', style: {maxHeight: 'calc(100vh - 9rem)'}}}
>

View File

@ -0,0 +1,111 @@
import {
DialogTrigger,
DialogSurface,
DialogTitle,
DialogBody,
DialogActions,
DialogContent,
Button, Field, Input, makeStyles, tokens, Checkbox,
} from "@fluentui/react-components";
import {useState} from "react";
import {useToast} from "../../message";
import {useStoreConnection} from "../../store/connection";
import {useStoreBucket} from "../../store/bucket";
const useStyle = makeStyles({
container: {
backgroundColor: tokens.colorNeutralBackground1,
display: "flex",
flexDirection: "row",
height: "100%",
width: "100%",
gridColumnStart: 0,
},
content: {},
input: {
margin: '1rem',
},
checks: {
margin: '1rem',
display: "flex",
},
});
export interface BucketCreateProps {
openFn: (open: boolean) => void;
}
export function BucketCreate(props: BucketCreateProps) {
const styles = useStyle();
const {dispatchMessage} = useToast();
const [name, set_name] = useState<string>();
const [public_read, set_public_read] = useState(false);
const [public_read_write, set_public_read_write] = useState(false);
const {conn_active} = useStoreConnection();
const {bucket_create, bucket_get} = useStoreBucket()
async function create() {
if (!name) {
dispatchMessage('桶名不能为空', "warning")
return
}
bucket_create(conn_active!, name, public_read, public_read_write);
bucket_get(conn_active!, true)
props.openFn(false)
}
return <>
<DialogSurface>
<DialogBody>
<DialogTitle> {conn_active?.name} </DialogTitle>
<DialogContent>
<div className={styles.content}>
<Field className={styles.input} label="桶名">
<Input
value={name}
onChange={(e) => {
set_name(e.target.value)
}}></Input>
</Field>
<div className={styles.checks}>
<Field>
<Checkbox
checked={public_read}
onChange={(e) => {
if (public_read_write) {
return
}
set_public_read(e.target.checked);
}}
label={"公共读"}></Checkbox>
</Field>
<Field>
<Checkbox
checked={public_read_write}
onChange={(e) => {
set_public_read_write(e.target.checked)
if (e.target.checked) {
set_public_read(e.target.checked)
}
}}
label={"公共读/写"}></Checkbox>
</Field>
</div>
</div>
</DialogContent>
<DialogActions className={styles.container}>
<DialogTrigger disableButtonEnhancement>
<Button appearance="secondary"></Button>
</DialogTrigger>
<Button onClick={async () => {
await create()
}} appearance="primary"></Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</>
}

View File

@ -2,16 +2,16 @@ import {
Button,
Input,
makeStyles,
Menu,
MenuItem,
MenuList, MenuPopover, MenuProps,
mergeClasses, PositioningImperativeRef,
MenuList,
mergeClasses,
tokens,
Tooltip
} from "@fluentui/react-components"
import {DatabaseLinkRegular, DismissRegular} from "@fluentui/react-icons";
import React, {useState} from "react";
import {Bucket, Connection} from "../../interfaces/connection";
import React, { useEffect, useState} from "react";
import { Connection} from "../../interfaces/connection";
import {useToast} from "../../message";
import {Dial} from "../../api";
import {useStoreConnection} from "../../store/connection";
@ -40,6 +40,15 @@ const useStyles = makeStyles({
marginLeft: "0.5rem",
marginRight: "0.5rem",
},
ctx_menu: {
position: "absolute",
zIndex: "1000",
width: "15rem",
backgroundColor: tokens.colorNeutralBackground1,
boxShadow: `${tokens.shadow16}`,
paddingTop: "4px",
paddingBottom: "4px",
},
items: {
height: "100%",
width: "100%",
@ -78,6 +87,20 @@ export function ConnectionList() {
const {conn_list, conn_update} = useStoreConnection();
const [conn_filter, set_conn_filter] = useState<string>('');
const {bucket_get, bucket_set} = useStoreBucket()
const [ctx_menu, set_ctx_menu] = useState<{
x: number,
y: number,
display: 'none' | 'block'
}>({x: 0, y: 0, display: 'none'});
useEffect(() => {
document.addEventListener("click", (e) => {
set_ctx_menu({x: 0, y: 0, display: 'none'});
})
return () => {
document.removeEventListener("click", (e) => {})
}
}, [])
async function handleSelect(item: Connection) {
conn_list.map((one: Connection) => {
@ -114,6 +137,11 @@ export function ConnectionList() {
e.preventDefault()
console.log('[DEBUG] right click connection =', item, 'event =', e)
console.log(`[DEBUG] click position: [${e.pageX}, ${e.pageY}]`)
set_ctx_menu({
x: e.pageX,
y: e.pageY,
display: 'block',
})
}
return (
@ -132,6 +160,15 @@ export function ConnectionList() {
onChange={(e) => set_conn_filter(e.target.value)}
/>
</div>
<div className={styles.ctx_menu}
style={{left: ctx_menu.x, top: ctx_menu.y, display: ctx_menu.display}}
>
<MenuList>
<MenuItem></MenuItem>
<MenuItem></MenuItem>
<MenuItem></MenuItem>
</MenuList>
</div>
<div className={styles.items}>
<MenuList>
{conn_list.filter(item => item.name.includes(conn_filter)).map(item => {

View File

@ -5,7 +5,7 @@ import {
DialogBody,
DialogActions,
DialogContent,
Button, Spinner, Field, Input, FieldProps, makeStyles, tokens,
Button, Spinner, Field, Input, makeStyles, tokens,
} from "@fluentui/react-components";
import {useState} from "react";
import {CheckmarkFilled, DismissRegular} from "@fluentui/react-icons";
@ -84,8 +84,6 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
<div className='connection-form-field'>
<Field
label="name"
validationState="success"
validationMessage="This is a success message."
>
<Input placeholder='名称 (example: 测试S3-minio)' value={value.name}
onChange={(e) => {
@ -96,11 +94,11 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
<div className='connection-form-field'>
<Field
label="endpoint"
validationState="success"
validationMessage="This is a success message."
required
>
<Input placeholder='地址 (example: https://ip_or_server-name:port)'
value={value.endpoint}
required
onChange={(e) => {
setValue({...value, endpoint: e.target.value});
}}/>
@ -109,10 +107,11 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
<div className='connection-form-field'>
<Field
label="secret access"
validationState="success"
validationMessage="This is a success message."
required
>
<Input placeholder='' value={value.access} onChange={(e) => {
<Input placeholder=''
required
value={value.access} onChange={(e) => {
setValue({...value, access: e.target.value});
}}/>
</Field>
@ -120,10 +119,11 @@ export function ConnectionCreate(props: ConnectionCreateProps) {
<div className='connection-form-field'>
<Field
label="secret key"
validationState="success"
validationMessage="This is a success message."
required
>
<Input placeholder='' value={value.key} onChange={(e) => {
<Input placeholder=''
required
value={value.key} onChange={(e) => {
setValue({...value, key: e.target.value});
}}/>
</Field>

View File

@ -1,8 +1,7 @@
import {Path} from "./path";
import {ListBucketComponent} from "./list_bucket";
import {ListBucketComponent} from "../bucket/list_bucket";
import {makeStyles} from "@fluentui/react-components";
import {useStoreBucket} from "../../store/bucket";
import {useStoreFile} from "../../store/file";
import {ListFileComponent} from "./list_file";
const useStyles = makeStyles({
@ -18,9 +17,7 @@ const useStyles = makeStyles({
export function Content() {
const styles = useStyles()
const {bucket_active, bucket_list} = useStoreBucket()
const {file_list} = useStoreFile()
const {bucket_active } = useStoreBucket()
return <div className={styles.content}>
<Path/>
{

View File

@ -1,9 +1,15 @@
import {makeStyles, MenuItem, MenuList, Text, tokens} from "@fluentui/react-components";
import {ArchiveRegular, DocumentBulletListRegular, DocumentDismissRegular, FolderRegular} from "@fluentui/react-icons";
import {
DocumentBulletListRegular, DocumentChevronDoubleRegular, DocumentCssRegular, DocumentDatabaseRegular,
DocumentDismissRegular,
DocumentImageRegular, DocumentJavascriptRegular, DocumentPdfRegular, DocumentYmlRegular,
FolderRegular
} from "@fluentui/react-icons";
import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
import React, {useEffect} from "react";
import React from "react";
import {useStoreBucket} from "../../store/bucket";
import {Bucket, S3File} from "../../interfaces/connection";
import { S3File} from "../../interfaces/connection";
import {useStoreFile} from "../../store/file";
import {useStoreConnection} from "../../store/connection";
import {TrimSuffix} from "../../hook/strings";
@ -85,7 +91,8 @@ export function ListFileComponent() {
handleRightClick(e, files_list[idx])
}}>
<MenuItem className={styles.item}
icon={files_list[idx].type ? <FolderRegular/> : <DocumentBulletListRegular/>}>
icon={files_list[idx].type ? <FolderRegular/> :
<FileIcon name={files_list[idx].name}/>}>
<Text truncate wrap={false} className={styles.text}>
{filename(files_list[idx].key)}
</Text>
@ -94,7 +101,7 @@ export function ListFileComponent() {
}}
</VirtualizerScrollView> : <div className={styles.no_data}>
<div>
<DocumentDismissRegular />
<DocumentDismissRegular/>
</div>
<Text size={900}>
@ -102,4 +109,47 @@ export function ListFileComponent() {
</div>
}
</MenuList>
}
type FileIconProps = {
name: string
}
function FileIcon(props: FileIconProps) {
const strings = props.name.split(".")
const suffix = strings[strings.length - 1]
switch (suffix) {
case "png":
return <DocumentImageRegular/>
case "jpg":
return <DocumentImageRegular/>
case "jpeg":
return <DocumentImageRegular/>
case "gif":
return <DocumentImageRegular/>
case "db":
return <DocumentDatabaseRegular/>
case "sqlite":
return <DocumentDatabaseRegular/>
case "sqlite3":
return <DocumentDatabaseRegular/>
case "pdf":
return <DocumentPdfRegular/>
case "css":
return <DocumentCssRegular />
case "js":
return <DocumentJavascriptRegular/>
case "yaml":
return <DocumentYmlRegular/>
case "yml":
return <DocumentYmlRegular/>
case "html":
return <DocumentChevronDoubleRegular/>
case "json":
return <DocumentChevronDoubleRegular/>
case "go":
return <DocumentChevronDoubleRegular/>
default:
return <DocumentBulletListRegular/>
}
}

View File

@ -54,11 +54,19 @@ const useStyles = makeStyles({
export function Path() {
const styles = useStyles()
const {conn_active} = useStoreConnection()
const {bucket_active} = useStoreBucket()
const {bucket_active, bucket_get, bucket_set} = useStoreBucket()
const {prefix, files_get} = useStoreFile()
async function handleClickUp() {
const dirs = prefix.split('/').filter((item => item))
if (dirs.length > 0) {
dirs.pop()
files_get(conn_active!, bucket_active!, dirs.join("/"))
return
}
bucket_get(conn_active!, false)
bucket_set(null)
}

View File

@ -0,0 +1,123 @@
import {
DialogTrigger,
DialogSurface,
DialogTitle,
DialogBody,
DialogActions,
DialogContent,
Button, Field, Input, makeStyles, tokens, Tooltip,
} from "@fluentui/react-components";
import {useState} from "react";
import {useToast} from "../../message";
import {Dial} from "../../api";
import {useStoreConnection} from "../../store/connection";
import {useStoreBucket} from "../../store/bucket";
import {useStoreFile} from "../../store/file";
import {MoreHorizontalRegular} from "@fluentui/react-icons";
const useStyle = makeStyles({
container: {
backgroundColor: tokens.colorNeutralBackground1,
display: "flex",
flexDirection: "row",
height: "100%",
width: "100%",
gridColumnStart: 0,
},
input: {
cursor: "pointer",
display: 'flex',
},
select: {
minWidth: 'unset',
},
});
export interface UploadFilesProps {
openFn: (open: boolean) => void;
}
export function UploadFiles(props: UploadFilesProps) {
const styles = useStyle();
const {dispatchMessage} = useToast();
const { conn_active} = useStoreConnection();
const {bucket_active} = useStoreBucket();
const {prefix, files_get} = useStoreFile()
const [selected, set_selected] = useState<string[]>([]);
async function handleSelect() {
const res = await Dial<{ result: string[] }>('/runtime/dialog/open', {
title: '选择文件',
type: 'multi'
})
if (res.status !== 200) {
return
}
set_selected(res.data.result)
}
async function create() {
let ok = true
for (const item of selected) {
const res = await Dial('/api/file/upload', {
conn_id: conn_active?.id,
bucket: bucket_active?.name,
location: item,
detect_content_type: true,
})
if (res.status !== 200) {
dispatchMessage(`上传文件: ${item} 失败`, "error")
ok = false
return
}
}
if(ok) {
files_get(conn_active!, bucket_active!, prefix)
dispatchMessage('上传成功!', 'success')
props.openFn(false)
return
}
}
return <>
<DialogSurface>
<DialogBody>
<DialogTitle> {`${bucket_active?.name} / ${prefix}`}</DialogTitle>
<DialogContent>
<Field label="选择文件" required>
<Input
className={styles.input}
value={selected?.join('; ')}
contentAfter={
<Tooltip content={'点击选择文件'} relationship={'description'}>
<Button
className={styles.select}
onClick={async () => {
await handleSelect()
}}
size={'small'}
appearance={'transparent'}>
<MoreHorizontalRegular/>
</Button>
</Tooltip>
}/>
</Field>
</DialogContent>
<DialogActions className={styles.container}>
<DialogTrigger disableButtonEnhancement>
<Button appearance="secondary"></Button>
</DialogTrigger>
<Button onClick={async () => {
await create()
}} appearance="primary"></Button>
</DialogActions>
</DialogBody>
</DialogSurface>
</>
}

View File

@ -1,37 +1,76 @@
import {Button, Dialog, DialogTrigger, makeStyles} from "@fluentui/react-components";
import {Button, Dialog, DialogTrigger, makeStyles} from "@fluentui/react-components";
import {ConnectionCreate} from "../connection/new";
import {CloudAddFilled} from "@fluentui/react-icons";
import {AppsAddInRegular, DocumentArrowUpRegular, PlugConnectedAddRegular} from "@fluentui/react-icons";
import {useState} from "react";
import {useStoreConnection} from "../../store/connection";
import {BucketCreate} from "../bucket/new";
import {useStoreBucket} from "../../store/bucket";
import {UploadFiles} from "../file/upload_files";
const useStyles = makeStyles({
header: {
height: "5rem",
height: "5rem",
width: "100%",
display: 'flex',
alignItems: "center",
borderBottom: "1px solid lightgray",
},
button_new_connection: {
margin: '0.5rem',
button_new: {
margin: '0.5rem',
},
})
export function Header() {
const styles = useStyles();
const [openCreate, setOpenCreate] = useState(false);
const {conn_active} = useStoreConnection()
const {bucket_active} = useStoreBucket()
const [open_create_conn, set_open_create_conn] = useState(false);
const [open_create_bucket, set_open_create_bucket] = useState(false);
const [open_upload, set_open_upload] = useState(false);
return <div className={styles.header}>
<div className={styles.button_new_connection}>
<div className={styles.button_new}>
<Dialog
open={openCreate}
onOpenChange={(event, data) => setOpenCreate(data.open)}>
open={open_create_conn}
onOpenChange={(event, data) => set_open_create_conn(data.open)}>
<DialogTrigger disableButtonEnhancement>
<Button appearance="primary" icon={<CloudAddFilled/>}>
<Button appearance="primary" icon={<PlugConnectedAddRegular/>}>
</Button>
</DialogTrigger>
<ConnectionCreate openFn={setOpenCreate}/>
<ConnectionCreate openFn={set_open_create_conn}/>
</Dialog>
</div>
{conn_active &&
<div className={styles.button_new}>
<Dialog
open={open_create_bucket}
onOpenChange={(event, data) => set_open_create_bucket(data.open)}>
<DialogTrigger disableButtonEnhancement>
<Button appearance="primary" icon={<AppsAddInRegular/>}>
</Button>
</DialogTrigger>
<BucketCreate openFn={set_open_create_bucket}/>
</Dialog>
</div>
}
{
bucket_active &&
<div className={styles.button_new}>
<Dialog
open={open_upload}
onOpenChange={(event, data) => set_open_upload(data.open)}>
<DialogTrigger disableButtonEnhancement>
<Button appearance="primary" icon={<DocumentArrowUpRegular />}>
</Button>
</DialogTrigger>
<UploadFiles openFn={set_open_upload}/>
</Dialog>
</div>
}
</div>
}

View File

@ -3,4 +3,8 @@ export function TrimSuffix(str: string, suffix: string) {
return str.substring(0, str.length - suffix.length);
}
return str;
}
}
export function GetBaseFileName(fullPath: string) {
return fullPath.replace(/.*[\/\\]/, '');
}

View File

@ -7,6 +7,7 @@ interface StoreBucket {
bucket_set: (Bucket: Bucket | null) => void;
bucket_list: Bucket[];
bucket_get: (conn: Connection, refresh: boolean) => void;
bucket_create: (conn: Connection, name: string, public_read: boolean, public_read_write: boolean) => void;
}
let bucket_map: { [id: number]: Bucket[] };
@ -34,5 +35,21 @@ export const useStoreBucket = create<StoreBucket>()((set) => ({
return {bucket_list: bucket_map[conn.id]};
})
},
bucket_create: async (conn: Connection, name: string, public_read: boolean) => {
const res = await Dial<{ bucket: string }>('/api/bucket/create', {
conn_id: conn.id,
name: name,
public_read: public_read,
public_read_write: public_read,
})
if (res.status !== 200) {
return
}
set((state) => {
return {bucket_list: [...state.bucket_list, {name: res.data.bucket, created_at: 0}]}
})
}
}))

View File

@ -1,7 +1,4 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {context} from '../models';
export function Init(arg1:context.Context):Promise<void>;
export function Invoke(arg1:string,arg2:string):Promise<string>;

View File

@ -2,10 +2,6 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Init(arg1) {
return window['go']['controller']['App']['Init'](arg1);
}
export function Invoke(arg1, arg2) {
return window['go']['controller']['App']['Invoke'](arg1, arg2);
}