diff --git a/src/components/Transfer/Drawer.tsx b/src/components/Transfer/Drawer.tsx index 9ed67d2..a2ea16b 100644 --- a/src/components/Transfer/Drawer.tsx +++ b/src/components/Transfer/Drawer.tsx @@ -1,5 +1,3 @@ -"use client"; - import React from "react"; import { Drawer, @@ -34,18 +32,81 @@ import { usePathname } from "next/navigation"; import { Item, useGlobusTransferStore } from "@/store/globus-transfer"; import NextLink from "next/link"; -import { MinusCircleIcon } from "@heroicons/react/24/outline"; +import { + ExclamationCircleIcon, + MinusCircleIcon, +} from "@heroicons/react/24/outline"; import { CollectionName } from "@/globus/Collection"; +import { useStat } from "@/hooks/useGlobusAPI"; +import { readableBytes } from "@globus/sdk/services/transfer/utils"; + import { isTransferEnabled } from "../../../static"; +export const TransferListItem = ({ + item, + onClick = () => {}, +}: { + item: Item; + onClick?: () => void; +}) => { + const stat = useStat(item.collection, item.path); + const removeItemBySubject = useGlobusTransferStore( + (state) => state.removeItemBySubject, + ); + return ( + + + + + {item.label} + + + {stat.data?.type === "file" && stat.data?.size && ( + {readableBytes(stat.data?.size)} + )} + + + + {stat.isError && ( + + )} + {item.path} + + + + + + + } + onClick={() => removeItemBySubject(item.subject)} + /> + + + ); +}; + export default function TransferDrawer() { const pathname = usePathname(); const { isOpen, onOpen, onClose } = useDisclosure(); const items = useGlobusTransferStore((state) => state.items); - const removeItemBySubject = useGlobusTransferStore( - (state) => state.removeItemBySubject, - ); const itemsByCollection = items.reduce( (acc: { [key: Item["collection"]]: Item[] }, item) => { @@ -127,38 +188,11 @@ export default function TransferDrawer() { {itemsByCollection[collection].map((item) => ( - - - - - {item.label} - - - - {item.path} - - - - - } - onClick={() => - removeItemBySubject(item.subject) - } - /> - - + ))} diff --git a/src/globus/collection-browser/CollectionBrowser.tsx b/src/globus/collection-browser/CollectionBrowser.tsx index 02b2d95..d0e2fce 100644 --- a/src/globus/collection-browser/CollectionBrowser.tsx +++ b/src/globus/collection-browser/CollectionBrowser.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Box, Input, @@ -23,15 +23,22 @@ import { useGlobusAuth } from "@globus/react-auth-context"; export type Endpoint = Record; export function CollectionSearch({ - defaultValue = null, + value = null, onSelect = () => {}, }: { - defaultValue?: Endpoint | null; + value?: Endpoint | null; onSelect: (endpoint: Endpoint) => void; }) { const auth = useGlobusAuth(); const [results, setResults] = useState([]); - const [selection, setSelection] = useState(defaultValue); + const [selection, setSelection] = useState(value); + + useEffect(() => { + setSelection(value); + if (value === null) { + setResults([]); + } + }, [value]); async function handleSearch(e: React.FormEvent) { const query = e.currentTarget.value; diff --git a/src/hooks/useGlobusAPI.ts b/src/hooks/useGlobusAPI.ts index 21d3562..a13289a 100644 --- a/src/hooks/useGlobusAPI.ts +++ b/src/hooks/useGlobusAPI.ts @@ -6,7 +6,7 @@ import { type AuthorizationManager } from "@globus/sdk/core/authorization/Author import { STATIC } from "../../static"; -export async function fetchCollection( +async function fetchCollection( authorization: AuthorizationManager | undefined, id: string, ) { @@ -18,12 +18,12 @@ export async function fetchCollection( return response.json(); } -export function useCollection(id: string) { +export function useCollection(collectionId: string) { const auth = useGlobusAuth(); return useQuery({ enabled: auth?.isAuthenticated, - queryKey: ["transfer", "collections", id], - queryFn: () => fetchCollection(auth.authorization, id), + queryKey: ["transfer", "collections", collectionId], + queryFn: () => fetchCollection(auth.authorization, collectionId), }); } @@ -58,3 +58,29 @@ export function useSubject(id: string) { queryFn: () => fetchSubject(auth?.authorization, id), }); } + +export function useStat(collectionId: string, path: string) { + const auth = useGlobusAuth(); + const key = ["transfer", "collections", collectionId, "stat", path]; + return useQuery({ + enabled: auth?.isAuthenticated, + queryKey: key, + queryFn: async () => { + const response = await transfer.fileOperations.stat( + collectionId, + { + query: { + path, + }, + }, + { manager: auth.authorization }, + ); + + if (!response.ok) { + throw new Error(response.statusText); + } + + return response.json(); + }, + }); +} diff --git a/src/pages/transfer.tsx b/src/pages/transfer.tsx index c07d4a1..b7b6a87 100644 --- a/src/pages/transfer.tsx +++ b/src/pages/transfer.tsx @@ -10,9 +10,7 @@ import { CardHeader, CardBody, Box, - HStack, Icon, - IconButton, Spacer, Stack, Button, @@ -22,18 +20,13 @@ import { Input, useToast, SimpleGrid, - Tooltip, Alert, AlertIcon, AlertTitle, FormHelperText, AlertDescription, } from "@chakra-ui/react"; -import { - XCircleIcon, - ArrowTopRightOnSquareIcon, -} from "@heroicons/react/24/outline"; -import NextLink from "next/link"; +import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; import { transfer, webapp } from "@globus/sdk"; import { useGlobusAuth } from "@globus/react-auth-context"; @@ -42,14 +35,12 @@ import { CollectionSearch } from "@/globus/collection-browser/CollectionBrowser" import { isTransferEnabled } from "../../static"; import PathVerifier from "@/globus/PathVerifier"; import { CollectionName } from "@/globus/Collection"; +import { TransferListItem } from "@/components/Transfer/Drawer"; export default function ResultPage() { const auth = useGlobusAuth(); const toast = useToast(); const transferStore = useGlobusTransferStore(); - const removeItemBySubject = useGlobusTransferStore( - (state) => state.removeItemBySubject, - ); if (isTransferEnabled === false) { return ( @@ -91,6 +82,7 @@ export default function ResultPage() { ) ).json(); + const basePath = path.endsWith("/") ? path : `${path}/`; const response = await transfer.taskSubmission.submitTransfer( { payload: { @@ -105,7 +97,7 @@ export default function ResultPage() { /** * @todo Should we allow (or require) configuration of `item.name` and `item.type`? */ - destination_path: `${path}${item.path}`, + destination_path: `${basePath}${item.path}`, recursive: item.type === "directory", }; }), @@ -117,6 +109,7 @@ export default function ResultPage() { const data = await response.json(); if (response.ok) { + transferStore.resetTransferSettings(); toast({ title: `Transfer: ${data.code}`, description: ( @@ -190,36 +183,7 @@ export default function ResultPage() { {itemsByCollection[collection].map((item) => ( - - - } - onClick={() => removeItemBySubject(item.subject)} - /> - - - {item.label} - - - - {item.path} - - - - - - + ))} @@ -243,9 +207,7 @@ export default function ResultPage() { Destination { transferStore.setDestination(destination); }} @@ -254,7 +216,7 @@ export default function ResultPage() { Path { @@ -264,7 +226,7 @@ export default function ResultPage() { {transferStore.transfer?.path && ( Label { transferStore.setLabel(e.currentTarget.value); diff --git a/src/store/globus-transfer.ts b/src/store/globus-transfer.ts index 110c94e..0c7ff6f 100644 --- a/src/store/globus-transfer.ts +++ b/src/store/globus-transfer.ts @@ -34,6 +34,7 @@ type Actions = { removeItemBySubject: (subject: Item["subject"]) => void; addItem: (item: Item) => void; reset: () => void; + resetTransferSettings: () => void; }; const initialState = { @@ -84,6 +85,15 @@ export const useGlobusTransferStore = create()( items: state.items.filter((i) => i.subject !== item), })); }, + /** + * Preserves the items selected for transfer but resets the settings. + */ + resetTransferSettings: () => { + return set((state) => ({ + ...state, + transfer: undefined, + })); + }, reset: () => { return set(initialState); },