diff --git a/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css b/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css new file mode 100644 index 00000000..48482b9b --- /dev/null +++ b/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css @@ -0,0 +1,93 @@ +.bodyContainer { + position: relative; +} + +.note { + margin-top: 0.5em; + margin-bottom: 1em; +} + +.tableContainer { + margin-bottom: 2em; +} + +.tableTitle { + font-size: 16px; + font-weight: 600; + margin: 4px 0 8px 0; +} + +.tableWrapper { + position: relative; +} + +.fileTableContainer { + max-height: 300px; + overflow-y: auto; + background-color: var(--secondary-background-color); + position: relative; + z-index: 1; +} + +.gradientOverlay { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 50px; + background-image: linear-gradient(transparent, black); + pointer-events: none; + z-index: 2; +} + +.file-table { + width: 100%; + border-collapse: collapse; + position: relative; + z-index: 1; +} + +.file-table th, +.file-table td { + padding: 8px; + text-align: left; + white-space: nowrap; + color: var(--primary-text-color); + background-color: var(--secondary-color); +} + +.file-table th { + background-color: var(--secondary-background-color); + position: sticky; + top: 0; + z-index: 3; +} + +.file-table td:first-child { + border-right: 1px solid var(--border-color); +} + +.footerButtons { + display: flex; + justify-content: flex-end; + gap: 8px; +} + +.summary { + display: flex; + justify-content: flex-end; + gap: 1em; + margin-top: var(--row-count-margin-top); + color: var(--secondary-text-color); + opacity: 0.8; + font-size: var(--row-count-intrisic-height); + height: var(--row-count-intrisic-height-height); +} + +.totalSize { + text-align: right; +} + +.fileCount { + text-align: right; +} diff --git a/packages/core/components/Modal/CopyFileManifest/index.tsx b/packages/core/components/Modal/CopyFileManifest/index.tsx new file mode 100644 index 00000000..8deeecd2 --- /dev/null +++ b/packages/core/components/Modal/CopyFileManifest/index.tsx @@ -0,0 +1,162 @@ +import filesize from "filesize"; +import * as React from "react"; +import { useDispatch, useSelector } from "react-redux"; + +import { ModalProps } from ".."; +import BaseModal from "../BaseModal"; +import { PrimaryButton, SecondaryButton } from "../../Buttons"; +import FileDetail from "../../../entity/FileDetail"; +import FileSelection from "../../../entity/FileSelection"; +import { interaction, selection } from "../../../state"; + +import styles from "./CopyFileManifest.module.css"; + +/** + * Table component for rendering file details. + */ +function FileTable({ files, title }: { files: FileDetail[]; title: string }) { + const containerRef = React.useRef(null); + const [hasScroll, setHasScroll] = React.useState(false); + + React.useEffect(() => { + const checkScroll = () => { + if (containerRef.current) { + const isScrollable = + containerRef.current.scrollHeight > containerRef.current.clientHeight; + setHasScroll(isScrollable); + } + }; + checkScroll(); // Initial check + window.addEventListener("resize", checkScroll); + return () => window.removeEventListener("resize", checkScroll); + }, [files]); + + const clipFileName = (filename: string) => { + if (filename.length > 20) { + return filename.slice(0, 9) + "..." + filename.slice(-8); + } + return filename; + }; + + const calculateTotalSize = (files: FileDetail[]) => { + if (files.length === 0) return ""; + const totalBytes = files.reduce((acc, file) => acc + (file.size || 0), 0); + return totalBytes ? filesize(totalBytes) : "Calculating..."; + }; + + return ( +
+

{title}

+
+
+ + + + + + + + + {files.map((file) => ( + + + + + ))} + +
File NameFile Size
{clipFileName(file.name)}{filesize(file.size || 0)}
+
+ {hasScroll &&
} +
+
+ {files.length > 0 && ( + {calculateTotalSize(files)} + )} + {files.length.toLocaleString()} files +
+
+ ); +} + +/** + * Modal overlay for displaying details of selected files for NAS cache (VAST) operations. + */ +export default function CopyFileManifest({ onDismiss }: ModalProps) { + const dispatch = useDispatch(); + const fileSelection = useSelector( + selection.selectors.getFileSelection, + FileSelection.selectionsAreEqual + ); + + const [fileDetails, setFileDetails] = React.useState([]); + + React.useEffect(() => { + async function fetchDetails() { + const details = await fileSelection.fetchAllDetails(); + setFileDetails(details); + } + fetchDetails(); + }, [fileSelection]); + + const onMove = () => { + dispatch(interaction.actions.copyFiles(fileDetails)); + onDismiss(); + }; + + const filesInLocalCache = fileDetails.filter((file) => + file.annotations.some( + (annotation) => + annotation.name === "Should Be in Local Cache" && annotation.values[0] === true + ) + ); + + const filesNotInLocalCache = fileDetails.filter( + (file) => + file.annotations.some( + (annotation) => + annotation.name === "Should Be in Local Cache" && annotation.values[0] === false + ) || + !file.annotations.some((annotation) => annotation.name === "Should Be in Local Cache") + ); + + return ( + +

+ Files copied to the local storage (VAST) are stored with a 180-day + expiration, after which they revert to cloud-only storage. To extend the + expiration, reselect the files and confirm the update. +

+ + +
+ } + footer={ +
+ + +
+ } + onDismiss={onDismiss} + title="Copy files to local storage (VAST)" + /> + ); +} diff --git a/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css b/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css deleted file mode 100644 index 641907b9..00000000 --- a/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css +++ /dev/null @@ -1,33 +0,0 @@ -.fileTableContainer { - max-height: 300px; - overflow-y: auto; - border: 1px solid var(--border-color); - margin-bottom: 1em; - padding: 1em; - background: linear-gradient(to bottom, transparent, var(--secondary-background-color)); -} - -.file-table { - width: 100%; - border-collapse: collapse; -} - -.file-table th, -.file-table td { - padding: 8px; - text-align: left; - border-bottom: 1px solid var(--border-color); - white-space: nowrap; - color: var(--primary-text-color); -} - -.file-table th { - background-color: var(--secondary-background-color); - position: sticky; - top: 0; - z-index: 1; -} - -.file-table td:first-child { - border-right: 1px solid var(--border-color); -} diff --git a/packages/core/components/Modal/MoveFileManifest/index.tsx b/packages/core/components/Modal/MoveFileManifest/index.tsx deleted file mode 100644 index 41611686..00000000 --- a/packages/core/components/Modal/MoveFileManifest/index.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import filesize from "filesize"; -import * as React from "react"; -import { useDispatch, useSelector } from "react-redux"; - -import { ModalProps } from ".."; -import BaseModal from "../BaseModal"; -import { PrimaryButton } from "../../Buttons"; -import FileDetail from "../../../entity/FileDetail"; -import FileSelection from "../../../entity/FileSelection"; -import { interaction, selection } from "../../../state"; - -import styles from "./MoveFileManifest.module.css"; - -/** - * Modal overlay for displaying details of selected files for NAS cache operations. - */ -export default function MoveFileManifest({ onDismiss }: ModalProps) { - const dispatch = useDispatch(); - const fileService = useSelector(interaction.selectors.getFileService); - const fileSelection = useSelector( - selection.selectors.getFileSelection, - FileSelection.selectionsAreEqual - ); - - const [fileDetails, setFileDetails] = React.useState([]); - const [totalSize, setTotalSize] = React.useState(); - const [isLoading, setLoading] = React.useState(false); - - React.useEffect(() => { - async function fetchDetails() { - setLoading(true); - const details = await fileSelection.fetchAllDetails(); - setFileDetails(details); - - const aggregateInfo = await fileService.getAggregateInformation(fileSelection); - const formattedSize = aggregateInfo.size ? filesize(aggregateInfo.size) : undefined; - setTotalSize(formattedSize); - setLoading(false); - } - - fetchDetails(); - }, [fileSelection, fileService]); - - const onMove = () => { - dispatch(interaction.actions.moveFiles(fileDetails)); - onDismiss(); - }; - - const body = ( -
-

Selected Files:

-
- - - - - - - - - {fileDetails.map((file) => ( - - - - - ))} - -
File NameFile Size
{file.name}{filesize(file.size || 0)}
-
-

Total Files: {fileDetails.length}

-

Total Size: {isLoading ? "Loading..." : totalSize}

-
- ); - - return ( - - } - onDismiss={onDismiss} - title="Move Files to NAS Cache" - /> - ); -} diff --git a/packages/core/components/Modal/index.tsx b/packages/core/components/Modal/index.tsx index 66f998f5..ac37e475 100644 --- a/packages/core/components/Modal/index.tsx +++ b/packages/core/components/Modal/index.tsx @@ -6,7 +6,7 @@ import CodeSnippet from "./CodeSnippet"; import DataSource from "./DataSource"; import MetadataManifest from "./MetadataManifest"; import SmallScreenWarning from "./SmallScreenWarning"; -import MoveFileManifest from "./MoveFileManifest"; +import CopyFileManifest from "./CopyFileManifest"; export interface ModalProps { onDismiss: () => void; @@ -17,7 +17,7 @@ export enum ModalType { DataSource = 2, MetadataManifest = 3, SmallScreenWarning = 4, - MoveFileManifest = 5, + CopyFileManifest = 5, } /** @@ -40,8 +40,8 @@ export default function Modal() { return ; case ModalType.SmallScreenWarning: return ; - case ModalType.MoveFileManifest: - return ; + case ModalType.CopyFileManifest: + return ; default: return null; } diff --git a/packages/core/hooks/useFileAccessContextMenu.ts b/packages/core/hooks/useFileAccessContextMenu.ts index 307b6cad..a07c1d57 100644 --- a/packages/core/hooks/useFileAccessContextMenu.ts +++ b/packages/core/hooks/useFileAccessContextMenu.ts @@ -138,13 +138,13 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => { ...(isQueryingAicsFms && !isOnWeb ? [ { - key: "move-to-cache", - text: "Move to Cache", - title: "Move selected files to NAS Cache", + key: "copy-to-cache", + text: "Copy to vast", + title: "Copy selected files to NAS Cache (VAST)", disabled: !filters && fileSelection.count() === 0, iconProps: { iconName: "MoveToFolder" }, onClick() { - dispatch(interaction.actions.showMoveFileManifest()); + dispatch(interaction.actions.showCopyFileManifest()); }, }, ] diff --git a/packages/core/state/interaction/actions.ts b/packages/core/state/interaction/actions.ts index eca0a8c5..40ac7bec 100644 --- a/packages/core/state/interaction/actions.ts +++ b/packages/core/state/interaction/actions.ts @@ -679,35 +679,35 @@ export function setSelectedPublicDataset(dataset: PublicDataset): SetSelectedPub } /** - * SHOW_MOVE_FILE_MANIFEST + * SHOW_COPY_FILE_MANIFEST * - * Action to show the Move File dialog (manifest) for NAS cache operations. - * This modal will allow users to move files onto the NAS cache. + * Action to show the Copy File dialog (manifest) for NAS cache operations. + * This modal will allow users to copy files onto the NAS cache. */ -export const SHOW_MOVE_FILE_MANIFEST = makeConstant(STATE_BRANCH_NAME, "show-move-file-manifest"); +export const SHOW_COPY_FILE_MANIFEST = makeConstant(STATE_BRANCH_NAME, "show-copy-file-manifest"); -export interface ShowMoveFileManifestAction { +export interface ShowCopyFileManifestAction { type: string; } -export function showMoveFileManifest(): ShowMoveFileManifestAction { +export function showCopyFileManifest(): ShowCopyFileManifestAction { return { - type: SHOW_MOVE_FILE_MANIFEST, + type: SHOW_COPY_FILE_MANIFEST, }; } -export const MOVE_FILES = makeConstant(STATE_BRANCH_NAME, "move-files"); +export const COPY_FILES = makeConstant(STATE_BRANCH_NAME, "copy-files"); -export interface MoveFilesAction { +export interface CopyFilesAction { type: string; payload: { fileDetails: FileDetail[]; }; } -export function moveFiles(fileDetails: FileDetail[]): MoveFilesAction { +export function copyFiles(fileDetails: FileDetail[]): CopyFilesAction { return { - type: MOVE_FILES, + type: COPY_FILES, payload: { fileDetails, }, diff --git a/packages/core/state/interaction/logics.ts b/packages/core/state/interaction/logics.ts index ad07d649..a29987f6 100644 --- a/packages/core/state/interaction/logics.ts +++ b/packages/core/state/interaction/logics.ts @@ -31,8 +31,8 @@ import { SetIsSmallScreenAction, setVisibleModal, hideVisibleModal, - MoveFilesAction, - MOVE_FILES, + CopyFilesAction, + COPY_FILES, } from "./actions"; import * as interactionSelectors from "./selectors"; import { DownloadResolution, FileInfo } from "../../services/FileDownloadService"; @@ -578,16 +578,16 @@ const setIsSmallScreen = createLogic({ }); /** - * Interceptor responsible for handling the MOVE_FILES action. - * Logs details of files that are being moved. + * Interceptor responsible for handling the COPY_FILES action. + * Logs details of files that are being copied to cache. */ -const moveFilesLogic = createLogic({ +const copyFilesLogic = createLogic({ async process({ action, getState }: ReduxLogicDeps, dispatch, done) { try { const httpFileService = interactionSelectors.getHttpFileService(getState()); const username = interactionSelectors.getUserName(getState()); - const fileDetails = (action as MoveFilesAction).payload.fileDetails; + const fileDetails = (action as CopyFilesAction).payload.fileDetails; // Map file IDs to file names for easy lookup const fileIdToNameMap = Object.fromEntries( @@ -652,13 +652,16 @@ const moveFilesLogic = createLogic({ } catch (err) { // Service call itself fails dispatch( - interaction.actions.processError("moveFilesFailure", `Failed to cache files, details: ${(err as Error).message}.`) + interaction.actions.processError( + "moveFilesFailure", + `Failed to cache files, details: ${(err as Error).message}.` + ) ); } finally { done(); } }, - type: MOVE_FILES, + type: COPY_FILES, }); export default [ @@ -672,5 +675,5 @@ export default [ showContextMenu, refresh, setIsSmallScreen, - moveFilesLogic, + copyFilesLogic, ]; diff --git a/packages/core/state/interaction/reducer.ts b/packages/core/state/interaction/reducer.ts index 735b8663..4439e70e 100644 --- a/packages/core/state/interaction/reducer.ts +++ b/packages/core/state/interaction/reducer.ts @@ -15,7 +15,7 @@ import { SHOW_CONTEXT_MENU, SHOW_DATASET_DETAILS_PANEL, SHOW_MANIFEST_DOWNLOAD_DIALOG, - SHOW_MOVE_FILE_MANIFEST, + SHOW_COPY_FILE_MANIFEST, StatusUpdate, MARK_AS_USED_APPLICATION_BEFORE, MARK_AS_DISMISSED_SMALL_SCREEN_WARNING, @@ -196,9 +196,9 @@ export default makeReducer( ...state, selectedPublicDataset: action.payload, }), - [SHOW_MOVE_FILE_MANIFEST]: (state) => ({ + [SHOW_COPY_FILE_MANIFEST]: (state) => ({ ...state, - visibleModal: ModalType.MoveFileManifest, + visibleModal: ModalType.CopyFileManifest, }), }, initialState