Newer
Older
import { ChangeEvent, MouseEvent, useCallback, useEffect, useRef, useState } from 'react';
import { BucketObject } from '../../models/bucket';
import { Column, Table } from '../../components/Table';
import { getHumanSize } from '../../commons/utils';
import { Button } from '../../components/Button';
import { BucketInspector } from '../../components/BucketInspector';
import {
DocumentIcon,
PhotoIcon,
ArrowLeftIcon,
FolderIcon,
ArrowUpOnSquareIcon,
TrashIcon
import { useNavigate } from 'react-router-dom';
import { useS3Service } from '../../services/S3Service';
import { InputFile } from '../../components/InputFile';
import {
ListObjectsV2Command,
PutObjectCommand,
DeleteObjectCommand
} from '@aws-sdk/client-s3';
Jacopo Gasparetto
committed
type PropsType = {
bucketName: string
}
export const BucketBrowser = ({ bucketName }: PropsType) => {
const [bucketObjects, setBucketObjects] = useState<BucketObject[]>([]);
const [selectedRows, setSelectedRows] = useState<Set<number>>(new Set());
const s3 = useS3Service();
const navigate = useNavigate();
const inputRef = useRef<HTMLInputElement>();
Jacopo Gasparetto
committed
const lockRef = useRef<boolean>(false);
const columns: Column[] = [
{ id: "icon" },
{ id: "name", name: "Name" },
{ id: "last_modified", name: "Last Modified" },
{ id: "bucket_size", name: "Size" },
];
const tableData = bucketObjects.map((bucket: BucketObject) => {
const { Key } = bucket;
const isFolder = Key?.includes("/") || false;
const [name, extension] = (() => {
if (Key) {
const name = isFolder ? Key.split("/").slice()[0] : bucket.Key;
const ext = Key.split(".").slice(-1)[0];
return [name, ext];
}
return ["N/A", "N/A"];
})();
const getIcon = () => {
if (isFolder) return <FolderIcon />;
switch (extension) {
case "png":
case "jpeg":
case "jpg":
return <PhotoIcon />;
default:
return <DocumentIcon />
}
}
const Icon = () => {
return (
<div className='w-5'>
{getIcon()}
</div>
)
}
const bucketSize = bucket.Size ? getHumanSize(bucket.Size) : "N/A"
{ columnId: "icon", value: <Icon /> },
{ columnId: "name", value: name },
{ columnId: "last_modified", value: bucket.LastModified?.toString() ?? "N/A" },
{ columnId: "bucket_size", value: bucketSize },
Jacopo Gasparetto
committed
const refreshBucketObjects = useCallback(() => {
const f = async () => {
if (!s3.isAuthenticated()) {
return;
}
console.log("List Bucket objects...")
const listObjCmd = new ListObjectsV2Command({ Bucket: bucketName });
const response = await s3.client.send(listObjCmd);
const { Contents } = response;
if (Contents) {
setBucketObjects(Contents);
} else {
console.warn("Warning: bucket looks empty.");
}
};
f().catch(err => console.error(err));
}, [s3, bucketName]);
Jacopo Gasparetto
committed
if (bucketObjects.length === 0 && !lockRef.current) {
refreshBucketObjects()
Jacopo Gasparetto
committed
return () => {
lockRef.current = true;
}
}, [bucketObjects, s3, refreshBucketObjects])
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
Jacopo Gasparetto
committed
if (!e.target.files) {
return;
Jacopo Gasparetto
committed
if (!s3.isAuthenticated()) {
console.warn(
"Warning: cannot upload file because S3 service not authenticated"
);
Jacopo Gasparetto
committed
inputRef.current = e.target;
const { files } = e.target;
// Upload all files FIXME: use different approach for multiple files
Array.from(files).forEach(file => {
const putObjCmd = new PutObjectCommand({
Bucket: bucketName,
Body: file,
Key: file.name
});
Jacopo Gasparetto
committed
s3.client.send(putObjCmd)
.then(() => {
console.log("File uploaded");
if (inputRef.current) {
inputRef.current.files = null;
inputRef.current.value = "";
}
})
.then(refreshBucketObjects)
.catch(err => console.error(err));
});
}
const deleteObject = async (bucketName: string, key: string) => {
const delObjCmd = new DeleteObjectCommand({ Bucket: bucketName, Key: key });
try {
await s3.client.send(delObjCmd);
console.log(`Object with key ${key} deleted.`);
} catch (err) {
console.error(err);
}
}
const onSelect = (el: ChangeEvent<HTMLInputElement>, index: number) => {
const newState = new Set(selectedRows);
if (el.target.checked) {
newState.add(index);
} else {
newState.delete(index);
}
setSelectedRows(newState);
}
const onClick = (_: MouseEvent<HTMLTableRowElement>, index: number) => {
const newState = new Set([index]);
setSelectedRows(newState);
}
const deleteSelectedObjects = () => {
// Queue all delete asynchronously
let promises: Promise<void>[] = [];
selectedRows.forEach(rowIndex => {
const { Key } = bucketObjects[rowIndex];
if (Key) {
promises.push(deleteObject(bucketName, Key));
}
});
// Wait untill all delete are done then refresh the UI.
Promise.all(promises)
.then(() => {
Jacopo Gasparetto
committed
setSelectedRows(new Set());
refreshBucketObjects();
<Button
title="Back"
icon={<ArrowLeftIcon />}
onClick={() => navigate(-1)}
/>
<div className='top-0 fixed z-10 right-0 w-64 bg-slate-300'>
<BucketInspector
isOpen={selectedRows.size > 0}
bucket={bucketName}
objects={Array.from(selectedRows).map(index => bucketObjects[index])}
<div className={`transition-all ease-in-out duration-200 ${selectedRows.size > 0 ? "mr-72" : "mr-0"}`}>
<div className='container w-2/3'>
<div className="flex mt-8 place-content-between">
<InputFile
icon={<ArrowUpOnSquareIcon />}
onChange={handleFileChange}
/>
<Button
title="Delete file(s)"
icon={<TrashIcon />}
onClick={deleteSelectedObjects}
disabled={selectedRows.size === 0}
/>
</div>
<div className="flex place-content-center mt-4">
<Table
selectable={true}
columns={columns}
data={tableData}
onSelect={onSelect}