Skip to content
Snippets Groups Projects
index.tsx 6.37 KiB
Newer Older
  • Learn to ignore specific revisions
  • import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
    
    import { Page } from '../../components/Page';
    
    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
    
    } from '@heroicons/react/24/outline';
    
    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';
    
    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>();
    
      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 },
    
      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]);
    
    
    
      useEffect(() => {
    
        if (bucketObjects.length === 0 && !lockRef.current) {
          refreshBucketObjects()
    
        return () => {
          lockRef.current = true;
        }
      }, [bucketObjects, s3, refreshBucketObjects])
    
    
      const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
    
        if (!s3.isAuthenticated()) {
          console.warn(
            "Warning: cannot upload file because S3 service not authenticated"
          );
    
        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
          });
    
          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 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(() => {
    
            setSelectedRows(new Set());
            refreshBucketObjects();
    
    
      return (
        <Page title={bucketName}>
    
          <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}
              buckets={Array.from(selectedRows).map(index => bucketObjects[index])}
            />
          </div>
          <div className={`transition-all ease-in-out duration-200 ${selectedRows.size > 0 ? "mr-64" : "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}
                  selectedRows={selectedRows}
    
          </div>