Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Transfer): Adds stat integration for files added to the Transfer list. #219

Merged
merged 3 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 72 additions & 38 deletions src/components/Transfer/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
"use client";

import React from "react";
import {
Drawer,
Expand Down Expand Up @@ -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 (
<Box key={item.subject}>
<HStack align="flex-start">
<Stack spacing={1}>
<Link
noOfLines={1}
as={NextLink}
href={`/results/${item.subject}`}
onClick={onClick}
>
{item.label}
</Link>
<HStack>
{stat.data?.type === "file" && stat.data?.size && (
<Text fontSize="xs">{readableBytes(stat.data?.size)}</Text>
)}
<Tooltip
label={
stat.isError ? `Unable to access "${item.path}".` : item.path
}
>
<Text noOfLines={1} fontSize="xs">
<Flex align="center">
{stat.isError && (
<Icon
as={ExclamationCircleIcon}
boxSize={4}
color="red.500"
mr={1}
/>
)}
{item.path}
</Flex>
</Text>
</Tooltip>
</HStack>
</Stack>
<Spacer />
<IconButton
variant="ghost"
aria-label="Remove item from transfer list"
icon={<Icon as={MinusCircleIcon} boxSize={6} />}
onClick={() => removeItemBySubject(item.subject)}
/>
</HStack>
</Box>
);
};

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) => {
Expand Down Expand Up @@ -127,38 +188,11 @@ export default function TransferDrawer() {
<CardBody>
<Stack overflow="hidden">
{itemsByCollection[collection].map((item) => (
<Box key={item.subject}>
<HStack align="flex-start">
<Stack spacing={1}>
<Link
noOfLines={1}
as={NextLink}
href={`/results?subject=${item.subject}`}
onClick={onClose}
>
{item.label}
</Link>
<Tooltip label={item.path}>
<Text
noOfLines={1}
fontSize="xs"
maxWidth="90%"
>
{item.path}
</Text>
</Tooltip>
</Stack>
<Spacer />
<IconButton
variant="ghost"
aria-label="Remove item from transfer list"
icon={<Icon as={MinusCircleIcon} boxSize={6} />}
onClick={() =>
removeItemBySubject(item.subject)
}
/>
</HStack>
</Box>
<TransferListItem
key={item.subject}
item={item}
onClick={onClose}
/>
))}
</Stack>
</CardBody>
Expand Down
15 changes: 11 additions & 4 deletions src/globus/collection-browser/CollectionBrowser.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import {
Box,
Input,
Expand All @@ -23,15 +23,22 @@ import { useGlobusAuth } from "@globus/react-auth-context";
export type Endpoint = Record<string, any>;

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<Endpoint[]>([]);
const [selection, setSelection] = useState(defaultValue);
const [selection, setSelection] = useState(value);

useEffect(() => {
setSelection(value);
if (value === null) {
setResults([]);
}
}, [value]);

async function handleSearch(e: React.FormEvent<HTMLInputElement>) {
const query = e.currentTarget.value;
Expand Down
34 changes: 30 additions & 4 deletions src/hooks/useGlobusAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
Expand All @@ -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),
});
}

Expand Down Expand Up @@ -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();
},
});
}
58 changes: 10 additions & 48 deletions src/pages/transfer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ import {
CardHeader,
CardBody,
Box,
HStack,
Icon,
IconButton,
Spacer,
Stack,
Button,
Expand All @@ -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";

Expand All @@ -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 (
Expand Down Expand Up @@ -91,6 +82,7 @@ export default function ResultPage() {
)
).json();

const basePath = path.endsWith("/") ? path : `${path}/`;
const response = await transfer.taskSubmission.submitTransfer(
{
payload: {
Expand All @@ -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",
};
}),
Expand All @@ -117,6 +109,7 @@ export default function ResultPage() {
const data = await response.json();

if (response.ok) {
transferStore.resetTransferSettings();
toast({
title: `Transfer: ${data.code}`,
description: (
Expand Down Expand Up @@ -190,36 +183,7 @@ export default function ResultPage() {
<CardBody>
<Stack>
{itemsByCollection[collection].map((item) => (
<Box key={item.subject}>
<HStack align="flex-start">
<IconButton
size="xs"
variant="ghost"
aria-label="Remove item from transfer list"
icon={<Icon as={XCircleIcon} boxSize={4} />}
onClick={() => removeItemBySubject(item.subject)}
/>
<Stack spacing={1}>
<Link
noOfLines={1}
as={NextLink}
href={`/results?subject=${item.subject}`}
>
{item.label}
</Link>
<Tooltip label={item.path}>
<Text
noOfLines={1}
fontSize="xs"
maxWidth="50%"
>
{item.path}
</Text>
</Tooltip>
</Stack>
<Spacer />
</HStack>
</Box>
<TransferListItem key={item.subject} item={item} />
))}
</Stack>
</CardBody>
Expand All @@ -243,9 +207,7 @@ export default function ResultPage() {
<FormControl>
<FormLabel>Destination</FormLabel>
<CollectionSearch
defaultValue={
transferStore.transfer?.destination ?? null
}
value={transferStore.transfer?.destination ?? null}
onSelect={(destination) => {
transferStore.setDestination(destination);
}}
Expand All @@ -254,7 +216,7 @@ export default function ResultPage() {
<FormControl>
<FormLabel>Path</FormLabel>
<Input
defaultValue={transferStore.transfer?.path}
value={transferStore.transfer?.path || ""}
required
disabled={!auth.isAuthenticated}
onChange={(e) => {
Expand All @@ -264,7 +226,7 @@ export default function ResultPage() {
<FormHelperText>
{transferStore.transfer?.path && (
<PathVerifier
path={transferStore.transfer?.path}
path={transferStore.transfer.path}
collectionId={
transferStore.transfer?.destination?.id
}
Expand All @@ -275,7 +237,7 @@ export default function ResultPage() {
<FormControl>
<FormLabel>Label</FormLabel>
<Input
defaultValue={transferStore.transfer?.label}
value={transferStore.transfer?.label || ""}
disabled={!auth.isAuthenticated}
onChange={(e) => {
transferStore.setLabel(e.currentTarget.value);
Expand Down
10 changes: 10 additions & 0 deletions src/store/globus-transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Actions = {
removeItemBySubject: (subject: Item["subject"]) => void;
addItem: (item: Item) => void;
reset: () => void;
resetTransferSettings: () => void;
};

const initialState = {
Expand Down Expand Up @@ -84,6 +85,15 @@ export const useGlobusTransferStore = create<State & Actions>()(
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);
},
Expand Down