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);
},