feat: 完成了 file list 右键简单菜单
feat: 完成了 file 下载 todo: 桶右键菜单和删除桶
This commit is contained in:
@ -9,9 +9,15 @@ import {
|
||||
tokens,
|
||||
Tooltip
|
||||
} from "@fluentui/react-components"
|
||||
import {DatabaseLinkRegular, DismissRegular} from "@fluentui/react-icons";
|
||||
import React, { useEffect, useState} from "react";
|
||||
import { Connection} from "../../interfaces/connection";
|
||||
import {
|
||||
DatabaseLinkRegular,
|
||||
DeleteRegular,
|
||||
DismissRegular,
|
||||
PlugConnectedRegular, PlugDisconnectedRegular,
|
||||
SettingsRegular
|
||||
} from "@fluentui/react-icons";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {Connection} from "../../interfaces/connection";
|
||||
import {useToast} from "../../message";
|
||||
import {Dial} from "../../api";
|
||||
import {useStoreConnection} from "../../store/connection";
|
||||
@ -92,13 +98,15 @@ export function ConnectionList() {
|
||||
y: number,
|
||||
display: 'none' | 'block'
|
||||
}>({x: 0, y: 0, display: 'none'});
|
||||
const [menu_conn, set_menu_conn] = useState<Connection | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("click", (e) => {
|
||||
set_ctx_menu({x: 0, y: 0, display: 'none'});
|
||||
})
|
||||
return () => {
|
||||
document.removeEventListener("click", (e) => {})
|
||||
document.removeEventListener("click", (e) => {
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -112,7 +120,8 @@ export function ConnectionList() {
|
||||
})
|
||||
}
|
||||
|
||||
async function handleConnect(item: Connection) {
|
||||
async function handleConnect(item: Connection | null) {
|
||||
if (!item) return;
|
||||
let res = await Dial('/api/connection/connect', {id: item.id});
|
||||
if (res.status !== 200) {
|
||||
dispatchMessage(res.msg, "error")
|
||||
@ -124,7 +133,8 @@ export function ConnectionList() {
|
||||
bucket_set(null)
|
||||
}
|
||||
|
||||
async function handleDisconnect(item: Connection) {
|
||||
async function handleDisconnect(item: Connection | null) {
|
||||
if (!item) return;
|
||||
let res = await Dial('/api/connection/disconnect', {id: item.id})
|
||||
if (res.status !== 200) {
|
||||
dispatchMessage(res.msg, "error")
|
||||
@ -135,11 +145,17 @@ export function ConnectionList() {
|
||||
|
||||
async function handleRightClick(e: React.MouseEvent<HTMLDivElement>, item: Connection) {
|
||||
e.preventDefault()
|
||||
console.log('[DEBUG] right click connection =', item, 'event =', e)
|
||||
console.log(`[DEBUG] click position: [${e.pageX}, ${e.pageY}]`)
|
||||
set_menu_conn(item)
|
||||
|
||||
const ele = document.querySelector('#list-connection-container')
|
||||
const eleX = ele ? ele.clientWidth : 0
|
||||
const eleY = ele ? ele.clientHeight : 0
|
||||
const positionX = (e.pageX + eleX > window.innerWidth) ? e.pageX - eleX : e.pageX
|
||||
const positionY = (e.pageY + eleY > window.innerHeight) ? e.pageY - eleY : e.pageY
|
||||
|
||||
set_ctx_menu({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
x: positionX,
|
||||
y: positionY,
|
||||
display: 'block',
|
||||
})
|
||||
}
|
||||
@ -160,13 +176,24 @@ 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}}
|
||||
<div
|
||||
id={'list-connection-container'}
|
||||
className={styles.ctx_menu}
|
||||
style={{left: ctx_menu.x, top: ctx_menu.y, display: ctx_menu.display}}
|
||||
>
|
||||
<MenuList>
|
||||
<MenuItem>连接</MenuItem>
|
||||
<MenuItem>设置</MenuItem>
|
||||
<MenuItem>删除</MenuItem>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await handleConnect(menu_conn)
|
||||
}}
|
||||
icon={<PlugConnectedRegular/>}>连接</MenuItem>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await handleDisconnect(menu_conn)
|
||||
}}
|
||||
icon={<PlugDisconnectedRegular/>}>断开</MenuItem>
|
||||
<MenuItem icon={<SettingsRegular/>}>设置</MenuItem>
|
||||
<MenuItem icon={<DeleteRegular/>}>删除</MenuItem>
|
||||
</MenuList>
|
||||
</div>
|
||||
<div className={styles.items}>
|
||||
@ -186,18 +213,6 @@ export function ConnectionList() {
|
||||
icon={<DatabaseLinkRegular/>}
|
||||
key={item.id}>
|
||||
{item.name}
|
||||
<Tooltip
|
||||
content="断开连接"
|
||||
relationship="label">
|
||||
<Button
|
||||
appearance={'transparent'}
|
||||
size="small"
|
||||
icon={<DismissRegular/>}
|
||||
className={styles.items_disconn}
|
||||
onClick={async () => {
|
||||
await handleDisconnect(item)
|
||||
}}/>
|
||||
</Tooltip>
|
||||
</MenuItem>
|
||||
})}
|
||||
</MenuList>
|
||||
|
@ -1,18 +1,28 @@
|
||||
import {makeStyles, MenuItem, MenuList, Text, tokens} from "@fluentui/react-components";
|
||||
import {
|
||||
|
||||
DocumentBulletListRegular, DocumentChevronDoubleRegular, DocumentCssRegular, DocumentDatabaseRegular,
|
||||
ArrowDownloadFilled,
|
||||
DeleteRegular,
|
||||
DocumentBulletListRegular,
|
||||
DocumentChevronDoubleRegular,
|
||||
DocumentCssRegular,
|
||||
DocumentDatabaseRegular,
|
||||
DocumentDismissRegular,
|
||||
DocumentImageRegular, DocumentJavascriptRegular, DocumentPdfRegular, DocumentYmlRegular,
|
||||
FolderRegular
|
||||
DocumentImageRegular,
|
||||
DocumentJavascriptRegular,
|
||||
DocumentPdfRegular,
|
||||
DocumentYmlRegular,
|
||||
FolderRegular,
|
||||
PreviewLinkRegular
|
||||
} from "@fluentui/react-icons";
|
||||
import {VirtualizerScrollView} from "@fluentui/react-components/unstable";
|
||||
import React from "react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useStoreBucket} from "../../store/bucket";
|
||||
import { S3File} from "../../interfaces/connection";
|
||||
import {S3File} from "../../interfaces/connection";
|
||||
import {useStoreFile} from "../../store/file";
|
||||
import {useStoreConnection} from "../../store/connection";
|
||||
import {TrimSuffix} from "../../hook/strings";
|
||||
import {Dial} from "../../api";
|
||||
import {useToast} from "../../message";
|
||||
|
||||
const useStyles = makeStyles({
|
||||
container: {
|
||||
@ -49,14 +59,39 @@ const useStyles = makeStyles({
|
||||
fontSize: '8rem',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
ctx_menu: {
|
||||
position: "absolute",
|
||||
zIndex: "1000",
|
||||
width: "15rem",
|
||||
backgroundColor: tokens.colorNeutralBackground1,
|
||||
boxShadow: `${tokens.shadow16}`,
|
||||
paddingTop: "4px",
|
||||
paddingBottom: "4px",
|
||||
},
|
||||
})
|
||||
|
||||
export function ListFileComponent() {
|
||||
|
||||
const styles = useStyles();
|
||||
const {dispatchMessage} = useToast();
|
||||
const {conn_active} = useStoreConnection();
|
||||
const {bucket_active} = useStoreBucket()
|
||||
const {files_get, files_list} = useStoreFile()
|
||||
const {file_active, files_get, file_set, files_list} = useStoreFile()
|
||||
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) => {
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
const filename = (key: string) => {
|
||||
let strs = TrimSuffix(key, "/").split("/")
|
||||
@ -70,45 +105,99 @@ export function ListFileComponent() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleRightClick(e: React.MouseEvent<HTMLDivElement>, item: S3File) {
|
||||
async function handleRightClick(e: React.MouseEvent<HTMLDivElement>, item: S3File) {
|
||||
e.preventDefault()
|
||||
await file_set(item.key)
|
||||
const ele = document.querySelector('#list-file-container')
|
||||
const eleX = ele ? ele.clientWidth : 0
|
||||
const eleY = ele ? ele.clientHeight : 0
|
||||
const positionX = (e.pageX + eleX > window.innerWidth) ? e.pageX - eleX : e.pageX
|
||||
const positionY = (e.pageY + eleY > window.innerHeight) ? e.pageY - eleY : e.pageY
|
||||
set_ctx_menu({
|
||||
x: positionX,
|
||||
y: positionY,
|
||||
display: 'block',
|
||||
})
|
||||
// const res = await Dial('/api/file/info', {conn_id: conn_active?.id, bucket: bucket_active?.name, key: item.key})
|
||||
}
|
||||
|
||||
return <MenuList className={styles.container}>
|
||||
{files_list.length ?
|
||||
<VirtualizerScrollView
|
||||
numItems={files_list.length}
|
||||
itemSize={32}
|
||||
container={{role: 'list', style: {maxHeight: 'calc(100vh - 9rem)'}}}
|
||||
>
|
||||
{(idx) => {
|
||||
return <div
|
||||
className={styles.row} key={idx}
|
||||
onClick={async () => {
|
||||
await handleClick(files_list[idx])
|
||||
}}
|
||||
onContextMenu={async (e) => {
|
||||
handleRightClick(e, files_list[idx])
|
||||
}}>
|
||||
<MenuItem className={styles.item}
|
||||
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>
|
||||
</MenuItem>
|
||||
</div>
|
||||
}}
|
||||
</VirtualizerScrollView> : <div className={styles.no_data}>
|
||||
<div>
|
||||
<DocumentDismissRegular/>
|
||||
</div>
|
||||
<Text size={900}>
|
||||
没有文件
|
||||
</Text>
|
||||
</div>
|
||||
async function handleDownload(file: string | null) {
|
||||
if (!file) return
|
||||
const res1 = await Dial<{result:string}>("/runtime/dialog/save", {
|
||||
default_filename: file,
|
||||
})
|
||||
if (res1.status !== 200) {
|
||||
return
|
||||
}
|
||||
</MenuList>
|
||||
if (!res1.data) return
|
||||
const res2 = await Dial('/api/file/download', {
|
||||
conn_id: conn_active?.id,
|
||||
bucket: bucket_active?.name,
|
||||
key: file,
|
||||
location: res1.data.result,
|
||||
})
|
||||
if (res2.status === 200) {
|
||||
dispatchMessage('保存文件成功', 'success')
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<div
|
||||
id={'list-file-container'}
|
||||
className={styles.ctx_menu}
|
||||
style={{left: ctx_menu.x, top: ctx_menu.y, display: ctx_menu.display}}
|
||||
>
|
||||
<MenuList>
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
await handleDownload(file_active)
|
||||
}}
|
||||
icon={<ArrowDownloadFilled/>}>下载</MenuItem>
|
||||
<MenuItem
|
||||
disabled
|
||||
onClick={async () => {
|
||||
// await handleDisconnect(menu_conn)
|
||||
}}
|
||||
icon={<PreviewLinkRegular/>}>预览</MenuItem>
|
||||
<MenuItem icon={<DeleteRegular/>}>删除</MenuItem>
|
||||
</MenuList>
|
||||
</div>
|
||||
<MenuList className={styles.container}>
|
||||
{files_list.length ?
|
||||
<VirtualizerScrollView
|
||||
numItems={files_list.length}
|
||||
itemSize={32}
|
||||
container={{role: 'list', style: {maxHeight: 'calc(100vh - 9rem)'}}}
|
||||
>
|
||||
{(idx) => {
|
||||
return <div
|
||||
className={styles.row} key={idx}
|
||||
onClick={async () => {
|
||||
await handleClick(files_list[idx])
|
||||
}}
|
||||
onContextMenu={async (e) => {
|
||||
await handleRightClick(e, files_list[idx])
|
||||
}}>
|
||||
<MenuItem className={styles.item}
|
||||
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>
|
||||
</MenuItem>
|
||||
</div>
|
||||
}}
|
||||
</VirtualizerScrollView> : <div className={styles.no_data}>
|
||||
<div>
|
||||
<DocumentDismissRegular/>
|
||||
</div>
|
||||
<Text size={900}>
|
||||
没有文件
|
||||
</Text>
|
||||
</div>
|
||||
}
|
||||
</MenuList>
|
||||
</>
|
||||
}
|
||||
|
||||
type FileIconProps = {
|
||||
@ -136,7 +225,7 @@ function FileIcon(props: FileIconProps) {
|
||||
case "pdf":
|
||||
return <DocumentPdfRegular/>
|
||||
case "css":
|
||||
return <DocumentCssRegular />
|
||||
return <DocumentCssRegular/>
|
||||
case "js":
|
||||
return <DocumentJavascriptRegular/>
|
||||
case "yaml":
|
||||
|
@ -66,7 +66,7 @@ export function Path() {
|
||||
}
|
||||
|
||||
bucket_get(conn_active!, false)
|
||||
bucket_set(null)
|
||||
await bucket_set(null)
|
||||
}
|
||||
|
||||
|
||||
|
3
frontend/src/component/preview/preview.tsx
Normal file
3
frontend/src/component/preview/preview.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export function PreviewFile() {
|
||||
|
||||
}
|
@ -4,7 +4,7 @@ import {Dial, Resp} from "../api";
|
||||
|
||||
interface StoreBucket {
|
||||
bucket_active: Bucket | null;
|
||||
bucket_set: (Bucket: Bucket | null) => void;
|
||||
bucket_set: (Bucket: Bucket | null) => Promise<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;
|
||||
|
@ -6,7 +6,7 @@ interface StoreConnection {
|
||||
conn_active: Connection | null;
|
||||
conn_list: Connection[];
|
||||
conn_get: () => void;
|
||||
conn_update: (connection: Connection) => void;
|
||||
conn_update: (connection: Connection) => Promise<void>;
|
||||
}
|
||||
|
||||
export const useStoreConnection = create<StoreConnection>()((set) => ({
|
||||
|
@ -3,13 +3,23 @@ import {Bucket, Connection, S3File} from "../interfaces/connection";
|
||||
import {Dial} from "../api";
|
||||
|
||||
interface StoreFile {
|
||||
file_active: string | null,
|
||||
file_set: (key: string) => Promise<void>,
|
||||
prefix: string;
|
||||
filter: string;
|
||||
files_list: S3File[];
|
||||
files_get: (conn: Connection, bucket: Bucket, prefix?: string, filter?: string) => void;
|
||||
}
|
||||
|
||||
export const useStoreFile = create<StoreFile>()((set) => ({
|
||||
file_active: null,
|
||||
file_set: async (key: string) => {
|
||||
set((state) => {
|
||||
return {file_active: key}
|
||||
})
|
||||
},
|
||||
prefix: "",
|
||||
filter: "",
|
||||
files_list: [],
|
||||
files_get: async (conn: Connection, bucket: Bucket, prefix = '', filter = '') => {
|
||||
const res = await Dial<{ list: S3File[] }>('/api/bucket/files', {
|
||||
@ -23,7 +33,7 @@ export const useStoreFile = create<StoreFile>()((set) => ({
|
||||
}
|
||||
|
||||
set((state) => {
|
||||
return {files_list: res.data.list, prefix: prefix}
|
||||
return {files_list: res.data.list, prefix: prefix, filter: filter}
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
18
frontend/src/store/preview.tsx
Normal file
18
frontend/src/store/preview.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import {create} from 'zustand'
|
||||
import {Bucket, Connection} from "../interfaces/connection";
|
||||
|
||||
interface StorePreview {
|
||||
preview_key: string;
|
||||
preview_url: string;
|
||||
preview_content_type: string;
|
||||
preview_set: (key: string) => void;
|
||||
}
|
||||
|
||||
export const useStorePreview = create<StorePreview>()((set) => ({
|
||||
preview_key: '',
|
||||
preview_url: '',
|
||||
preview_content_type: '',
|
||||
preview_set: async (key: string) => set(state => {
|
||||
return {preview_key: key}
|
||||
}),
|
||||
}))
|
Reference in New Issue
Block a user