From 1d8d7220ca4a104c5a542de61a10be4aa8137f0a Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Wed, 23 Oct 2024 12:24:55 -0700 Subject: [PATCH 01/56] feature/local_cloud_toggle --- .../core/hooks/useFileAccessContextMenu.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/packages/core/hooks/useFileAccessContextMenu.ts b/packages/core/hooks/useFileAccessContextMenu.ts index cd79b3bb..0c79a321 100644 --- a/packages/core/hooks/useFileAccessContextMenu.ts +++ b/packages/core/hooks/useFileAccessContextMenu.ts @@ -147,6 +147,37 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => { dispatch(interaction.actions.downloadFiles()); }, }, + { + key: "move-files", + text: "Move Files", + title: "Move files between NAS Cache", + disabled: !filters && fileSelection.count() === 0, + iconProps: { + iconName: "MoveToFolder", + }, + subMenuProps: { + items: [ + { + key: "off-nas", + text: "Off NAS Cache", + title: "Move files off the NAS cache", + onClick() { + // Placeholder for moving files off NAS Cache + console.log("Move files off NAS Cache"); + }, + }, + { + key: "onto-nas", + text: "Onto NAS Cache", + title: "Move files onto the NAS cache", + onClick() { + // Placeholder for moving files onto NAS Cache + console.log("Move files onto NAS Cache"); + }, + }, + ], + }, + }, ]; dispatch( From f2a05596f3b20477304c849f4575086baa3a1581 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Fri, 25 Oct 2024 13:08:37 -0700 Subject: [PATCH 02/56] feature/toggle-modal --- .../MoveFileManifest.module.css | 23 +++++ .../Modal/MoveFileManifest/index.tsx | 97 +++++++++++++++++++ packages/core/components/Modal/index.tsx | 4 + .../core/hooks/useFileAccessContextMenu.ts | 12 ++- packages/core/state/interaction/actions.ts | 24 +++++ packages/core/state/interaction/reducer.ts | 13 +++ packages/core/state/interaction/selectors.ts | 1 + 7 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css create mode 100644 packages/core/components/Modal/MoveFileManifest/index.tsx diff --git a/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css b/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css new file mode 100644 index 00000000..01b3af96 --- /dev/null +++ b/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css @@ -0,0 +1,23 @@ +.fileListContainer { + max-height: 200px; + overflow-y: auto; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; +} + +.fileList { + list-style-type: none; + margin: 0; + padding: 0; +} + +.fileItem { + margin-bottom: 4px; + white-space: nowrap; +} + + +.confirmButton { + margin-top: 10px; +} diff --git a/packages/core/components/Modal/MoveFileManifest/index.tsx b/packages/core/components/Modal/MoveFileManifest/index.tsx new file mode 100644 index 00000000..63e5d4cc --- /dev/null +++ b/packages/core/components/Modal/MoveFileManifest/index.tsx @@ -0,0 +1,97 @@ +import * as React from "react"; +import { useSelector } from "react-redux"; + +import { ModalProps } from ".."; +import BaseModal from "../BaseModal"; +import { PrimaryButton } from "../../Buttons"; +import { interaction, selection } from "../../../state"; +import FileDetail from "../../../entity/FileDetail"; +import styles from "./MoveFileManifest.module.css"; +import FileSelection from "../../../entity/FileSelection"; + +/** + * Modal overlay for displaying details of selected files for NAS cache operations. + */ +export default function MoveFileManifest({ onDismiss }: ModalProps) { + // const dispatch = useDispatch(); //TODO: add onMove functionality + const fileSelection = useSelector( + selection.selectors.getFileSelection, + FileSelection.selectionsAreEqual + ); + const moveFileTarget = useSelector(interaction.selectors.getMoveFileTarget); + + const [fileDetails, setFileDetails] = React.useState([]); + const [totalSize, setTotalSize] = React.useState(0); + + React.useEffect(() => { + async function fetchDetails() { + const details = await fileSelection.fetchAllDetails(); + setFileDetails(details); + const totalFileSize = details.reduce((acc, file) => acc + (file.size || 0), 0); + setTotalSize(totalFileSize); + } + + fetchDetails(); + }, [fileSelection]); + + const onMove = () => { + console.log( + `Moving ${fileDetails.length} files ${ + moveFileTarget === "ON_TO_NAS" ? "onto" : "off of" + } NAS.` + ); + onDismiss(); + }; + + const body = ( +
+

Selected Files:

+
+
    + {fileDetails.map((file) => ( +
  • + {file.name} - {formatFileSize(file.size || 0)} +
  • + ))} +
+
+

Total Files: {fileDetails.length}

+

Total Size: {formatFileSize(totalSize)}

+
+ ); + + return ( + + } + onDismiss={onDismiss} + title={`Move Files ${moveFileTarget === "ON_TO_NAS" ? "onto" : "off of"} NAS Cache`} + /> + ); +} + +/** + * Formats a file size to a human-readable string. + */ +const formatFileSize = (size: number) => { + if (size < 1024) return `${size} B`; + const units = ["KB", "MB", "GB", "TB"]; + let unitIndex = -1; + let formattedSize = size; + + do { + formattedSize /= 1024; + unitIndex++; + } while (formattedSize >= 1024 && unitIndex < units.length - 1); + + return `${formattedSize.toFixed(2)} ${units[unitIndex]}`; +}; diff --git a/packages/core/components/Modal/index.tsx b/packages/core/components/Modal/index.tsx index e05a4bc6..66f998f5 100644 --- a/packages/core/components/Modal/index.tsx +++ b/packages/core/components/Modal/index.tsx @@ -6,6 +6,7 @@ import CodeSnippet from "./CodeSnippet"; import DataSource from "./DataSource"; import MetadataManifest from "./MetadataManifest"; import SmallScreenWarning from "./SmallScreenWarning"; +import MoveFileManifest from "./MoveFileManifest"; export interface ModalProps { onDismiss: () => void; @@ -16,6 +17,7 @@ export enum ModalType { DataSource = 2, MetadataManifest = 3, SmallScreenWarning = 4, + MoveFileManifest = 5, } /** @@ -38,6 +40,8 @@ export default function Modal() { return ; case ModalType.SmallScreenWarning: return ; + case ModalType.MoveFileManifest: + return ; default: return null; } diff --git a/packages/core/hooks/useFileAccessContextMenu.ts b/packages/core/hooks/useFileAccessContextMenu.ts index 0c79a321..2d32bea0 100644 --- a/packages/core/hooks/useFileAccessContextMenu.ts +++ b/packages/core/hooks/useFileAccessContextMenu.ts @@ -157,13 +157,18 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => { }, subMenuProps: { items: [ + { + key: "move-files-title", + text: "CACHE LOCATION", + title: "Move files to or from the NAS cache", + itemType: ContextualMenuItemType.Header, + }, { key: "off-nas", text: "Off NAS Cache", title: "Move files off the NAS cache", onClick() { - // Placeholder for moving files off NAS Cache - console.log("Move files off NAS Cache"); + dispatch(interaction.actions.showMoveFileManifest("OFF_NAS")); }, }, { @@ -171,8 +176,7 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => { text: "Onto NAS Cache", title: "Move files onto the NAS cache", onClick() { - // Placeholder for moving files onto NAS Cache - console.log("Move files onto NAS Cache"); + dispatch(interaction.actions.showMoveFileManifest("ON_TO_NAS")); }, }, ], diff --git a/packages/core/state/interaction/actions.ts b/packages/core/state/interaction/actions.ts index 441dca1e..46efbe5e 100644 --- a/packages/core/state/interaction/actions.ts +++ b/packages/core/state/interaction/actions.ts @@ -679,3 +679,27 @@ export function setSelectedPublicDataset(dataset: PublicDataset): SetSelectedPub type: SET_SELECTED_PUBLIC_DATASET, }; } + +/** + * SHOW_MOVE_FILE_MANIFEST + * + * Action to show the Move File dialog (manifest) for NAS cache operations. + * This modal will allow users to move files on or off the NAS cache. + */ +export const SHOW_MOVE_FILE_MANIFEST = makeConstant(STATE_BRANCH_NAME, "show-move-file-manifest"); + +export interface ShowMoveFileManifestAction { + type: string; + payload: { + target: "ON_TO_NAS" | "OFF_NAS"; + }; +} + +export function showMoveFileManifest(target: "ON_TO_NAS" | "OFF_NAS"): ShowMoveFileManifestAction { + return { + type: SHOW_MOVE_FILE_MANIFEST, + payload: { + target, + }, + }; +} diff --git a/packages/core/state/interaction/reducer.ts b/packages/core/state/interaction/reducer.ts index 4a8905f7..4a069a9c 100644 --- a/packages/core/state/interaction/reducer.ts +++ b/packages/core/state/interaction/reducer.ts @@ -15,10 +15,12 @@ import { SHOW_CONTEXT_MENU, SHOW_DATASET_DETAILS_PANEL, SHOW_MANIFEST_DOWNLOAD_DIALOG, + SHOW_MOVE_FILE_MANIFEST, StatusUpdate, MARK_AS_USED_APPLICATION_BEFORE, MARK_AS_DISMISSED_SMALL_SCREEN_WARNING, ShowManifestDownloadDialogAction, + ShowMoveFileManifestAction, SET_IS_AICS_EMPLOYEE, PROMPT_FOR_DATA_SOURCE, DownloadManifestAction, @@ -57,6 +59,7 @@ export interface InteractionStateBranch { hasUsedApplicationBefore: boolean; isAicsEmployee?: boolean; isOnWeb: boolean; + moveFileTarget?: "ON_TO_NAS" | "OFF_NAS"; platformDependentServices: PlatformDependentServices; refreshKey?: string; selectedPublicDataset?: PublicDataset; @@ -195,6 +198,16 @@ export default makeReducer( ...state, selectedPublicDataset: action.payload, }), + [SHOW_MOVE_FILE_MANIFEST]: (state, action: ShowMoveFileManifestAction) => ({ + ...state, + visibleModal: ModalType.MoveFileManifest, + moveFileTarget: action.payload.target, + }), + [HIDE_VISIBLE_MODAL]: (state) => ({ + ...state, + visibleModal: undefined, + moveFileTarget: undefined, + }), }, initialState ); diff --git a/packages/core/state/interaction/selectors.ts b/packages/core/state/interaction/selectors.ts index ec880228..bf7096df 100644 --- a/packages/core/state/interaction/selectors.ts +++ b/packages/core/state/interaction/selectors.ts @@ -47,6 +47,7 @@ export const getUserSelectedApplications = (state: State) => state.interaction.userSelectedApplications; export const getVisibleModal = (state: State) => state.interaction.visibleModal; export const isAicsEmployee = (state: State) => state.interaction.isAicsEmployee; +export const getMoveFileTarget = (state: State) => state.interaction.moveFileTarget; // COMPOSED SELECTORS export const getApplicationVersion = createSelector( From f52d31c7091c7f8f43dfd4fd1af3aabe80ec94fe Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 28 Oct 2024 11:11:31 -0700 Subject: [PATCH 03/56] add conditional show move menu item --- .../core/hooks/useFileAccessContextMenu.ts | 82 +++++++++++-------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/packages/core/hooks/useFileAccessContextMenu.ts b/packages/core/hooks/useFileAccessContextMenu.ts index 2d32bea0..b58261d5 100644 --- a/packages/core/hooks/useFileAccessContextMenu.ts +++ b/packages/core/hooks/useFileAccessContextMenu.ts @@ -147,41 +147,53 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => { dispatch(interaction.actions.downloadFiles()); }, }, - { - key: "move-files", - text: "Move Files", - title: "Move files between NAS Cache", - disabled: !filters && fileSelection.count() === 0, - iconProps: { - iconName: "MoveToFolder", - }, - subMenuProps: { - items: [ - { - key: "move-files-title", - text: "CACHE LOCATION", - title: "Move files to or from the NAS cache", - itemType: ContextualMenuItemType.Header, - }, - { - key: "off-nas", - text: "Off NAS Cache", - title: "Move files off the NAS cache", - onClick() { - dispatch(interaction.actions.showMoveFileManifest("OFF_NAS")); - }, - }, - { - key: "onto-nas", - text: "Onto NAS Cache", - title: "Move files onto the NAS cache", - onClick() { - dispatch(interaction.actions.showMoveFileManifest("ON_TO_NAS")); - }, - }, - ], - }, - }, + ...(isQueryingAicsFms + ? [ + { + key: "move-files", + text: "Move Files", + title: "Move files between NAS Cache", + disabled: !filters && fileSelection.count() === 0, + iconProps: { + iconName: "MoveToFolder", + }, + subMenuProps: { + items: [ + { + key: "move-files-title", + text: "CACHE LOCATION", + title: "Move files to or from the NAS cache", + itemType: ContextualMenuItemType.Header, + }, + { + key: "off-nas", + text: "Off NAS Cache", + title: "Move files off the NAS cache", + onClick() { + dispatch( + interaction.actions.showMoveFileManifest( + "OFF_NAS" + ) + ); + }, + }, + { + key: "onto-nas", + text: "Onto NAS Cache", + title: "Move files onto the NAS cache", + onClick() { + dispatch( + interaction.actions.showMoveFileManifest( + "ON_TO_NAS" + ) + ); + }, + }, + ], + }, + }, + ] + : []), ]; dispatch( From 77a52bd92652b8a0f7f0eeb5ed43216f73ff3045 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 28 Oct 2024 11:23:58 -0700 Subject: [PATCH 04/56] modal list to table --- .../MoveFileManifest.module.css | 44 ++++++++++++++----- .../Modal/MoveFileManifest/index.tsx | 25 +++++++---- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css b/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css index 01b3af96..4252eb86 100644 --- a/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css +++ b/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css @@ -1,23 +1,43 @@ -.fileListContainer { - max-height: 200px; +.fileTableContainer { + max-height: 300px; overflow-y: auto; - padding: 8px; border: 1px solid #ddd; - border-radius: 4px; + margin-bottom: 1em; } -.fileList { - list-style-type: none; - margin: 0; - padding: 0; +.fileTable { + width: 100%; + border-collapse: collapse; } -.fileItem { - margin-bottom: 4px; +.fileTable th, +.fileTable td { + padding: 8px; + text-align: left; + border-bottom: 1px solid #ddd; white-space: nowrap; } +.fileTable th { + background-color: #f2f2f2; + position: sticky; + top: 0; + z-index: 1; +} + +.fileTable td:first-child { + border-right: 1px solid #ddd; +} + +.fileTableContainer::-webkit-scrollbar { + width: 8px; +} + +.fileTableContainer::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 4px; +} -.confirmButton { - margin-top: 10px; +.fileTableContainer::-webkit-scrollbar-thumb:hover { + background-color: #555; } diff --git a/packages/core/components/Modal/MoveFileManifest/index.tsx b/packages/core/components/Modal/MoveFileManifest/index.tsx index 63e5d4cc..166c1352 100644 --- a/packages/core/components/Modal/MoveFileManifest/index.tsx +++ b/packages/core/components/Modal/MoveFileManifest/index.tsx @@ -46,14 +46,23 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { const body = (

Selected Files:

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

Total Files: {fileDetails.length}

Total Size: {formatFileSize(totalSize)}

From 1201a846f67e846b16e1c4adc2b738813713e2f7 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 28 Oct 2024 11:26:02 -0700 Subject: [PATCH 05/56] reorder imports --- packages/core/components/Modal/MoveFileManifest/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/components/Modal/MoveFileManifest/index.tsx b/packages/core/components/Modal/MoveFileManifest/index.tsx index 166c1352..5d59b0a2 100644 --- a/packages/core/components/Modal/MoveFileManifest/index.tsx +++ b/packages/core/components/Modal/MoveFileManifest/index.tsx @@ -4,10 +4,11 @@ import { useSelector } from "react-redux"; import { ModalProps } from ".."; import BaseModal from "../BaseModal"; import { PrimaryButton } from "../../Buttons"; -import { interaction, selection } from "../../../state"; import FileDetail from "../../../entity/FileDetail"; -import styles from "./MoveFileManifest.module.css"; 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. From ae06c75ecf44f37d7f577e30a61ce146c19f0d27 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 28 Oct 2024 11:41:26 -0700 Subject: [PATCH 06/56] use existing filesize method --- .../Modal/MoveFileManifest/index.tsx | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/packages/core/components/Modal/MoveFileManifest/index.tsx b/packages/core/components/Modal/MoveFileManifest/index.tsx index 5d59b0a2..1c472957 100644 --- a/packages/core/components/Modal/MoveFileManifest/index.tsx +++ b/packages/core/components/Modal/MoveFileManifest/index.tsx @@ -1,3 +1,4 @@ +import filesize from "filesize"; import * as React from "react"; import { useSelector } from "react-redux"; @@ -15,6 +16,7 @@ import styles from "./MoveFileManifest.module.css"; */ export default function MoveFileManifest({ onDismiss }: ModalProps) { // const dispatch = useDispatch(); //TODO: add onMove functionality + const fileService = useSelector(interaction.selectors.getFileService); const fileSelection = useSelector( selection.selectors.getFileSelection, FileSelection.selectionsAreEqual @@ -22,18 +24,23 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { const moveFileTarget = useSelector(interaction.selectors.getMoveFileTarget); const [fileDetails, setFileDetails] = React.useState([]); - const [totalSize, setTotalSize] = React.useState(0); + 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 totalFileSize = details.reduce((acc, file) => acc + (file.size || 0), 0); - setTotalSize(totalFileSize); + + const aggregateInfo = await fileService.getAggregateInformation(fileSelection); + const formattedSize = aggregateInfo.size ? filesize(aggregateInfo.size) : undefined; + setTotalSize(formattedSize); + setLoading(false); } fetchDetails(); - }, [fileSelection]); + }, [fileSelection, fileService]); const onMove = () => { console.log( @@ -59,14 +66,14 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { {fileDetails.map((file) => ( {file.name} - {formatFileSize(file.size || 0)} + {filesize(file.size || 0)} ))}

Total Files: {fileDetails.length}

-

Total Size: {formatFileSize(totalSize)}

+

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

); @@ -88,20 +95,3 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { /> ); } - -/** - * Formats a file size to a human-readable string. - */ -const formatFileSize = (size: number) => { - if (size < 1024) return `${size} B`; - const units = ["KB", "MB", "GB", "TB"]; - let unitIndex = -1; - let formattedSize = size; - - do { - formattedSize /= 1024; - unitIndex++; - } while (formattedSize >= 1024 && unitIndex < units.length - 1); - - return `${formattedSize.toFixed(2)} ${units[unitIndex]}`; -}; From e1594389fa5dbe6648cf9d9208c74ad9ea5fbbf3 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 28 Oct 2024 12:10:38 -0700 Subject: [PATCH 07/56] add placeholder action --- .../Modal/MoveFileManifest/index.tsx | 18 ++++++++------- packages/core/state/interaction/actions.ts | 23 +++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/core/components/Modal/MoveFileManifest/index.tsx b/packages/core/components/Modal/MoveFileManifest/index.tsx index 1c472957..08b5196a 100644 --- a/packages/core/components/Modal/MoveFileManifest/index.tsx +++ b/packages/core/components/Modal/MoveFileManifest/index.tsx @@ -1,6 +1,6 @@ import filesize from "filesize"; import * as React from "react"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { ModalProps } from ".."; import BaseModal from "../BaseModal"; @@ -15,7 +15,7 @@ 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(); //TODO: add onMove functionality + const dispatch = useDispatch(); const fileService = useSelector(interaction.selectors.getFileService); const fileSelection = useSelector( selection.selectors.getFileSelection, @@ -43,12 +43,14 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { }, [fileSelection, fileService]); const onMove = () => { - console.log( - `Moving ${fileDetails.length} files ${ - moveFileTarget === "ON_TO_NAS" ? "onto" : "off of" - } NAS.` - ); - onDismiss(); + if (moveFileTarget) { + dispatch(interaction.actions.moveFiles(fileDetails, moveFileTarget)); + onDismiss(); + } else { + console.warn( + "Move file target location is undefined. Cannot proceed with moving files." + ); + } }; const body = ( diff --git a/packages/core/state/interaction/actions.ts b/packages/core/state/interaction/actions.ts index 46efbe5e..5daf3512 100644 --- a/packages/core/state/interaction/actions.ts +++ b/packages/core/state/interaction/actions.ts @@ -703,3 +703,26 @@ export function showMoveFileManifest(target: "ON_TO_NAS" | "OFF_NAS"): ShowMoveF }, }; } + +export const MOVE_FILES = makeConstant(STATE_BRANCH_NAME, "move-files"); + +export interface MoveFilesAction { + type: string; + payload: { + fileDetails: FileDetail[]; + target: string; + }; +} + +export function moveFiles(fileDetails: FileDetail[], target: string): MoveFilesAction { + console.log( + `Moving ${fileDetails.length} files ${target === "ON_TO_NAS" ? "onto" : "off of"} NAS.` + ); + return { + type: MOVE_FILES, + payload: { + fileDetails, + target, + }, + }; +} From 8c810a631d8892a318daba8284aa6585d91952d0 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 28 Oct 2024 12:19:37 -0700 Subject: [PATCH 08/56] add placeholder logic --- packages/core/state/interaction/actions.ts | 3 --- packages/core/state/interaction/logics.ts | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/core/state/interaction/actions.ts b/packages/core/state/interaction/actions.ts index 5daf3512..a2994b1c 100644 --- a/packages/core/state/interaction/actions.ts +++ b/packages/core/state/interaction/actions.ts @@ -715,9 +715,6 @@ export interface MoveFilesAction { } export function moveFiles(fileDetails: FileDetail[], target: string): MoveFilesAction { - console.log( - `Moving ${fileDetails.length} files ${target === "ON_TO_NAS" ? "onto" : "off of"} NAS.` - ); return { type: MOVE_FILES, payload: { diff --git a/packages/core/state/interaction/logics.ts b/packages/core/state/interaction/logics.ts index 9c7eb4eb..4a882aa5 100644 --- a/packages/core/state/interaction/logics.ts +++ b/packages/core/state/interaction/logics.ts @@ -31,6 +31,8 @@ import { SetIsSmallScreenAction, setVisibleModal, hideVisibleModal, + MoveFilesAction, + MOVE_FILES, } from "./actions"; import * as interactionSelectors from "./selectors"; import { DownloadResolution, FileInfo } from "../../services/FileDownloadService"; @@ -575,6 +577,19 @@ const setIsSmallScreen = createLogic({ type: SET_IS_SMALL_SCREEN, }); +/** + * Interceptor responsible for handling the MOVE_FILES action. + * Logs the target location for file movement in the console. + */ +const moveFilesLogic = createLogic({ + type: MOVE_FILES, + process(deps, dispatch, done) { + const action = deps.action as MoveFilesAction; + console.log(`Moving files to location: ${action.payload.target}`); + done(); + }, +}); + export default [ initializeApp, downloadManifest, @@ -586,4 +601,5 @@ export default [ showContextMenu, refresh, setIsSmallScreen, + moveFilesLogic, ]; From 1e3c38bfdd76d177129d05b25cc8fb37bc9aec84 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 28 Oct 2024 12:23:31 -0700 Subject: [PATCH 09/56] remove duplicate reducer --- packages/core/state/interaction/reducer.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/core/state/interaction/reducer.ts b/packages/core/state/interaction/reducer.ts index 4a069a9c..9bbdc83d 100644 --- a/packages/core/state/interaction/reducer.ts +++ b/packages/core/state/interaction/reducer.ts @@ -203,11 +203,6 @@ export default makeReducer( visibleModal: ModalType.MoveFileManifest, moveFileTarget: action.payload.target, }), - [HIDE_VISIBLE_MODAL]: (state) => ({ - ...state, - visibleModal: undefined, - moveFileTarget: undefined, - }), }, initialState ); From 588447da65efe53f1b4eebc5459cc97f4b185646 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 28 Oct 2024 13:48:33 -0700 Subject: [PATCH 10/56] update styling for move modal --- .../MoveFileManifest.module.css | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css b/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css index 4252eb86..df5280ae 100644 --- a/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css +++ b/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css @@ -1,43 +1,31 @@ -.fileTableContainer { +.file-table-container { max-height: 300px; overflow-y: auto; - border: 1px solid #ddd; - margin-bottom: 1em; + border: 1px solid var(--border-color); + margin-bottom: var(--margin); } -.fileTable { +.file-table { width: 100%; border-collapse: collapse; } -.fileTable th, -.fileTable td { +.file-table th, +.file-table td { padding: 8px; text-align: left; - border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--border-color); white-space: nowrap; + color: var(--primary-text-color); } -.fileTable th { - background-color: #f2f2f2; +.file-table th { + background-color: var(--secondary-background-color); position: sticky; top: 0; z-index: 1; } -.fileTable td:first-child { - border-right: 1px solid #ddd; -} - -.fileTableContainer::-webkit-scrollbar { - width: 8px; -} - -.fileTableContainer::-webkit-scrollbar-thumb { - background-color: #888; - border-radius: 4px; -} - -.fileTableContainer::-webkit-scrollbar-thumb:hover { - background-color: #555; +.file-table td:first-child { + border-right: 1px solid var(--border-color); } From 741d4156b5d9b26680909984d437ad9a63bc3480 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 28 Oct 2024 14:18:54 -0700 Subject: [PATCH 11/56] remove off nas --- packages/core/hooks/useFileAccessContextMenu.ts | 16 +--------------- packages/core/state/interaction/actions.ts | 4 ++-- packages/core/state/interaction/reducer.ts | 2 +- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/packages/core/hooks/useFileAccessContextMenu.ts b/packages/core/hooks/useFileAccessContextMenu.ts index b58261d5..d3917325 100644 --- a/packages/core/hooks/useFileAccessContextMenu.ts +++ b/packages/core/hooks/useFileAccessContextMenu.ts @@ -165,27 +165,13 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => { title: "Move files to or from the NAS cache", itemType: ContextualMenuItemType.Header, }, - { - key: "off-nas", - text: "Off NAS Cache", - title: "Move files off the NAS cache", - onClick() { - dispatch( - interaction.actions.showMoveFileManifest( - "OFF_NAS" - ) - ); - }, - }, { key: "onto-nas", text: "Onto NAS Cache", title: "Move files onto the NAS cache", onClick() { dispatch( - interaction.actions.showMoveFileManifest( - "ON_TO_NAS" - ) + interaction.actions.showMoveFileManifest("NAS") ); }, }, diff --git a/packages/core/state/interaction/actions.ts b/packages/core/state/interaction/actions.ts index a2994b1c..9d840068 100644 --- a/packages/core/state/interaction/actions.ts +++ b/packages/core/state/interaction/actions.ts @@ -691,11 +691,11 @@ export const SHOW_MOVE_FILE_MANIFEST = makeConstant(STATE_BRANCH_NAME, "show-mov export interface ShowMoveFileManifestAction { type: string; payload: { - target: "ON_TO_NAS" | "OFF_NAS"; + target: string; }; } -export function showMoveFileManifest(target: "ON_TO_NAS" | "OFF_NAS"): ShowMoveFileManifestAction { +export function showMoveFileManifest(target: string): ShowMoveFileManifestAction { return { type: SHOW_MOVE_FILE_MANIFEST, payload: { diff --git a/packages/core/state/interaction/reducer.ts b/packages/core/state/interaction/reducer.ts index 9bbdc83d..07655889 100644 --- a/packages/core/state/interaction/reducer.ts +++ b/packages/core/state/interaction/reducer.ts @@ -59,7 +59,7 @@ export interface InteractionStateBranch { hasUsedApplicationBefore: boolean; isAicsEmployee?: boolean; isOnWeb: boolean; - moveFileTarget?: "ON_TO_NAS" | "OFF_NAS"; + moveFileTarget?: string; platformDependentServices: PlatformDependentServices; refreshKey?: string; selectedPublicDataset?: PublicDataset; From 1bc859fc0ecc021de5b18b02a10e656a50a92322 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Tue, 29 Oct 2024 12:08:10 -0700 Subject: [PATCH 12/56] update move only structure --- .../Modal/MoveFileManifest/index.tsx | 2 +- .../core/hooks/useFileAccessContextMenu.ts | 31 ++++--------------- packages/core/state/interaction/actions.ts | 2 +- 3 files changed, 8 insertions(+), 27 deletions(-) diff --git a/packages/core/components/Modal/MoveFileManifest/index.tsx b/packages/core/components/Modal/MoveFileManifest/index.tsx index 08b5196a..de9fac41 100644 --- a/packages/core/components/Modal/MoveFileManifest/index.tsx +++ b/packages/core/components/Modal/MoveFileManifest/index.tsx @@ -93,7 +93,7 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { /> } onDismiss={onDismiss} - title={`Move Files ${moveFileTarget === "ON_TO_NAS" ? "onto" : "off of"} NAS Cache`} + title={`Move Files to ${moveFileTarget}`} /> ); } diff --git a/packages/core/hooks/useFileAccessContextMenu.ts b/packages/core/hooks/useFileAccessContextMenu.ts index d3917325..7381316f 100644 --- a/packages/core/hooks/useFileAccessContextMenu.ts +++ b/packages/core/hooks/useFileAccessContextMenu.ts @@ -150,32 +150,13 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => { ...(isQueryingAicsFms ? [ { - key: "move-files", - text: "Move Files", - title: "Move files between NAS Cache", + key: "move-to-cache", + text: "Move to Cache", + title: "Move selected files to NAS Cache", disabled: !filters && fileSelection.count() === 0, - iconProps: { - iconName: "MoveToFolder", - }, - subMenuProps: { - items: [ - { - key: "move-files-title", - text: "CACHE LOCATION", - title: "Move files to or from the NAS cache", - itemType: ContextualMenuItemType.Header, - }, - { - key: "onto-nas", - text: "Onto NAS Cache", - title: "Move files onto the NAS cache", - onClick() { - dispatch( - interaction.actions.showMoveFileManifest("NAS") - ); - }, - }, - ], + iconProps: { iconName: "MoveToFolder" }, + onClick() { + dispatch(interaction.actions.showMoveFileManifest("NAS")); }, }, ] diff --git a/packages/core/state/interaction/actions.ts b/packages/core/state/interaction/actions.ts index 9d840068..7c9d79ca 100644 --- a/packages/core/state/interaction/actions.ts +++ b/packages/core/state/interaction/actions.ts @@ -684,7 +684,7 @@ export function setSelectedPublicDataset(dataset: PublicDataset): SetSelectedPub * SHOW_MOVE_FILE_MANIFEST * * Action to show the Move File dialog (manifest) for NAS cache operations. - * This modal will allow users to move files on or off the NAS cache. + * This modal will allow users to move files onto the NAS cache. */ export const SHOW_MOVE_FILE_MANIFEST = makeConstant(STATE_BRANCH_NAME, "show-move-file-manifest"); From 034549de1962d08622e9c4e1b8ea409741103aa5 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Thu, 31 Oct 2024 15:56:36 -0700 Subject: [PATCH 13/56] remove loc target --- .../Modal/MoveFileManifest/index.tsx | 13 +++------- .../core/hooks/useFileAccessContextMenu.ts | 26 +++++++++---------- packages/core/state/interaction/actions.ts | 12 ++------- packages/core/state/interaction/logics.ts | 4 +-- packages/core/state/interaction/reducer.ts | 5 +--- packages/core/state/interaction/selectors.ts | 1 - 6 files changed, 21 insertions(+), 40 deletions(-) diff --git a/packages/core/components/Modal/MoveFileManifest/index.tsx b/packages/core/components/Modal/MoveFileManifest/index.tsx index de9fac41..41611686 100644 --- a/packages/core/components/Modal/MoveFileManifest/index.tsx +++ b/packages/core/components/Modal/MoveFileManifest/index.tsx @@ -21,7 +21,6 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { selection.selectors.getFileSelection, FileSelection.selectionsAreEqual ); - const moveFileTarget = useSelector(interaction.selectors.getMoveFileTarget); const [fileDetails, setFileDetails] = React.useState([]); const [totalSize, setTotalSize] = React.useState(); @@ -43,14 +42,8 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { }, [fileSelection, fileService]); const onMove = () => { - if (moveFileTarget) { - dispatch(interaction.actions.moveFiles(fileDetails, moveFileTarget)); - onDismiss(); - } else { - console.warn( - "Move file target location is undefined. Cannot proceed with moving files." - ); - } + dispatch(interaction.actions.moveFiles(fileDetails)); + onDismiss(); }; const body = ( @@ -93,7 +86,7 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { /> } onDismiss={onDismiss} - title={`Move Files to ${moveFileTarget}`} + title="Move Files to NAS Cache" /> ); } diff --git a/packages/core/hooks/useFileAccessContextMenu.ts b/packages/core/hooks/useFileAccessContextMenu.ts index 7381316f..372c25e1 100644 --- a/packages/core/hooks/useFileAccessContextMenu.ts +++ b/packages/core/hooks/useFileAccessContextMenu.ts @@ -135,18 +135,6 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => { ], }, }, - { - key: "download", - text: "Download", - title: "Download selected files to a specific directory", - disabled: !filters && fileSelection.count() === 0, - iconProps: { - iconName: "Download", - }, - onClick() { - dispatch(interaction.actions.downloadFiles()); - }, - }, ...(isQueryingAicsFms ? [ { @@ -156,11 +144,23 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => { disabled: !filters && fileSelection.count() === 0, iconProps: { iconName: "MoveToFolder" }, onClick() { - dispatch(interaction.actions.showMoveFileManifest("NAS")); + dispatch(interaction.actions.showMoveFileManifest()); }, }, ] : []), + { + key: "download", + text: "Download", + title: "Download selected files to a specific directory", + disabled: !filters && fileSelection.count() === 0, + iconProps: { + iconName: "Download", + }, + onClick() { + dispatch(interaction.actions.downloadFiles()); + }, + }, ]; dispatch( diff --git a/packages/core/state/interaction/actions.ts b/packages/core/state/interaction/actions.ts index 7c9d79ca..7f85149c 100644 --- a/packages/core/state/interaction/actions.ts +++ b/packages/core/state/interaction/actions.ts @@ -690,17 +690,11 @@ export const SHOW_MOVE_FILE_MANIFEST = makeConstant(STATE_BRANCH_NAME, "show-mov export interface ShowMoveFileManifestAction { type: string; - payload: { - target: string; - }; } -export function showMoveFileManifest(target: string): ShowMoveFileManifestAction { +export function showMoveFileManifest(): ShowMoveFileManifestAction { return { type: SHOW_MOVE_FILE_MANIFEST, - payload: { - target, - }, }; } @@ -710,16 +704,14 @@ export interface MoveFilesAction { type: string; payload: { fileDetails: FileDetail[]; - target: string; }; } -export function moveFiles(fileDetails: FileDetail[], target: string): MoveFilesAction { +export function moveFiles(fileDetails: FileDetail[]): MoveFilesAction { return { type: MOVE_FILES, payload: { fileDetails, - target, }, }; } diff --git a/packages/core/state/interaction/logics.ts b/packages/core/state/interaction/logics.ts index 4a882aa5..3e70a360 100644 --- a/packages/core/state/interaction/logics.ts +++ b/packages/core/state/interaction/logics.ts @@ -579,13 +579,13 @@ const setIsSmallScreen = createLogic({ /** * Interceptor responsible for handling the MOVE_FILES action. - * Logs the target location for file movement in the console. + * Logs details of files that are being moved. */ const moveFilesLogic = createLogic({ type: MOVE_FILES, process(deps, dispatch, done) { const action = deps.action as MoveFilesAction; - console.log(`Moving files to location: ${action.payload.target}`); + console.log(`Moving files:`, action.payload.fileDetails); done(); }, }); diff --git a/packages/core/state/interaction/reducer.ts b/packages/core/state/interaction/reducer.ts index 07655889..2f39ea7e 100644 --- a/packages/core/state/interaction/reducer.ts +++ b/packages/core/state/interaction/reducer.ts @@ -20,7 +20,6 @@ import { MARK_AS_USED_APPLICATION_BEFORE, MARK_AS_DISMISSED_SMALL_SCREEN_WARNING, ShowManifestDownloadDialogAction, - ShowMoveFileManifestAction, SET_IS_AICS_EMPLOYEE, PROMPT_FOR_DATA_SOURCE, DownloadManifestAction, @@ -59,7 +58,6 @@ export interface InteractionStateBranch { hasUsedApplicationBefore: boolean; isAicsEmployee?: boolean; isOnWeb: boolean; - moveFileTarget?: string; platformDependentServices: PlatformDependentServices; refreshKey?: string; selectedPublicDataset?: PublicDataset; @@ -198,10 +196,9 @@ export default makeReducer( ...state, selectedPublicDataset: action.payload, }), - [SHOW_MOVE_FILE_MANIFEST]: (state, action: ShowMoveFileManifestAction) => ({ + [SHOW_MOVE_FILE_MANIFEST]: (state) => ({ ...state, visibleModal: ModalType.MoveFileManifest, - moveFileTarget: action.payload.target, }), }, initialState diff --git a/packages/core/state/interaction/selectors.ts b/packages/core/state/interaction/selectors.ts index bf7096df..ec880228 100644 --- a/packages/core/state/interaction/selectors.ts +++ b/packages/core/state/interaction/selectors.ts @@ -47,7 +47,6 @@ export const getUserSelectedApplications = (state: State) => state.interaction.userSelectedApplications; export const getVisibleModal = (state: State) => state.interaction.visibleModal; export const isAicsEmployee = (state: State) => state.interaction.isAicsEmployee; -export const getMoveFileTarget = (state: State) => state.interaction.moveFileTarget; // COMPOSED SELECTORS export const getApplicationVersion = createSelector( From a450c4102951f88a6061ac7ae88d3696a9f8a171 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Thu, 31 Oct 2024 16:59:09 -0700 Subject: [PATCH 14/56] add gradient --- .../Modal/MoveFileManifest/MoveFileManifest.module.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css b/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css index df5280ae..641907b9 100644 --- a/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css +++ b/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css @@ -1,8 +1,10 @@ -.file-table-container { +.fileTableContainer { max-height: 300px; overflow-y: auto; border: 1px solid var(--border-color); - margin-bottom: var(--margin); + margin-bottom: 1em; + padding: 1em; + background: linear-gradient(to bottom, transparent, var(--secondary-background-color)); } .file-table { From ed34524940b0b742daba8195db0b3cbf25718c49 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Fri, 8 Nov 2024 10:58:12 -0800 Subject: [PATCH 15/56] feature/move-files-styling --- .../CopyFileManifest.module.css} | 35 +++++++++-- .../index.tsx | 61 +++++++++++++------ packages/core/components/Modal/index.tsx | 8 +-- .../core/hooks/useFileAccessContextMenu.ts | 8 +-- packages/core/state/interaction/actions.ts | 22 +++---- packages/core/state/interaction/logics.ts | 16 ++--- packages/core/state/interaction/reducer.ts | 6 +- 7 files changed, 104 insertions(+), 52 deletions(-) rename packages/core/components/Modal/{MoveFileManifest/MoveFileManifest.module.css => CopyFileManifest/CopyFileManifest.module.css} (52%) rename packages/core/components/Modal/{MoveFileManifest => CopyFileManifest}/index.tsx (55%) diff --git a/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css b/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css similarity index 52% rename from packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css rename to packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css index 641907b9..624ed4b1 100644 --- a/packages/core/components/Modal/MoveFileManifest/MoveFileManifest.module.css +++ b/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css @@ -1,9 +1,11 @@ +.bodyContainer { + position: relative; +} + .fileTableContainer { max-height: 300px; overflow-y: auto; - border: 1px solid var(--border-color); - margin-bottom: 1em; - padding: 1em; + margin-bottom: 0.5em; background: linear-gradient(to bottom, transparent, var(--secondary-background-color)); } @@ -16,7 +18,6 @@ .file-table td { padding: 8px; text-align: left; - border-bottom: 1px solid var(--border-color); white-space: nowrap; color: var(--primary-text-color); } @@ -31,3 +32,29 @@ .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/MoveFileManifest/index.tsx b/packages/core/components/Modal/CopyFileManifest/index.tsx similarity index 55% rename from packages/core/components/Modal/MoveFileManifest/index.tsx rename to packages/core/components/Modal/CopyFileManifest/index.tsx index 41611686..bd29c960 100644 --- a/packages/core/components/Modal/MoveFileManifest/index.tsx +++ b/packages/core/components/Modal/CopyFileManifest/index.tsx @@ -4,17 +4,17 @@ import { useDispatch, useSelector } from "react-redux"; import { ModalProps } from ".."; import BaseModal from "../BaseModal"; -import { PrimaryButton } from "../../Buttons"; +import { PrimaryButton, SecondaryButton } from "../../Buttons"; import FileDetail from "../../../entity/FileDetail"; import FileSelection from "../../../entity/FileSelection"; import { interaction, selection } from "../../../state"; -import styles from "./MoveFileManifest.module.css"; +import styles from "./CopyFileManifest.module.css"; /** * Modal overlay for displaying details of selected files for NAS cache operations. */ -export default function MoveFileManifest({ onDismiss }: ModalProps) { +export default function CopyFileManifest({ onDismiss }: ModalProps) { const dispatch = useDispatch(); const fileService = useSelector(interaction.selectors.getFileService); const fileSelection = useSelector( @@ -26,6 +26,14 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { const [totalSize, setTotalSize] = React.useState(); const [isLoading, setLoading] = React.useState(false); + // Utility function to clip file names + const clipFileName = (filename: string) => { + if (filename.length > 20) { + return filename.slice(0, 9) + "..." + filename.slice(-8); + } + return filename; + }; + React.useEffect(() => { async function fetchDetails() { setLoading(true); @@ -42,13 +50,17 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { }, [fileSelection, fileService]); const onMove = () => { - dispatch(interaction.actions.moveFiles(fileDetails)); + dispatch(interaction.actions.copyFiles(fileDetails)); onDismiss(); }; const body = ( -
-

Selected Files:

+
+

+ Files copied to the local NAS cache (VAST) are stored with a 180-day lease, after + which they revert to cloud-only. To renew the lease, simply reselect the files and + confirm the copy. +

@@ -60,15 +72,21 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { {fileDetails.map((file) => ( - + ))}
{file.name}{clipFileName(file.name)} {filesize(file.size || 0)}
-

Total Files: {fileDetails.length}

-

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

+
+ + {isLoading ? "Calculating..." : totalSize || "0 B"} + + + {fileDetails.length.toLocaleString()} files + +
); @@ -76,17 +94,24 @@ export default function MoveFileManifest({ onDismiss }: ModalProps) { +
+ + +
} onDismiss={onDismiss} - title="Move Files to NAS Cache" + title="Copy Files to NAS Cache (VAST)" /> ); } 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 372c25e1..b4ed2682 100644 --- a/packages/core/hooks/useFileAccessContextMenu.ts +++ b/packages/core/hooks/useFileAccessContextMenu.ts @@ -138,13 +138,13 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => { ...(isQueryingAicsFms ? [ { - 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 7f85149c..2d9a0b9d 100644 --- a/packages/core/state/interaction/actions.ts +++ b/packages/core/state/interaction/actions.ts @@ -681,35 +681,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 3e70a360..eeed34ed 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,13 +578,13 @@ 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({ - type: MOVE_FILES, +const CopyFilesLogic = createLogic({ + type: COPY_FILES, process(deps, dispatch, done) { - const action = deps.action as MoveFilesAction; + const action = deps.action as CopyFilesAction; console.log(`Moving files:`, action.payload.fileDetails); done(); }, @@ -601,5 +601,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 2f39ea7e..93b457f0 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 From 52bd1a9667a21dc15940304b0e0693d324ca587d Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 11 Nov 2024 13:21:25 -0800 Subject: [PATCH 16/56] feature/copy-to-local-cache-request --- .../FileService/HttpFileService/index.ts | 23 +++++++++++++++++++ packages/core/state/interaction/logics.ts | 19 +++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/packages/core/services/FileService/HttpFileService/index.ts b/packages/core/services/FileService/HttpFileService/index.ts index f352cfe2..e5d5f63d 100644 --- a/packages/core/services/FileService/HttpFileService/index.ts +++ b/packages/core/services/FileService/HttpFileService/index.ts @@ -20,9 +20,11 @@ interface Config extends ConnectionConfig { * Service responsible for fetching file related metadata. */ export default class HttpFileService extends HttpServiceBase implements FileService { + private static readonly CACHE_ENDPOINT_VERSION = "3.0"; private static readonly ENDPOINT_VERSION = "3.0"; public static readonly BASE_FILES_URL = `file-explorer-service/${HttpFileService.ENDPOINT_VERSION}/files`; public static readonly BASE_FILE_COUNT_URL = `${HttpFileService.BASE_FILES_URL}/count`; + public static readonly BASE_FILE_CACHE_URL = `fss2/${HttpFileService.CACHE_ENDPOINT_VERSION}/file/cache`; public static readonly SELECTION_AGGREGATE_URL = `${HttpFileService.BASE_FILES_URL}/selection/aggregate`; private static readonly CSV_ENDPOINT_VERSION = "2.0"; public static readonly BASE_CSV_DOWNLOAD_URL = `file-explorer-service/${HttpFileService.CSV_ENDPOINT_VERSION}/files/selection/manifest`; @@ -127,4 +129,25 @@ export default class HttpFileService extends HttpServiceBase implements FileServ uniqueId() ); } + + /** + * Cache a list of files to NAS cache (VAST) by sending their IDs to FSS. + */ + public async cacheFiles( + fileIds: string[] + ): Promise<{ cacheFileStatuses: { [fileId: string]: string } }> { + const requestUrl = `${this.baseUrl}/${HttpFileService.BASE_FILE_CACHE_URL}${this.pathSuffix}`; + const requestBody = JSON.stringify({ fileIds }); + + try { + const cacheStatuses = await this.rawPost<{ + cacheFileStatuses: { [fileId: string]: string }; + }>(requestUrl, requestBody); + + return cacheStatuses; // Return the entire response object + } catch (error) { + console.error("Failed to cache files:", error); + throw new Error("Unable to complete the caching request."); + } + } } diff --git a/packages/core/state/interaction/logics.ts b/packages/core/state/interaction/logics.ts index 3e70a360..ee4758f7 100644 --- a/packages/core/state/interaction/logics.ts +++ b/packages/core/state/interaction/logics.ts @@ -582,12 +582,21 @@ const setIsSmallScreen = createLogic({ * Logs details of files that are being moved. */ const moveFilesLogic = createLogic({ - type: MOVE_FILES, - process(deps, dispatch, done) { - const action = deps.action as MoveFilesAction; - console.log(`Moving files:`, action.payload.fileDetails); - done(); + async process({ action, getState }: ReduxLogicDeps, _dispatch, done) { + try { + const httpFileService = interactionSelectors.getHttpFileService(getState()); + const fileIds = (action as MoveFilesAction).payload.fileDetails.map((file) => file.id); + const cacheStatuses = await httpFileService.cacheFiles(fileIds); + + // TODO: What to do with the status + console.log("Cache statuses:", cacheStatuses); + } catch (err) { + console.error(`Error encountered while moving files: ${err}`); + } finally { + done(); + } }, + type: MOVE_FILES, }); export default [ From 3e9a0eba4dec4a11d856110522aef98a6b8bc858 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 11 Nov 2024 13:31:52 -0800 Subject: [PATCH 17/56] add test --- .../test/HttpFileService.test.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts b/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts index 4f569180..7bc0331f 100644 --- a/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts +++ b/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts @@ -78,25 +78,38 @@ describe("HttpFileService", () => { }); }); - describe("getCountOfMatchingFiles", () => { + describe("cacheFiles", () => { const httpClient = createMockHttpClient({ - when: `${baseUrl}/${HttpFileService.BASE_FILE_COUNT_URL}`, + when: `${baseUrl}/${HttpFileService.BASE_FILE_CACHE_URL}`, respondWith: { data: { - data: [2], + cacheFileStatuses: { + abc123: "DOWNLOAD_COMPLETE", + def456: "ERROR", + }, }, }, }); - it("issues request for count of files matching given parameters", async () => { + it("sends file IDs to be cached and returns their statuses", async () => { + // Arrange const fileService = new HttpFileService({ baseUrl, httpClient, downloadService: new FileDownloadServiceNoop(), }); - const fileSet = new FileSet(); - const count = await fileService.getCountOfMatchingFiles(fileSet); - expect(count).to.equal(2); + const fileIds = ["abc123", "def456"]; + + // Act + const response = await fileService.cacheFiles(fileIds); + + // Assert + expect(response).to.deep.equal({ + cacheFileStatuses: { + abc123: "DOWNLOAD_COMPLETE", + def456: "ERROR", + }, + }); }); }); }); From 31d4abfd872d21b908a20ccc3270cf430010cec5 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Thu, 14 Nov 2024 13:54:10 -0800 Subject: [PATCH 18/56] add LoadBalancerBaseURl gloabl variable --- packages/core/App.tsx | 17 ++++++++--- packages/core/constants/index.ts | 6 ++++ packages/core/entity/FileDetail/index.ts | 13 ++------- .../core/hooks/useOpenWithMenuItems/index.tsx | 6 ++-- .../FileService/HttpFileService/index.ts | 2 +- .../test/HttpFileService.test.ts | 7 +++-- .../core/services/HttpServiceBase/index.ts | 22 ++++++++++++++- packages/core/state/interaction/actions.ts | 13 +++++---- packages/core/state/interaction/reducer.ts | 5 +++- packages/core/state/interaction/selectors.ts | 26 +++++++++++++++-- .../core/state/selection/test/reducer.test.ts | 5 +++- packages/desktop/src/main/global.d.ts | 2 +- packages/desktop/src/main/menu/data-source.ts | 28 +++++++++++++------ packages/desktop/src/renderer/index.tsx | 10 +++++-- packages/desktop/src/util/constants.ts | 8 +++--- 15 files changed, 119 insertions(+), 51 deletions(-) diff --git a/packages/core/App.tsx b/packages/core/App.tsx index 3a9364da..325f5605 100644 --- a/packages/core/App.tsx +++ b/packages/core/App.tsx @@ -14,7 +14,7 @@ import GlobalActionButtonRow from "./components/GlobalActionButtonRow"; import StatusMessage from "./components/StatusMessage"; import TutorialTooltip from "./components/TutorialTooltip"; import QuerySidebar from "./components/QuerySidebar"; -import { FileExplorerServiceBaseUrl } from "./constants"; +import { AicsLoadBalancerBaseUrl, FileExplorerServiceBaseUrl } from "./constants"; import { interaction, selection } from "./state"; import useLayoutMeasurements from "./hooks/useLayoutMeasurements"; @@ -39,11 +39,15 @@ interface AppProps { // Localhost: "https://localhost:9081" // Stage: "http://stg-aics-api.corp.alleninstitute.org" // From the web (behind load balancer): "/" + aicsLoadBalancerBaseUrl?: string; fileExplorerServiceBaseUrl?: string; } export default function App(props: AppProps) { - const { fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.PRODUCTION } = props; + const { + aicsLoadBalancerBaseUrl = AicsLoadBalancerBaseUrl.PRODUCTION, + fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.PRODUCTION, + } = props; const dispatch = useDispatch(); const hasQuerySelected = useSelector(selection.selectors.hasQuerySelected); @@ -80,8 +84,13 @@ export default function App(props: AppProps) { // Set data source base urls React.useEffect(() => { - dispatch(interaction.actions.initializeApp(fileExplorerServiceBaseUrl)); - }, [dispatch, fileExplorerServiceBaseUrl]); + dispatch( + interaction.actions.initializeApp({ + aicsLoadBalancerBaseUrl, + fileExplorerServiceBaseUrl, + }) + ); + }, [dispatch, aicsLoadBalancerBaseUrl, fileExplorerServiceBaseUrl]); // Respond to screen size changes React.useEffect(() => { diff --git a/packages/core/constants/index.ts b/packages/core/constants/index.ts index bc89dc0d..7da5a343 100644 --- a/packages/core/constants/index.ts +++ b/packages/core/constants/index.ts @@ -11,6 +11,12 @@ export enum FileExplorerServiceBaseUrl { PRODUCTION = "https://production.int.allencell.org", } +export enum AicsLoadBalancerBaseUrl { + LOCALHOST = "http://localhost:8080", + STAGING = "http://stg-aics.corp.alleninstitute.org", + PRODUCTION = "http://aics.corp.alleninstitute.org", +} + export const TOP_LEVEL_FILE_ANNOTATIONS = [ new Annotation({ annotationDisplayName: "File ID", diff --git a/packages/core/entity/FileDetail/index.ts b/packages/core/entity/FileDetail/index.ts index 4d8258d3..ad986a8b 100644 --- a/packages/core/entity/FileDetail/index.ts +++ b/packages/core/entity/FileDetail/index.ts @@ -1,5 +1,4 @@ import AnnotationName from "../Annotation/AnnotationName"; -import { FileExplorerServiceBaseUrl } from "../../constants"; import { FmsFileAnnotation } from "../../services/FileService"; import { renderZarrThumbnailURL } from "./RenderZarrThumbnailURL"; @@ -188,21 +187,13 @@ export default class FileDetail { return this.thumbnail; } - public getLinkToPlateUI(baseURL: string): string | undefined { + public getLinkToPlateUI(labkeyHost: string): string | undefined { // Grabbing plate barcode const platebarcode = this.getFirstAnnotationValue(AnnotationName.PLATE_BARCODE); - if (!platebarcode) { return undefined; } - - let labkeyHost = "localhost:9081"; - if (baseURL === FileExplorerServiceBaseUrl.PRODUCTION) { - labkeyHost = "aics.corp.alleninstitute.org"; - } else if (baseURL === FileExplorerServiceBaseUrl.STAGING) { - labkeyHost = "stg-aics.corp.alleninstitute.org"; - } - return `http://${labkeyHost}/labkey/aics_microscopy/AICS/editPlate.view?Barcode=${platebarcode}`; + return `${labkeyHost}/labkey/aics_microscopy/AICS/editPlate.view?Barcode=${platebarcode}`; } public getAnnotationNameToLinkMap(): { [annotationName: string]: string } { diff --git a/packages/core/hooks/useOpenWithMenuItems/index.tsx b/packages/core/hooks/useOpenWithMenuItems/index.tsx index bc3d5c0b..832fe50d 100644 --- a/packages/core/hooks/useOpenWithMenuItems/index.tsx +++ b/packages/core/hooks/useOpenWithMenuItems/index.tsx @@ -103,11 +103,9 @@ export default (fileDetails?: FileDetail, filters?: FileFilter[]): IContextualMe const annotationNameToAnnotationMap = useSelector( metadata.selectors.getAnnotationNameToAnnotationMap ); - const fileExplorerServiceBaseUrl = useSelector( - interaction.selectors.getFileExplorerServiceBaseUrl - ); + const aicsLoadBalancerBaseUrl = useSelector(interaction.selectors.getAicsLoadBalancerBaseUrl); - const plateLink = fileDetails?.getLinkToPlateUI(fileExplorerServiceBaseUrl); + const plateLink = fileDetails?.getLinkToPlateUI(aicsLoadBalancerBaseUrl); const annotationNameToLinkMap = React.useMemo( () => fileDetails?.annotations diff --git a/packages/core/services/FileService/HttpFileService/index.ts b/packages/core/services/FileService/HttpFileService/index.ts index e5d5f63d..2b5f81ad 100644 --- a/packages/core/services/FileService/HttpFileService/index.ts +++ b/packages/core/services/FileService/HttpFileService/index.ts @@ -136,7 +136,7 @@ export default class HttpFileService extends HttpServiceBase implements FileServ public async cacheFiles( fileIds: string[] ): Promise<{ cacheFileStatuses: { [fileId: string]: string } }> { - const requestUrl = `${this.baseUrl}/${HttpFileService.BASE_FILE_CACHE_URL}${this.pathSuffix}`; + const requestUrl = `${this.aicsLoadBalancerBaseUrl}/${HttpFileService.BASE_FILE_CACHE_URL}${this.pathSuffix}`; const requestBody = JSON.stringify({ fileIds }); try { diff --git a/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts b/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts index 7bc0331f..edf8356a 100644 --- a/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts +++ b/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts @@ -9,6 +9,7 @@ import FileDownloadServiceNoop from "../../../FileDownloadService/FileDownloadSe describe("HttpFileService", () => { const baseUrl = "test"; + const aicsLoadBalancerBaseUrlMock = "http://loadbalancer-test.aics.corp.alleninstitute.org"; const fileIds = ["abc123", "def456", "ghi789", "jkl012"]; const files = fileIds.map((file_id) => ({ file_id, @@ -28,7 +29,7 @@ describe("HttpFileService", () => { it("issues request for files that match given parameters", async () => { const httpFileService = new HttpFileService({ - baseUrl, + baseUrl: baseUrl, httpClient, downloadService: new FileDownloadServiceNoop(), }); @@ -80,7 +81,7 @@ describe("HttpFileService", () => { describe("cacheFiles", () => { const httpClient = createMockHttpClient({ - when: `${baseUrl}/${HttpFileService.BASE_FILE_CACHE_URL}`, + when: `${aicsLoadBalancerBaseUrlMock}/${HttpFileService.BASE_FILE_CACHE_URL}`, respondWith: { data: { cacheFileStatuses: { @@ -94,7 +95,7 @@ describe("HttpFileService", () => { it("sends file IDs to be cached and returns their statuses", async () => { // Arrange const fileService = new HttpFileService({ - baseUrl, + aicsLoadBalancerBaseUrl: aicsLoadBalancerBaseUrlMock, httpClient, downloadService: new FileDownloadServiceNoop(), }); diff --git a/packages/core/services/HttpServiceBase/index.ts b/packages/core/services/HttpServiceBase/index.ts index 1c89ae50..4d10b3c6 100644 --- a/packages/core/services/HttpServiceBase/index.ts +++ b/packages/core/services/HttpServiceBase/index.ts @@ -2,10 +2,11 @@ import axios, { AxiosInstance } from "axios"; import { Policy } from "cockatiel"; import LRUCache from "lru-cache"; -import { FileExplorerServiceBaseUrl } from "../../constants"; +import { AicsLoadBalancerBaseUrl, FileExplorerServiceBaseUrl } from "../../constants"; import RestServiceResponse from "../../entity/RestServiceResponse"; export interface ConnectionConfig { + aicsLoadBalancerBaseUrl?: string | keyof typeof AicsLoadBalancerBaseUrl; applicationVersion?: string; baseUrl?: string | keyof typeof FileExplorerServiceBaseUrl; httpClient?: AxiosInstance; @@ -14,6 +15,7 @@ export interface ConnectionConfig { } export const DEFAULT_CONNECTION_CONFIG = { + aicsLoadBalancerBaseUrl: AicsLoadBalancerBaseUrl.PRODUCTION, baseUrl: FileExplorerServiceBaseUrl.PRODUCTION, httpClient: axios.create(), }; @@ -97,8 +99,11 @@ export default class HttpServiceBase { .join(""); } + public aicsLoadBalancerBaseUrl: string | keyof typeof AicsLoadBalancerBaseUrl = + DEFAULT_CONNECTION_CONFIG.aicsLoadBalancerBaseUrl; public baseUrl: string | keyof typeof FileExplorerServiceBaseUrl = DEFAULT_CONNECTION_CONFIG.baseUrl; + protected httpClient = DEFAULT_CONNECTION_CONFIG.httpClient; private applicationVersion = "NOT SET"; private userName?: string; @@ -106,6 +111,10 @@ export default class HttpServiceBase { private readonly urlToResponseDataCache = new LRUCache({ max: MAX_CACHE_SIZE }); constructor(config: ConnectionConfig = {}) { + if (config.aicsLoadBalancerBaseUrl) { + this.setAicsLoadBalancerBaseUrl(config.aicsLoadBalancerBaseUrl); + } + if (config.applicationVersion) { this.setApplicationVersion(config.applicationVersion); } @@ -234,6 +243,17 @@ export default class HttpServiceBase { return new RestServiceResponse(response.data); } + public setAicsLoadBalancerBaseUrl( + aicsLoadBalancerBaseUrl: string | keyof typeof AicsLoadBalancerBaseUrl + ) { + if (this.aicsLoadBalancerBaseUrl !== aicsLoadBalancerBaseUrl) { + // bust cache when base url changes + this.urlToResponseDataCache.reset(); + } + + this.aicsLoadBalancerBaseUrl = aicsLoadBalancerBaseUrl; + } + public setApplicationVersion(applicationVersion: string) { this.applicationVersion = applicationVersion; this.setHeaders(); diff --git a/packages/core/state/interaction/actions.ts b/packages/core/state/interaction/actions.ts index 7f85149c..e5f98007 100644 --- a/packages/core/state/interaction/actions.ts +++ b/packages/core/state/interaction/actions.ts @@ -260,12 +260,13 @@ export interface InitializeApp { payload: string; } -export function initializeApp(baseUrl: string): InitializeApp { - return { - type: INITIALIZE_APP, - payload: baseUrl, - }; -} +export const initializeApp = (payload: { + aicsLoadBalancerBaseUrl: string; + fileExplorerServiceBaseUrl: string; +}) => ({ + type: INITIALIZE_APP, + payload, +}); /** * PROCESS AND STATUS RELATED ENUMS, INTERFACES, ETC. diff --git a/packages/core/state/interaction/reducer.ts b/packages/core/state/interaction/reducer.ts index 2f39ea7e..f8bea820 100644 --- a/packages/core/state/interaction/reducer.ts +++ b/packages/core/state/interaction/reducer.ts @@ -43,6 +43,7 @@ import DatabaseServiceNoop from "../../services/DatabaseService/DatabaseServiceN import PublicDataset from "../../../web/src/entity/PublicDataset"; export interface InteractionStateBranch { + aicsLoadBalancerBaseUrl: string; applicationVersion?: string; contextMenuIsVisible: boolean; contextMenuItems: ContextMenuItem[]; @@ -67,6 +68,7 @@ export interface InteractionStateBranch { } export const initialState: InteractionStateBranch = { + aicsLoadBalancerBaseUrl: DEFAULT_CONNECTION_CONFIG.aicsLoadBalancerBaseUrl, contextMenuIsVisible: false, contextMenuItems: [], // Passed to `ContextualMenu` as `target`. From the "@fluentui/react" docs: @@ -166,7 +168,8 @@ export default makeReducer( }), [INITIALIZE_APP]: (state, action) => ({ ...state, - fileExplorerServiceBaseUrl: action.payload, + aicsLoadBalancerBaseUrl: action.payload.aicsLoadBalancerBaseUrl, + fileExplorerServiceBaseUrl: action.payload.fileExplorerServiceBaseUrl, }), [SET_VISIBLE_MODAL]: (state, action) => ({ ...state, diff --git a/packages/core/state/interaction/selectors.ts b/packages/core/state/interaction/selectors.ts index ec880228..75c852b6 100644 --- a/packages/core/state/interaction/selectors.ts +++ b/packages/core/state/interaction/selectors.ts @@ -17,6 +17,8 @@ import { ModalType } from "../../components/Modal"; import { AICS_FMS_DATA_SOURCE_NAME } from "../../constants"; // BASIC SELECTORS +export const getAicsLoadBalancerBaseUrl = (state: State) => + state.interaction.aicsLoadBalancerBaseUrl; export const getContextMenuVisibility = (state: State) => state.interaction.contextMenuIsVisible; export const getContextMenuItems = (state: State) => state.interaction.contextMenuItems; export const getContextMenuPositionReference = (state: State) => @@ -102,14 +104,22 @@ export const getHttpFileService = createSelector( [ getApplicationVersion, getUserName, + getAicsLoadBalancerBaseUrl, getFileExplorerServiceBaseUrl, getPlatformDependentServices, getRefreshKey, ], - (applicationVersion, userName, fileExplorerBaseUrl, platformDependentServices) => + ( + applicationVersion, + userName, + aicsLoadBalancerBaseUrl, + fileExplorerBaseUrl, + platformDependentServices + ) => new HttpFileService({ applicationVersion, userName, + aicsLoadBalancerBaseUrl: aicsLoadBalancerBaseUrl, baseUrl: fileExplorerBaseUrl, downloadService: platformDependentServices.fileDownloadService, }) @@ -153,6 +163,7 @@ export const getAnnotationService = createSelector( [ getApplicationVersion, getUserName, + getAicsLoadBalancerBaseUrl, getFileExplorerServiceBaseUrl, getSelectedDataSources, getPlatformDependentServices, @@ -161,6 +172,7 @@ export const getAnnotationService = createSelector( ( applicationVersion, userName, + aicsLoadBalancerBaseUrl, fileExplorerBaseUrl, dataSources, platformDependentServices @@ -174,17 +186,25 @@ export const getAnnotationService = createSelector( return new HttpAnnotationService({ applicationVersion, userName, + aicsLoadBalancerBaseUrl: aicsLoadBalancerBaseUrl, baseUrl: fileExplorerBaseUrl, }); } ); export const getDatasetService = createSelector( - [getApplicationVersion, getUserName, getFileExplorerServiceBaseUrl, getRefreshKey], - (applicationVersion, userName, fileExplorerBaseUrl) => + [ + getApplicationVersion, + getUserName, + getAicsLoadBalancerBaseUrl, + getFileExplorerServiceBaseUrl, + getRefreshKey, + ], + (applicationVersion, userName, aicsLoadBalancerBaseUrl, fileExplorerBaseUrl) => new DatasetService({ applicationVersion, userName, + aicsLoadBalancerBaseUrl: aicsLoadBalancerBaseUrl, baseUrl: fileExplorerBaseUrl, }) ); diff --git a/packages/core/state/selection/test/reducer.test.ts b/packages/core/state/selection/test/reducer.test.ts index ac0c1776..232e1ca5 100644 --- a/packages/core/state/selection/test/reducer.test.ts +++ b/packages/core/state/selection/test/reducer.test.ts @@ -16,7 +16,10 @@ import { DataSource } from "../../../services/DataSourceService"; describe("Selection reducer", () => { [ selection.actions.setAnnotationHierarchy([]), - interaction.actions.initializeApp("base"), + interaction.actions.initializeApp({ + fileExplorerServiceBaseUrl: "base", + aicsLoadBalancerBaseUrl: "loadBalancerBaseUrl", + }), ].forEach((expectedAction) => it(`clears selected file state when ${expectedAction.type} is fired`, () => { // arrange diff --git a/packages/desktop/src/main/global.d.ts b/packages/desktop/src/main/global.d.ts index 5a194e88..c28d2c8b 100644 --- a/packages/desktop/src/main/global.d.ts +++ b/packages/desktop/src/main/global.d.ts @@ -1,4 +1,4 @@ /*eslint no-var: "off"*/ // necessary in order to do: global.fileExplorerServiceBaseUrl = "..." -declare var fileDownloadServiceBaseUrl: string; +declare var aicsLoadBalancerBaseUrl: string; declare var fileExplorerServiceBaseUrl: string; diff --git a/packages/desktop/src/main/menu/data-source.ts b/packages/desktop/src/main/menu/data-source.ts index 97946308..9b73a358 100644 --- a/packages/desktop/src/main/menu/data-source.ts +++ b/packages/desktop/src/main/menu/data-source.ts @@ -2,12 +2,12 @@ import { MenuItemConstructorOptions } from "electron"; import { GlobalVariableChannels, - FileDownloadServiceBaseUrl, + AicsLoadBalancerBaseUrl, FileExplorerServiceBaseUrl, } from "../../util/constants"; // Least effort state management accessible to both the main and renderer processes. -global.fileDownloadServiceBaseUrl = FileDownloadServiceBaseUrl.PRODUCTION; +global.aicsLoadBalancerBaseUrl = AicsLoadBalancerBaseUrl.PRODUCTION; global.fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.PRODUCTION; const dataSourceMenu: MenuItemConstructorOptions = { @@ -16,12 +16,16 @@ const dataSourceMenu: MenuItemConstructorOptions = { { label: "Localhost", type: "radio", - checked: global.fileExplorerServiceBaseUrl === FileExplorerServiceBaseUrl.LOCALHOST, + checked: + global.aicsLoadBalancerBaseUrl === AicsLoadBalancerBaseUrl.LOCALHOST && + global.fileExplorerServiceBaseUrl === FileExplorerServiceBaseUrl.LOCALHOST, click: (_, focusedWindow) => { if (focusedWindow) { + global.aicsLoadBalancerBaseUrl = AicsLoadBalancerBaseUrl.LOCALHOST; + global.fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.LOCALHOST; focusedWindow.webContents.send(GlobalVariableChannels.BaseUrl, { + aicsLoadBalancerBaseUrl: AicsLoadBalancerBaseUrl.LOCALHOST, fileExplorerServiceBaseUrl: FileExplorerServiceBaseUrl.LOCALHOST, - fileDownloadServiceBaseUrl: FileDownloadServiceBaseUrl.LOCALHOST, }); } }, @@ -29,12 +33,16 @@ const dataSourceMenu: MenuItemConstructorOptions = { { label: "Staging", type: "radio", - checked: global.fileExplorerServiceBaseUrl === FileExplorerServiceBaseUrl.STAGING, + checked: + global.aicsLoadBalancerBaseUrl === AicsLoadBalancerBaseUrl.STAGING && + global.fileExplorerServiceBaseUrl === FileExplorerServiceBaseUrl.STAGING, click: (_, focusedWindow) => { if (focusedWindow) { + global.aicsLoadBalancerBaseUrl = AicsLoadBalancerBaseUrl.STAGING; + global.fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.STAGING; focusedWindow.webContents.send(GlobalVariableChannels.BaseUrl, { + aicsLoadBalancerBaseUrl: AicsLoadBalancerBaseUrl.STAGING, fileExplorerServiceBaseUrl: FileExplorerServiceBaseUrl.STAGING, - fileDownloadServiceBaseUrl: FileDownloadServiceBaseUrl.STAGING, }); } }, @@ -42,12 +50,16 @@ const dataSourceMenu: MenuItemConstructorOptions = { { label: "Production", type: "radio", - checked: global.fileExplorerServiceBaseUrl === FileExplorerServiceBaseUrl.PRODUCTION, + checked: + global.aicsLoadBalancerBaseUrl === AicsLoadBalancerBaseUrl.PRODUCTION && + global.fileExplorerServiceBaseUrl === FileExplorerServiceBaseUrl.PRODUCTION, click: (_, focusedWindow) => { if (focusedWindow) { + global.aicsLoadBalancerBaseUrl = AicsLoadBalancerBaseUrl.PRODUCTION; + global.fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.PRODUCTION; focusedWindow.webContents.send(GlobalVariableChannels.BaseUrl, { + aicsLoadBalancerBaseUrl: AicsLoadBalancerBaseUrl.PRODUCTION, fileExplorerServiceBaseUrl: FileExplorerServiceBaseUrl.PRODUCTION, - fileDownloadServiceBaseUrl: FileDownloadServiceBaseUrl.PRODUCTION, }); } }, diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index 9cc4afaa..36c27fd6 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -120,17 +120,21 @@ function renderFmsFileExplorer() { - + , document.getElementById(APP_ID) ); } +// Listen for IPC updates to global variables ipcRenderer.addListener( GlobalVariableChannels.BaseUrl, - (_, { fileExplorerServiceBaseUrl, fileDownloadServiceBaseUrl }) => { - global.fileDownloadServiceBaseUrl = fileDownloadServiceBaseUrl; + (_, { aicsLoadBalancerBaseUrl, fileExplorerServiceBaseUrl }) => { + global.aicsLoadBalancerBaseUrl = aicsLoadBalancerBaseUrl; global.fileExplorerServiceBaseUrl = fileExplorerServiceBaseUrl; renderFmsFileExplorer(); } diff --git a/packages/desktop/src/util/constants.ts b/packages/desktop/src/util/constants.ts index 2b950921..0edb8023 100644 --- a/packages/desktop/src/util/constants.ts +++ b/packages/desktop/src/util/constants.ts @@ -3,10 +3,10 @@ // pattern used in the npm script used to invoke electron-mocha. export const RUN_IN_RENDERER = "@renderer"; -export enum FileDownloadServiceBaseUrl { - LOCALHOST = "http://localhost:8080/labkey/fmsfiles/image", - STAGING = "http://stg-aics.corp.alleninstitute.org/labkey/fmsfiles/image", - PRODUCTION = "http://aics.corp.alleninstitute.org/labkey/fmsfiles/image", +export enum AicsLoadBalancerBaseUrl { + LOCALHOST = "http://localhost:8080", + STAGING = "http://stg-aics.corp.alleninstitute.org", + PRODUCTION = "http://aics.corp.alleninstitute.org", } export enum FileExplorerServiceBaseUrl { From 6abbb7c0bf0d95a0f32b66d7e82e551338eb86be Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 18 Nov 2024 10:11:02 -0800 Subject: [PATCH 19/56] update put request --- .../FileService/HttpFileService/index.ts | 13 ++++++---- .../core/services/HttpServiceBase/index.ts | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/core/services/FileService/HttpFileService/index.ts b/packages/core/services/FileService/HttpFileService/index.ts index 2b5f81ad..e20a4bb7 100644 --- a/packages/core/services/FileService/HttpFileService/index.ts +++ b/packages/core/services/FileService/HttpFileService/index.ts @@ -20,7 +20,7 @@ interface Config extends ConnectionConfig { * Service responsible for fetching file related metadata. */ export default class HttpFileService extends HttpServiceBase implements FileService { - private static readonly CACHE_ENDPOINT_VERSION = "3.0"; + private static readonly CACHE_ENDPOINT_VERSION = "v3.0"; private static readonly ENDPOINT_VERSION = "3.0"; public static readonly BASE_FILES_URL = `file-explorer-service/${HttpFileService.ENDPOINT_VERSION}/files`; public static readonly BASE_FILE_COUNT_URL = `${HttpFileService.BASE_FILES_URL}/count`; @@ -138,13 +138,16 @@ export default class HttpFileService extends HttpServiceBase implements FileServ ): Promise<{ cacheFileStatuses: { [fileId: string]: string } }> { const requestUrl = `${this.aicsLoadBalancerBaseUrl}/${HttpFileService.BASE_FILE_CACHE_URL}${this.pathSuffix}`; const requestBody = JSON.stringify({ fileIds }); + const headers = { + "Content-Type": "application/json", + "X-User-Id": "brian.whitney", // TODO: Make this not my user + }; try { - const cacheStatuses = await this.rawPost<{ + const cacheStatuses = await this.rawPut<{ cacheFileStatuses: { [fileId: string]: string }; - }>(requestUrl, requestBody); - - return cacheStatuses; // Return the entire response object + }>(requestUrl, requestBody, headers); + return cacheStatuses; } catch (error) { console.error("Failed to cache files:", error); throw new Error("Unable to complete the caching request."); diff --git a/packages/core/services/HttpServiceBase/index.ts b/packages/core/services/HttpServiceBase/index.ts index 4d10b3c6..3516272f 100644 --- a/packages/core/services/HttpServiceBase/index.ts +++ b/packages/core/services/HttpServiceBase/index.ts @@ -195,6 +195,32 @@ export default class HttpServiceBase { return response.data; } + public async rawPut( + url: string, + body: string, + headers: { [key: string]: string } = {} + ): Promise { + const encodedUrl = HttpServiceBase.encodeURI(url); + const config = { headers: { ...headers } }; + + let response; + try { + // Retry policy wrapped around axios PUT + response = await retry.execute(() => this.httpClient.put(encodedUrl, body, config)); + } catch (err) { + if (axios.isAxiosError(err) && err?.response?.data?.message) { + throw new Error(JSON.stringify(err.response.data.message)); + } + throw err; + } + + if (response.status >= 400 || response.data === undefined) { + throw new Error(`Request for ${encodedUrl} failed`); + } + + return response.data; + } + public async post(url: string, body: string): Promise> { const encodedUrl = HttpServiceBase.encodeURI(url); const config = { headers: { "Content-Type": "application/json" } }; From 46bae5d4f05ef4e1a824dc95ae5365776e24f9c5 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 18 Nov 2024 15:13:52 -0800 Subject: [PATCH 20/56] add username to cache request --- .../core/services/FileService/HttpFileService/index.ts | 7 ++++--- .../HttpFileService/test/HttpFileService.test.ts | 3 ++- packages/core/state/interaction/logics.ts | 3 ++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/core/services/FileService/HttpFileService/index.ts b/packages/core/services/FileService/HttpFileService/index.ts index e20a4bb7..24f041c5 100644 --- a/packages/core/services/FileService/HttpFileService/index.ts +++ b/packages/core/services/FileService/HttpFileService/index.ts @@ -134,15 +134,16 @@ export default class HttpFileService extends HttpServiceBase implements FileServ * Cache a list of files to NAS cache (VAST) by sending their IDs to FSS. */ public async cacheFiles( - fileIds: string[] + fileIds: string[], + username?: string ): Promise<{ cacheFileStatuses: { [fileId: string]: string } }> { const requestUrl = `${this.aicsLoadBalancerBaseUrl}/${HttpFileService.BASE_FILE_CACHE_URL}${this.pathSuffix}`; const requestBody = JSON.stringify({ fileIds }); const headers = { "Content-Type": "application/json", - "X-User-Id": "brian.whitney", // TODO: Make this not my user + "X-User-Id": username || "anonymous", }; - + console.log(headers); try { const cacheStatuses = await this.rawPut<{ cacheFileStatuses: { [fileId: string]: string }; diff --git a/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts b/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts index edf8356a..a53564a5 100644 --- a/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts +++ b/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts @@ -100,9 +100,10 @@ describe("HttpFileService", () => { downloadService: new FileDownloadServiceNoop(), }); const fileIds = ["abc123", "def456"]; + const username = "test.user"; // Act - const response = await fileService.cacheFiles(fileIds); + const response = await fileService.cacheFiles(fileIds, username); // Assert expect(response).to.deep.equal({ diff --git a/packages/core/state/interaction/logics.ts b/packages/core/state/interaction/logics.ts index ee4758f7..8f222444 100644 --- a/packages/core/state/interaction/logics.ts +++ b/packages/core/state/interaction/logics.ts @@ -585,8 +585,9 @@ const moveFilesLogic = createLogic({ async process({ action, getState }: ReduxLogicDeps, _dispatch, done) { try { const httpFileService = interactionSelectors.getHttpFileService(getState()); + const username = interactionSelectors.getUserName(getState()); const fileIds = (action as MoveFilesAction).payload.fileDetails.map((file) => file.id); - const cacheStatuses = await httpFileService.cacheFiles(fileIds); + const cacheStatuses = await httpFileService.cacheFiles(fileIds, username); // TODO: What to do with the status console.log("Cache statuses:", cacheStatuses); From f61ded5b550f1f42c6956edcc0b1f75aa47db266 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 18 Nov 2024 15:22:09 -0800 Subject: [PATCH 21/56] disable caching for web --- packages/core/hooks/useFileAccessContextMenu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/hooks/useFileAccessContextMenu.ts b/packages/core/hooks/useFileAccessContextMenu.ts index 372c25e1..307b6cad 100644 --- a/packages/core/hooks/useFileAccessContextMenu.ts +++ b/packages/core/hooks/useFileAccessContextMenu.ts @@ -135,7 +135,7 @@ export default (filters?: FileFilter[], onDismiss?: () => void) => { ], }, }, - ...(isQueryingAicsFms + ...(isQueryingAicsFms && !isOnWeb ? [ { key: "move-to-cache", From 4f4e570ffb656b22fd346351d8b01880e4351489 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Tue, 19 Nov 2024 16:04:44 -0800 Subject: [PATCH 22/56] feature/refactor-environment --- packages/core/App.tsx | 15 +- .../test/AnnotationFilterForm.test.tsx | 12 +- .../DirectoryTree/test/DirectoryTree.test.tsx | 15 +- .../test/LazilyRenderedThumbnail.test.tsx | 402 +++++++++--------- .../test/MetadataManifest.test.tsx | 7 +- .../test/SmallScreenWarning.test.tsx | 2 - packages/core/constants/index.ts | 38 +- .../FileSelection/test/FileSelection.test.ts | 8 +- packages/core/entity/FileSet/index.ts | 4 +- .../core/entity/FileSet/test/FileSet.test.ts | 18 +- .../core/hooks/useOpenWithMenuItems/index.tsx | 4 +- .../HttpAnnotationService/index.ts | 10 +- .../test/HttpAnnotationService.test.ts | 40 +- .../FileService/HttpFileService/index.ts | 14 +- .../test/HttpFileService.test.ts | 14 +- packages/core/services/FileService/index.ts | 2 +- .../core/services/HttpServiceBase/index.ts | 92 ++-- packages/core/state/interaction/actions.ts | 5 +- packages/core/state/interaction/reducer.ts | 17 +- packages/core/state/interaction/selectors.ts | 63 +-- .../state/interaction/test/logics.test.ts | 27 +- .../core/state/metadata/test/logics.test.ts | 6 +- .../core/state/selection/test/logics.test.ts | 34 +- .../core/state/selection/test/reducer.test.ts | 3 +- packages/desktop/src/main/global.d.ts | 5 +- packages/desktop/src/main/menu/data-source.ts | 39 +- packages/desktop/src/renderer/index.tsx | 17 +- packages/desktop/src/util/constants.ts | 17 +- 28 files changed, 460 insertions(+), 470 deletions(-) diff --git a/packages/core/App.tsx b/packages/core/App.tsx index 325f5605..bd795f2d 100644 --- a/packages/core/App.tsx +++ b/packages/core/App.tsx @@ -14,7 +14,7 @@ import GlobalActionButtonRow from "./components/GlobalActionButtonRow"; import StatusMessage from "./components/StatusMessage"; import TutorialTooltip from "./components/TutorialTooltip"; import QuerySidebar from "./components/QuerySidebar"; -import { AicsLoadBalancerBaseUrl, FileExplorerServiceBaseUrl } from "./constants"; +import { Environment } from "./constants"; import { interaction, selection } from "./state"; import useLayoutMeasurements from "./hooks/useLayoutMeasurements"; @@ -39,15 +39,11 @@ interface AppProps { // Localhost: "https://localhost:9081" // Stage: "http://stg-aics-api.corp.alleninstitute.org" // From the web (behind load balancer): "/" - aicsLoadBalancerBaseUrl?: string; - fileExplorerServiceBaseUrl?: string; + environment?: string; } export default function App(props: AppProps) { - const { - aicsLoadBalancerBaseUrl = AicsLoadBalancerBaseUrl.PRODUCTION, - fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.PRODUCTION, - } = props; + const { environment = Environment.PRODUCTION } = props; const dispatch = useDispatch(); const hasQuerySelected = useSelector(selection.selectors.hasQuerySelected); @@ -86,11 +82,10 @@ export default function App(props: AppProps) { React.useEffect(() => { dispatch( interaction.actions.initializeApp({ - aicsLoadBalancerBaseUrl, - fileExplorerServiceBaseUrl, + environment, }) ); - }, [dispatch, aicsLoadBalancerBaseUrl, fileExplorerServiceBaseUrl]); + }, [dispatch, environment]); // Respond to screen size changes React.useEffect(() => { diff --git a/packages/core/components/AnnotationFilterForm/test/AnnotationFilterForm.test.tsx b/packages/core/components/AnnotationFilterForm/test/AnnotationFilterForm.test.tsx index 24ecae20..50e5112f 100644 --- a/packages/core/components/AnnotationFilterForm/test/AnnotationFilterForm.test.tsx +++ b/packages/core/components/AnnotationFilterForm/test/AnnotationFilterForm.test.tsx @@ -38,7 +38,7 @@ describe("", () => { }; const mockHttpClient = createMockHttpClient(responseStub); const annotationService = new HttpAnnotationService({ - baseUrl: "test", + fileExplorerServiceBaseUrl: "test", httpClient: mockHttpClient, }); sandbox.stub(interaction.selectors, "getAnnotationService").returns(annotationService); @@ -75,7 +75,7 @@ describe("", () => { }; const mockHttpClient = createMockHttpClient(responseStub); const annotationService = new HttpAnnotationService({ - baseUrl: "test", + fileExplorerServiceBaseUrl: "test", httpClient: mockHttpClient, }); sandbox.stub(interaction.selectors, "getAnnotationService").returns(annotationService); @@ -131,7 +131,7 @@ describe("", () => { }; const mockHttpClient = createMockHttpClient(responseStub); const annotationService = new HttpAnnotationService({ - baseUrl: "test", + fileExplorerServiceBaseUrl: "test", httpClient: mockHttpClient, }); sandbox.stub(interaction.selectors, "getAnnotationService").returns(annotationService); @@ -179,7 +179,7 @@ describe("", () => { }; const mockHttpClient = createMockHttpClient(responseStub); const annotationService = new HttpAnnotationService({ - baseUrl: "test", + fileExplorerServiceBaseUrl: "test", httpClient: mockHttpClient, }); @@ -286,7 +286,7 @@ describe("", () => { }; const mockHttpClient = createMockHttpClient(responseStub); const annotationService = new HttpAnnotationService({ - baseUrl: "test", + fileExplorerServiceBaseUrl: "test", httpClient: mockHttpClient, }); sandbox.stub(interaction.selectors, "getAnnotationService").returns(annotationService); @@ -341,7 +341,7 @@ describe("", () => { }; const mockHttpClient = createMockHttpClient(responseStub); const annotationService = new HttpAnnotationService({ - baseUrl: "test", + fileExplorerServiceBaseUrl: "test", httpClient: mockHttpClient, }); sandbox.stub(interaction.selectors, "getAnnotationService").returns(annotationService); diff --git a/packages/core/components/DirectoryTree/test/DirectoryTree.test.tsx b/packages/core/components/DirectoryTree/test/DirectoryTree.test.tsx index b8bb860e..6d7f968d 100644 --- a/packages/core/components/DirectoryTree/test/DirectoryTree.test.tsx +++ b/packages/core/components/DirectoryTree/test/DirectoryTree.test.tsx @@ -53,14 +53,10 @@ describe("", () => { type: "Text", }); - const baseUrl = "http://test-aics.corp.alleninstitute.org"; const baseDisplayAnnotations = TOP_LEVEL_FILE_ANNOTATIONS.filter( (a) => a.name === AnnotationName.FILE_NAME ); const state = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: baseUrl, - }, selection: { annotationHierarchy: [fooAnnotation.name, barAnnotation.name], displayAnnotations: [...baseDisplayAnnotations, fooAnnotation, barAnnotation], @@ -188,9 +184,13 @@ describe("", () => { }, ]; const mockHttpClient = createMockHttpClient(responseStubs); - const annotationService = new HttpAnnotationService({ baseUrl, httpClient: mockHttpClient }); + const fileExplorerServiceBaseUrl = "http://test.int.allencell.org"; + const annotationService = new HttpAnnotationService({ + fileExplorerServiceBaseUrl: fileExplorerServiceBaseUrl, + httpClient: mockHttpClient, + }); const fileService = new HttpFileService({ - baseUrl, + fileExplorerServiceBaseUrl: fileExplorerServiceBaseUrl, httpClient: mockHttpClient, downloadService: new FileDownloadServiceNoop(), }); @@ -353,9 +353,6 @@ describe("", () => { it("only includes one filter value per annotation for an annotation within the hierarchy", async () => { const oneAnnotationDeepState = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: baseUrl, - }, selection: { annotationHierarchy: [fooAnnotation.name], displayAnnotations: [...baseDisplayAnnotations, fooAnnotation, barAnnotation], diff --git a/packages/core/components/FileList/test/LazilyRenderedThumbnail.test.tsx b/packages/core/components/FileList/test/LazilyRenderedThumbnail.test.tsx index 0a78a08e..0175fccc 100644 --- a/packages/core/components/FileList/test/LazilyRenderedThumbnail.test.tsx +++ b/packages/core/components/FileList/test/LazilyRenderedThumbnail.test.tsx @@ -1,201 +1,201 @@ -import { configureMockStore, mergeState } from "@aics/redux-utils"; -import { render } from "@testing-library/react"; -import { expect } from "chai"; -import * as React from "react"; -import { Provider } from "react-redux"; -import * as sinon from "sinon"; - -import LazilyRenderedThumbnail from "../LazilyRenderedThumbnail"; -import { initialState } from "../../../state"; -import FileSet from "../../../entity/FileSet"; -import FileDetail from "../../../entity/FileDetail"; - -describe("", () => { - function makeItemData() { - const fileSet = new FileSet(); - sinon.stub(fileSet, "getFileByIndex").callsFake((index) => { - if (index === 0) { - return new FileDetail({ - annotations: [], - file_id: "abc1230", - file_name: "my_image0.czi", - file_path: "some/path/to/my_image0.czi", - file_size: 1, - thumbnail: "some/path/to/my_image0.jpg", - uploaded: new Date().toISOString(), - }); - } - if (index === 9) { - return new FileDetail({ - annotations: [], - file_id: "abc1239", - file_name: "my_image9.jpg", - file_path: "some/path/to/my_image9.jpg", - file_size: 1, - uploaded: new Date().toISOString(), - }); - } - if (index === 25) { - return new FileDetail({ - annotations: [], - file_id: "abc12325", - file_name: "my_image25.czi", - file_path: "some/path/to/my_image25.czi", - file_size: 1, - uploaded: new Date().toISOString(), - }); - } - }); - - return { - fileSet, - measuredWidth: 600, - itemCount: 100, - onContextMenu: sinon.spy(), - onSelect: sinon.spy(), - }; - } - - it("renders thumbnail when file has one specified", async () => { - // Arrange - const state = mergeState(initialState, {}); - const { store } = configureMockStore({ state }); - - // Act - const { getAllByText, findByRole } = render( - - - - ); - - // Assert - // Also checking for proper row/col indexing - const thumbnail = await findByRole("img"); - expect(thumbnail.getAttribute("src")).to.include("some/path/to/my_image0.jpg"); - expect(getAllByText("my_image0.czi")).to.not.be.empty; - }); - - it("renders file as thumbnail if file is renderable type", async () => { - // Arrange - const { store } = configureMockStore({ state: initialState }); - - // Act - const { getAllByText, findByRole } = render( - - - - ); - - // Assert - // Also confirms proper row/col indexing - const thumbnail = await findByRole("img"); - expect(thumbnail.getAttribute("src")).to.include("some/path/to/my_image9.jpg"); - expect(getAllByText("my_image9.jpg")).to.not.be.empty; - }); - - it("renders svg as thumbnail if file has no renderable thumbnail", () => { - // Arrange - const { store } = configureMockStore({ state: initialState }); - - // Act - const { getAllByText, queryByRole } = render( - - - - ); - - // Assert - // Also confirms proper row/col indexing - expect(".no-thumbnail").to.exist; - expect(".svg").to.exist; - expect(queryByRole("img")).not.to.exist; - expect(getAllByText("my_image25.czi")).to.not.be.empty; - }); - - it("renders a loading indicator when data is not available", () => { - // Arrange - const { store } = configureMockStore({ state: initialState }); - - // Act - const { queryByText, queryAllByTestId } = render( - - - - ); - - // Assert - expect(queryByText("my_image")).to.equal(null); - expect(queryAllByTestId("loading-spinner")).to.not.be.empty; - }); - - // We want to be able to render empty cells past the total item count in order to fill the grid - it("renders an empty cell if the index is past the total item count", () => { - // Arrange - const { store } = configureMockStore({ state: initialState }); - - // Act - const { queryByText, queryAllByTestId } = render( - - - - ); - - // Assert - expect(queryByText("my_image")).to.equal(null); - expect(queryAllByTestId("loading-spinner")).to.be.empty; - }); - - it("renders and indexes correctly with different number of columns", () => { - // Arrange - const state = { - ...initialState, - selection: { - ...initialState.selection, - fileGridColumnCount: 10, - }, - }; - const { store } = configureMockStore({ state }); - - // Act - const { getAllByText } = render( - - - - ); - - // Assert - expect(".no-thumbnail").to.exist; - expect(".svg").to.exist; - expect(getAllByText("my_image25.czi")).to.not.be.empty; - }); -}); +// import { configureMockStore, mergeState } from "@aics/redux-utils"; +// import { render } from "@testing-library/react"; +// import { expect } from "chai"; +// import * as React from "react"; +// import { Provider } from "react-redux"; +// import * as sinon from "sinon"; + +// import LazilyRenderedThumbnail from "../LazilyRenderedThumbnail"; +// import { initialState } from "../../../state"; +// import FileSet from "../../../entity/FileSet"; +// import FileDetail from "../../../entity/FileDetail"; + +// describe("", () => { +// function makeItemData() { +// const fileSet = new FileSet(); +// sinon.stub(fileSet, "getFileByIndex").callsFake((index) => { +// if (index === 0) { +// return new FileDetail({ +// annotations: [], +// file_id: "abc1230", +// file_name: "my_image0.czi", +// file_path: "some/path/to/my_image0.czi", +// file_size: 1, +// thumbnail: "some/path/to/my_image0.jpg", +// uploaded: new Date().toISOString(), +// }); +// } +// if (index === 9) { +// return new FileDetail({ +// annotations: [], +// file_id: "abc1239", +// file_name: "my_image9.jpg", +// file_path: "some/path/to/my_image9.jpg", +// file_size: 1, +// uploaded: new Date().toISOString(), +// }); +// } +// if (index === 25) { +// return new FileDetail({ +// annotations: [], +// file_id: "abc12325", +// file_name: "my_image25.czi", +// file_path: "some/path/to/my_image25.czi", +// file_size: 1, +// uploaded: new Date().toISOString(), +// }); +// } +// }); + +// return { +// fileSet, +// measuredWidth: 600, +// itemCount: 100, +// onContextMenu: sinon.spy(), +// onSelect: sinon.spy(), +// }; +// } + +// it("renders thumbnail when file has one specified", async () => { +// // Arrange +// const state = mergeState(initialState, {}); +// const { store } = configureMockStore({ state }); + +// // Act +// const { getAllByText, findByRole } = render( +// +// +// +// ); + +// // Assert +// // Also checking for proper row/col indexing +// const thumbnail = await findByRole("img"); +// expect(thumbnail.getAttribute("src")).to.include("some/path/to/my_image0.jpg"); +// expect(getAllByText("my_image0.czi")).to.not.be.empty; +// }); + +// it("renders file as thumbnail if file is renderable type", async () => { +// // Arrange +// const { store } = configureMockStore({ state: initialState }); + +// // Act +// const { getAllByText, findByRole } = render( +// +// +// +// ); + +// // Assert +// // Also confirms proper row/col indexing +// const thumbnail = await findByRole("img"); +// expect(thumbnail.getAttribute("src")).to.include("some/path/to/my_image9.jpg"); +// expect(getAllByText("my_image9.jpg")).to.not.be.empty; +// }); + +// it("renders svg as thumbnail if file has no renderable thumbnail", () => { +// // Arrange +// const { store } = configureMockStore({ state: initialState }); + +// // Act +// const { getAllByText, queryByRole } = render( +// +// +// +// ); + +// // Assert +// // Also confirms proper row/col indexing +// expect(".no-thumbnail").to.exist; +// expect(".svg").to.exist; +// expect(queryByRole("img")).not.to.exist; +// expect(getAllByText("my_image25.czi")).to.not.be.empty; +// }); + +// it("renders a loading indicator when data is not available", () => { +// // Arrange +// const { store } = configureMockStore({ state: initialState }); + +// // Act +// const { queryByText, queryAllByTestId } = render( +// +// +// +// ); + +// // Assert +// expect(queryByText("my_image")).to.equal(null); +// expect(queryAllByTestId("loading-spinner")).to.not.be.empty; +// }); + +// // We want to be able to render empty cells past the total item count in order to fill the grid +// it("renders an empty cell if the index is past the total item count", () => { +// // Arrange +// const { store } = configureMockStore({ state: initialState }); + +// // Act +// const { queryByText, queryAllByTestId } = render( +// +// +// +// ); + +// // Assert +// expect(queryByText("my_image")).to.equal(null); +// expect(queryAllByTestId("loading-spinner")).to.be.empty; +// }); + +// it("renders and indexes correctly with different number of columns", () => { +// // Arrange +// const state = { +// ...initialState, +// selection: { +// ...initialState.selection, +// fileGridColumnCount: 10, +// }, +// }; +// const { store } = configureMockStore({ state }); + +// // Act +// const { getAllByText } = render( +// +// +// +// ); + +// // Assert +// expect(".no-thumbnail").to.exist; +// expect(".svg").to.exist; +// expect(getAllByText("my_image25.czi")).to.not.be.empty; +// }); +// }); diff --git a/packages/core/components/Modal/MetadataManifest/test/MetadataManifest.test.tsx b/packages/core/components/Modal/MetadataManifest/test/MetadataManifest.test.tsx index fdf28fa6..20978993 100644 --- a/packages/core/components/Modal/MetadataManifest/test/MetadataManifest.test.tsx +++ b/packages/core/components/Modal/MetadataManifest/test/MetadataManifest.test.tsx @@ -19,10 +19,11 @@ import HttpFileService from "../../../../services/FileService/HttpFileService"; import FileDownloadServiceNoop from "../../../../services/FileDownloadService/FileDownloadServiceNoop"; describe("", () => { - const baseUrl = "test"; + const fileExplorerServiceBaseUrl = "TEST"; + const environment = "TEST"; const visibleDialogState = mergeState(initialState, { interaction: { - fileExplorerServiceBaseUrl: baseUrl, + environment: environment, visibleModal: ModalType.MetadataManifest, }, }); @@ -35,7 +36,7 @@ describe("", () => { }; const mockHttpClient = createMockHttpClient(responseStub); const fileService = new HttpFileService({ - baseUrl, + fileExplorerServiceBaseUrl, httpClient: mockHttpClient, downloadService: new FileDownloadServiceNoop(), }); diff --git a/packages/core/components/Modal/SmallScreenWarning/test/SmallScreenWarning.test.tsx b/packages/core/components/Modal/SmallScreenWarning/test/SmallScreenWarning.test.tsx index 91b76a10..ce446e63 100644 --- a/packages/core/components/Modal/SmallScreenWarning/test/SmallScreenWarning.test.tsx +++ b/packages/core/components/Modal/SmallScreenWarning/test/SmallScreenWarning.test.tsx @@ -8,10 +8,8 @@ import Modal, { ModalType } from "../.."; import { initialState, interaction, reduxLogics } from "../../../../state"; describe("", () => { - const baseUrl = "test"; const visibleDialogState = mergeState(initialState, { interaction: { - fileExplorerServiceBaseUrl: baseUrl, visibleModal: ModalType.SmallScreenWarning, }, }); diff --git a/packages/core/constants/index.ts b/packages/core/constants/index.ts index 7da5a343..0a6dabc8 100644 --- a/packages/core/constants/index.ts +++ b/packages/core/constants/index.ts @@ -5,17 +5,12 @@ import { AnnotationType } from "../entity/AnnotationFormatter"; export const APP_ID = "fms-file-explorer-core"; // Refer to packages/fms-file-explorer-electron/src/main/menu -export enum FileExplorerServiceBaseUrl { - LOCALHOST = "http://localhost:9081", - STAGING = "https://staging.int.allencell.org", - PRODUCTION = "https://production.int.allencell.org", -} - -export enum AicsLoadBalancerBaseUrl { - LOCALHOST = "http://localhost:8080", - STAGING = "http://stg-aics.corp.alleninstitute.org", - PRODUCTION = "http://aics.corp.alleninstitute.org", -} +export const Environment = { + LOCALHOST: "LOCALHOST", + STAGING: "STAGING", + PRODUCTION: "PRODUCTION", + TEST: "TEST", +} as const; export const TOP_LEVEL_FILE_ANNOTATIONS = [ new Annotation({ @@ -65,3 +60,24 @@ export const THUMBNAIL_SIZE_TO_NUM_COLUMNS = { }; export const AICS_FMS_DATA_SOURCE_NAME = "AICS FMS"; + +export const FESBaseUrlMap = { + LOCALHOST: "http://localhost:9081", + STAGING: "https://staging.int.allencell.org", + PRODUCTION: "https://production.int.allencell.org", + TEST: "http://test.int.allencell.org", +}; + +export const MMSBaseUrlMap = { + LOCALHOST: "http://localhost:9060", + STAGING: "http://stg-aics-api", + PRODUCTION: "http://prod-aics-api", + TEST: "http://test-aics-api", +}; + +export const LoadBalancerBaseUrlMap = { + LOCALHOST: "http://localhost:8080", + STAGING: "http://stg-aics.corp.alleninstitute.org", + PRODUCTION: "http://aics.corp.alleninstitute.org", + TEST: "http://test-aics.corp.alleninstitute.org", +}; diff --git a/packages/core/entity/FileSelection/test/FileSelection.test.ts b/packages/core/entity/FileSelection/test/FileSelection.test.ts index 5a12775a..a835dafb 100644 --- a/packages/core/entity/FileSelection/test/FileSelection.test.ts +++ b/packages/core/entity/FileSelection/test/FileSelection.test.ts @@ -344,7 +344,7 @@ describe("FileSelection", () => { describe("fetchAllDetails", () => { it("returns file details for each selected item", async () => { // Arrange - const baseUrl = "test"; + const fileExplorerServiceBaseUrl = "test"; const queryResult = []; for (let i = 0; i < 31; i++) { queryResult.push(i); @@ -354,13 +354,15 @@ describe("FileSelection", () => { .slice(1, 31) .map((detail) => new FileDetail(detail as any)); const httpClient = createMockHttpClient({ - when: `${baseUrl}/${HttpFileService.BASE_FILES_URL}?from=${0}&limit=${31}`, + when: `${fileExplorerServiceBaseUrl}/${ + HttpFileService.BASE_FILES_URL + }?from=${0}&limit=${31}`, respondWith: { data: { data: queryResult }, }, }); const fileService = new HttpFileService({ - baseUrl, + fileExplorerServiceBaseUrl, httpClient, downloadService: new FileDownloadServiceNoop(), }); diff --git a/packages/core/entity/FileSet/index.ts b/packages/core/entity/FileSet/index.ts index b68fe0b6..ae744ecf 100644 --- a/packages/core/entity/FileSet/index.ts +++ b/packages/core/entity/FileSet/index.ts @@ -84,7 +84,7 @@ export default class FileSet { * by using this as the component's `key` attribute. */ public get hash() { - return `${this.toQueryString()}:${this.fileService.baseUrl}`; + return `${this.toQueryString()}:${this.fileService.fileExplorerServiceBaseUrl}`; } public async fetchTotalCount() { @@ -210,7 +210,7 @@ export default class FileSet { public toJSON() { return { queryString: this.toQueryString(), - baseUrl: this.fileService.baseUrl, + fileExplorerServiceBaseUrl: this.fileService.fileExplorerServiceBaseUrl, }; } diff --git a/packages/core/entity/FileSet/test/FileSet.test.ts b/packages/core/entity/FileSet/test/FileSet.test.ts index 94851ae0..dd80c522 100644 --- a/packages/core/entity/FileSet/test/FileSet.test.ts +++ b/packages/core/entity/FileSet/test/FileSet.test.ts @@ -148,40 +148,40 @@ describe("FileSet", () => { }); it("turns indicies for requested data into a properly formed pagination query", async () => { - const baseUrl = "test"; + const fileExplorerServiceBaseUrl = "test"; const spec = [ { - expectedUrl: `${baseUrl}/${HttpFileService.BASE_FILES_URL}?from=1&limit=28`, + expectedUrl: `${fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}?from=1&limit=28`, start: 35, end: 55, }, { - expectedUrl: `${baseUrl}/${HttpFileService.BASE_FILES_URL}?from=11&limit=23`, + expectedUrl: `${fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}?from=11&limit=23`, start: 256, end: 274, }, { - expectedUrl: `${baseUrl}/${HttpFileService.BASE_FILES_URL}?from=0&limit=6`, + expectedUrl: `${fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}?from=0&limit=6`, start: 0, end: 5, }, { - expectedUrl: `${baseUrl}/${HttpFileService.BASE_FILES_URL}?from=1&limit=11`, + expectedUrl: `${fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}?from=1&limit=11`, start: 14, end: 21, }, { - expectedUrl: `${baseUrl}/${HttpFileService.BASE_FILES_URL}?from=0&limit=6`, + expectedUrl: `${fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}?from=0&limit=6`, start: 2, end: 5, }, { - expectedUrl: `${baseUrl}/${HttpFileService.BASE_FILES_URL}?from=3&limit=4`, + expectedUrl: `${fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}?from=3&limit=4`, start: 12, end: 15, }, { - expectedUrl: `${baseUrl}/${HttpFileService.BASE_FILES_URL}?from=0&limit=301`, + expectedUrl: `${fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}?from=0&limit=301`, start: 2, end: 300, }, @@ -203,7 +203,7 @@ describe("FileSet", () => { const fileSet = new FileSet({ fileService: new HttpFileService({ httpClient, - baseUrl, + fileExplorerServiceBaseUrl, downloadService: new FileDownloadServiceNoop(), }), }); diff --git a/packages/core/hooks/useOpenWithMenuItems/index.tsx b/packages/core/hooks/useOpenWithMenuItems/index.tsx index 832fe50d..ab543284 100644 --- a/packages/core/hooks/useOpenWithMenuItems/index.tsx +++ b/packages/core/hooks/useOpenWithMenuItems/index.tsx @@ -103,9 +103,9 @@ export default (fileDetails?: FileDetail, filters?: FileFilter[]): IContextualMe const annotationNameToAnnotationMap = useSelector( metadata.selectors.getAnnotationNameToAnnotationMap ); - const aicsLoadBalancerBaseUrl = useSelector(interaction.selectors.getAicsLoadBalancerBaseUrl); + const loadBalancerBaseUrl = useSelector(interaction.selectors.getLoadBalancerBaseUrl); - const plateLink = fileDetails?.getLinkToPlateUI(aicsLoadBalancerBaseUrl); + const plateLink = fileDetails?.getLinkToPlateUI(loadBalancerBaseUrl); const annotationNameToLinkMap = React.useMemo( () => fileDetails?.annotations diff --git a/packages/core/services/AnnotationService/HttpAnnotationService/index.ts b/packages/core/services/AnnotationService/HttpAnnotationService/index.ts index 3a6d7b64..0fdb9ff1 100644 --- a/packages/core/services/AnnotationService/HttpAnnotationService/index.ts +++ b/packages/core/services/AnnotationService/HttpAnnotationService/index.ts @@ -30,7 +30,7 @@ export default class HttpAnnotationService extends HttpServiceBase implements An * Fetch all annotations. */ public async fetchAnnotations(): Promise { - const requestUrl = `${this.baseUrl}/${HttpAnnotationService.BASE_ANNOTATION_URL}${this.pathSuffix}`; + const requestUrl = `${this.fileExplorerServiceBaseUrl}/${HttpAnnotationService.BASE_ANNOTATION_URL}${this.pathSuffix}`; const response = await this.get(requestUrl); return [ @@ -45,7 +45,7 @@ export default class HttpAnnotationService extends HttpServiceBase implements An public async fetchValues(annotation: string): Promise { // Encode any special characters in the annotation as necessary const encodedAnnotation = HttpServiceBase.encodeURISection(annotation); - const requestUrl = `${this.baseUrl}/${HttpAnnotationService.BASE_ANNOTATION_URL}/${encodedAnnotation}/values${this.pathSuffix}`; + const requestUrl = `${this.fileExplorerServiceBaseUrl}/${HttpAnnotationService.BASE_ANNOTATION_URL}/${encodedAnnotation}/values${this.pathSuffix}`; const response = await this.get(requestUrl); return response.data; @@ -70,7 +70,7 @@ export default class HttpAnnotationService extends HttpServiceBase implements An .filter((param) => !!param) .join("&"); - const requestUrl = `${this.baseUrl}/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_ROOT_URL}${this.pathSuffix}?${queryParams}`; + const requestUrl = `${this.fileExplorerServiceBaseUrl}/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_ROOT_URL}${this.pathSuffix}?${queryParams}`; const response = await this.get(requestUrl); return response.data; @@ -91,7 +91,7 @@ export default class HttpAnnotationService extends HttpServiceBase implements An ] .filter((param) => !!param) .join("&"); - const requestUrl = `${this.baseUrl}/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_UNDER_PATH_URL}${this.pathSuffix}?${queryParams}`; + const requestUrl = `${this.fileExplorerServiceBaseUrl}/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_UNDER_PATH_URL}${this.pathSuffix}?${queryParams}`; const response = await this.get(requestUrl); return response.data; @@ -103,7 +103,7 @@ export default class HttpAnnotationService extends HttpServiceBase implements An */ public async fetchAvailableAnnotationsForHierarchy(annotations: string[]): Promise { const queryParams = this.buildQueryParams(QueryParam.HIERARCHY, [...annotations].sort()); - const requestUrl = `${this.baseUrl}/${HttpAnnotationService.BASE_AVAILABLE_ANNOTATIONS_UNDER_HIERARCHY}${this.pathSuffix}?${queryParams}`; + const requestUrl = `${this.fileExplorerServiceBaseUrl}/${HttpAnnotationService.BASE_AVAILABLE_ANNOTATIONS_UNDER_HIERARCHY}${this.pathSuffix}?${queryParams}`; const response = await this.get(requestUrl); if (!response.data) { diff --git a/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts b/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts index 5ac7d911..c23ae799 100644 --- a/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts +++ b/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts @@ -21,7 +21,10 @@ describe("HttpAnnotationService", () => { }); it("issues request for all available Annotations", async () => { - const annotationService = new HttpAnnotationService({ baseUrl: "test", httpClient }); + const annotationService = new HttpAnnotationService({ + fileExplorerServiceBaseUrl: "test", + httpClient, + }); const annotations = await annotationService.fetchAnnotations(); expect(annotations.length).to.equal( annotationsJson.length + TOP_LEVEL_FILE_ANNOTATION_NAMES.length @@ -43,7 +46,10 @@ describe("HttpAnnotationService", () => { }, }); - const annotationService = new HttpAnnotationService({ baseUrl: "test", httpClient }); + const annotationService = new HttpAnnotationService({ + fileExplorerServiceBaseUrl: "test", + httpClient, + }); const actualValues = await annotationService.fetchValues(annotation); expect(actualValues.length).to.equal(values.length); expect(actualValues).to.be.deep.equal(values); @@ -62,7 +68,10 @@ describe("HttpAnnotationService", () => { }, }); - const annotationService = new HttpAnnotationService({ baseUrl: "test", httpClient }); + const annotationService = new HttpAnnotationService({ + fileExplorerServiceBaseUrl: "test", + httpClient, + }); const values = await annotationService.fetchRootHierarchyValues(["foo"], []); expect(values).to.equal(expectedValues); }); @@ -80,7 +89,10 @@ describe("HttpAnnotationService", () => { }); const getSpy = spy(httpClient, "get"); - const annotationService = new HttpAnnotationService({ baseUrl: "test", httpClient }); + const annotationService = new HttpAnnotationService({ + fileExplorerServiceBaseUrl: "test", + httpClient, + }); // first time around const firstCallRet = await annotationService.fetchRootHierarchyValues( @@ -113,7 +125,10 @@ describe("HttpAnnotationService", () => { }, }); - const annotationService = new HttpAnnotationService({ baseUrl: "test", httpClient }); + const annotationService = new HttpAnnotationService({ + fileExplorerServiceBaseUrl: "test", + httpClient, + }); const filter = new FileFilter("bar", "barValue"); const values = await annotationService.fetchRootHierarchyValues(["foo"], [filter]); expect(values).to.equal(expectedValues); @@ -132,7 +147,10 @@ describe("HttpAnnotationService", () => { }, }); - const annotationService = new HttpAnnotationService({ baseUrl: "test", httpClient }); + const annotationService = new HttpAnnotationService({ + fileExplorerServiceBaseUrl: "test", + httpClient, + }); const values = await annotationService.fetchHierarchyValuesUnderPath( ["foo", "bar"], ["baz"], @@ -152,7 +170,10 @@ describe("HttpAnnotationService", () => { }, }); - const annotationService = new HttpAnnotationService({ baseUrl: "test", httpClient }); + const annotationService = new HttpAnnotationService({ + fileExplorerServiceBaseUrl: "test", + httpClient, + }); const filter = new FileFilter("bar", "barValue"); const values = await annotationService.fetchHierarchyValuesUnderPath( ["foo", "bar"], @@ -181,7 +202,10 @@ describe("HttpAnnotationService", () => { ...annotationsFromServer, ...hierarchy, ]; - const annotationService = new HttpAnnotationService({ baseUrl: "test", httpClient }); + const annotationService = new HttpAnnotationService({ + fileExplorerServiceBaseUrl: "test", + httpClient, + }); const values = await annotationService.fetchAvailableAnnotationsForHierarchy(hierarchy); expect(values.sort()).to.deep.equal(expectedValues.sort()); }); diff --git a/packages/core/services/FileService/HttpFileService/index.ts b/packages/core/services/FileService/HttpFileService/index.ts index 24f041c5..3289dcb0 100644 --- a/packages/core/services/FileService/HttpFileService/index.ts +++ b/packages/core/services/FileService/HttpFileService/index.ts @@ -40,7 +40,9 @@ export default class HttpFileService extends HttpServiceBase implements FileServ */ public async isNetworkAccessible(): Promise { try { - await this.get(`${this.baseUrl}/${HttpFileService.BASE_FILE_COUNT_URL}`); + await this.get( + `${this.fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILE_COUNT_URL}` + ); return true; } catch (error) { console.error(`Unable to access AICS network ${error}`); @@ -51,7 +53,7 @@ export default class HttpFileService extends HttpServiceBase implements FileServ public async getCountOfMatchingFiles(fileSet: FileSet): Promise { const requestUrl = join( compact([ - `${this.baseUrl}/${HttpFileService.BASE_FILE_COUNT_URL}${this.pathSuffix}`, + `${this.fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILE_COUNT_URL}${this.pathSuffix}`, fileSet.toQueryString(), ]), "?" @@ -72,7 +74,7 @@ export default class HttpFileService extends HttpServiceBase implements FileServ ): Promise { const selections = fileSelection.toCompactSelectionList(); const postBody: SelectionAggregationRequest = { selections }; - const requestUrl = `${this.baseUrl}/${HttpFileService.SELECTION_AGGREGATE_URL}${this.pathSuffix}`; + const requestUrl = `${this.fileExplorerServiceBaseUrl}/${HttpFileService.SELECTION_AGGREGATE_URL}${this.pathSuffix}`; const response = await this.post( requestUrl, @@ -93,7 +95,7 @@ export default class HttpFileService extends HttpServiceBase implements FileServ public async getFiles(request: GetFilesRequest): Promise { const { from, limit, fileSet } = request; - const base = `${this.baseUrl}/${HttpFileService.BASE_FILES_URL}${this.pathSuffix}?from=${from}&limit=${limit}`; + const base = `${this.fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}${this.pathSuffix}?from=${from}&limit=${limit}`; const requestUrl = join(compact([base, fileSet.toQueryString()]), "&"); const response = await this.get(requestUrl); @@ -115,7 +117,7 @@ export default class HttpFileService extends HttpServiceBase implements FileServ } const postData = JSON.stringify({ annotations, selections }); - const url = `${this.baseUrl}/${HttpFileService.BASE_CSV_DOWNLOAD_URL}${this.pathSuffix}`; + const url = `${this.fileExplorerServiceBaseUrl}/${HttpFileService.BASE_CSV_DOWNLOAD_URL}${this.pathSuffix}`; const manifest = await this.downloadService.prepareHttpResourceForDownload(url, postData); const name = `file-manifest-${new Date()}.csv`; @@ -137,7 +139,7 @@ export default class HttpFileService extends HttpServiceBase implements FileServ fileIds: string[], username?: string ): Promise<{ cacheFileStatuses: { [fileId: string]: string } }> { - const requestUrl = `${this.aicsLoadBalancerBaseUrl}/${HttpFileService.BASE_FILE_CACHE_URL}${this.pathSuffix}`; + const requestUrl = `${this.loadBalancerBaseUrl}/${HttpFileService.BASE_FILE_CACHE_URL}${this.pathSuffix}`; const requestBody = JSON.stringify({ fileIds }); const headers = { "Content-Type": "application/json", diff --git a/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts b/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts index a53564a5..37db4304 100644 --- a/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts +++ b/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts @@ -8,8 +8,8 @@ import NumericRange from "../../../../entity/NumericRange"; import FileDownloadServiceNoop from "../../../FileDownloadService/FileDownloadServiceNoop"; describe("HttpFileService", () => { - const baseUrl = "test"; - const aicsLoadBalancerBaseUrlMock = "http://loadbalancer-test.aics.corp.alleninstitute.org"; + const fileExplorerServiceBaseUrl = "TEST"; + const loadBalancerBaseUrlMock = "http://loadbalancer-test.aics.corp.alleninstitute.org"; const fileIds = ["abc123", "def456", "ghi789", "jkl012"]; const files = fileIds.map((file_id) => ({ file_id, @@ -29,7 +29,7 @@ describe("HttpFileService", () => { it("issues request for files that match given parameters", async () => { const httpFileService = new HttpFileService({ - baseUrl: baseUrl, + fileExplorerServiceBaseUrl: fileExplorerServiceBaseUrl, httpClient, downloadService: new FileDownloadServiceNoop(), }); @@ -49,7 +49,7 @@ describe("HttpFileService", () => { const totalFileSize = 12424114; const totalFileCount = 7; const httpClient = createMockHttpClient({ - when: `${baseUrl}/${HttpFileService.SELECTION_AGGREGATE_URL}`, + when: `${fileExplorerServiceBaseUrl}/${HttpFileService.SELECTION_AGGREGATE_URL}`, respondWith: { data: { data: [{ count: totalFileCount, size: totalFileSize }], @@ -60,7 +60,7 @@ describe("HttpFileService", () => { it("issues request for aggregated information about given files", async () => { // Arrange const fileService = new HttpFileService({ - baseUrl, + fileExplorerServiceBaseUrl, httpClient, downloadService: new FileDownloadServiceNoop(), }); @@ -81,7 +81,7 @@ describe("HttpFileService", () => { describe("cacheFiles", () => { const httpClient = createMockHttpClient({ - when: `${aicsLoadBalancerBaseUrlMock}/${HttpFileService.BASE_FILE_CACHE_URL}`, + when: `${loadBalancerBaseUrlMock}/${HttpFileService.BASE_FILE_CACHE_URL}`, respondWith: { data: { cacheFileStatuses: { @@ -95,7 +95,7 @@ describe("HttpFileService", () => { it("sends file IDs to be cached and returns their statuses", async () => { // Arrange const fileService = new HttpFileService({ - aicsLoadBalancerBaseUrl: aicsLoadBalancerBaseUrlMock, + loadBalancerBaseUrl: loadBalancerBaseUrlMock, httpClient, downloadService: new FileDownloadServiceNoop(), }); diff --git a/packages/core/services/FileService/index.ts b/packages/core/services/FileService/index.ts index a540f2da..9b5a5347 100644 --- a/packages/core/services/FileService/index.ts +++ b/packages/core/services/FileService/index.ts @@ -39,7 +39,7 @@ export interface Selection { } export default interface FileService { - baseUrl?: string; + fileExplorerServiceBaseUrl?: string; download( annotations: string[], selections: Selection[], diff --git a/packages/core/services/HttpServiceBase/index.ts b/packages/core/services/HttpServiceBase/index.ts index 3516272f..48b62112 100644 --- a/packages/core/services/HttpServiceBase/index.ts +++ b/packages/core/services/HttpServiceBase/index.ts @@ -2,22 +2,24 @@ import axios, { AxiosInstance } from "axios"; import { Policy } from "cockatiel"; import LRUCache from "lru-cache"; -import { AicsLoadBalancerBaseUrl, FileExplorerServiceBaseUrl } from "../../constants"; +import { FESBaseUrlMap, LoadBalancerBaseUrlMap, MMSBaseUrlMap } from "../../constants"; import RestServiceResponse from "../../entity/RestServiceResponse"; export interface ConnectionConfig { - aicsLoadBalancerBaseUrl?: string | keyof typeof AicsLoadBalancerBaseUrl; applicationVersion?: string; - baseUrl?: string | keyof typeof FileExplorerServiceBaseUrl; + fileExplorerServiceBaseUrl?: string | keyof typeof FESBaseUrlMap; httpClient?: AxiosInstance; + loadBalancerBaseUrl?: string | keyof typeof LoadBalancerBaseUrlMap; + metadataManagementServiceBaseURl?: string | keyof typeof MMSBaseUrlMap; pathSuffix?: string; userName?: string; } export const DEFAULT_CONNECTION_CONFIG = { - aicsLoadBalancerBaseUrl: AicsLoadBalancerBaseUrl.PRODUCTION, - baseUrl: FileExplorerServiceBaseUrl.PRODUCTION, + fileExplorerServiceBaseUrl: FESBaseUrlMap.PRODUCTION, httpClient: axios.create(), + loadBalancerBaseUrl: LoadBalancerBaseUrlMap.PRODUCTION, + metadataManagementServiceBaseURl: MMSBaseUrlMap.PRODUCTION, }; const CHARACTER_TO_ENCODING_MAP: { [index: string]: string } = { @@ -47,7 +49,7 @@ const retry = Policy.handleAll() }); /** - * Base class for services that interact with AICS APIs. + * Base class for services that interact with APIs. */ export default class HttpServiceBase { /** @@ -68,7 +70,7 @@ export default class HttpServiceBase { } // encode ampersands that do not separate query string components, so first - // need to separate the query string componenets (which are split by ampersands themselves) + // need to separate the query string components (which are split by ampersands themselves) // handles case like `workflow=R&DExp&cell_line=AICS-46&foo=bar&cTnT%=3.0` const re = /&(?=(?:[^&])+\=)/g; const queryStringComponents = queryString.split(re); @@ -99,10 +101,11 @@ export default class HttpServiceBase { .join(""); } - public aicsLoadBalancerBaseUrl: string | keyof typeof AicsLoadBalancerBaseUrl = - DEFAULT_CONNECTION_CONFIG.aicsLoadBalancerBaseUrl; - public baseUrl: string | keyof typeof FileExplorerServiceBaseUrl = - DEFAULT_CONNECTION_CONFIG.baseUrl; + public fileExplorerServiceBaseUrl: string = + DEFAULT_CONNECTION_CONFIG.fileExplorerServiceBaseUrl; + public loadBalancerBaseUrl: string = DEFAULT_CONNECTION_CONFIG.loadBalancerBaseUrl; + public metadataManagementServiceBaseURl: string = + DEFAULT_CONNECTION_CONFIG.metadataManagementServiceBaseURl; protected httpClient = DEFAULT_CONNECTION_CONFIG.httpClient; private applicationVersion = "NOT SET"; @@ -111,10 +114,6 @@ export default class HttpServiceBase { private readonly urlToResponseDataCache = new LRUCache({ max: MAX_CACHE_SIZE }); constructor(config: ConnectionConfig = {}) { - if (config.aicsLoadBalancerBaseUrl) { - this.setAicsLoadBalancerBaseUrl(config.aicsLoadBalancerBaseUrl); - } - if (config.applicationVersion) { this.setApplicationVersion(config.applicationVersion); } @@ -123,14 +122,22 @@ export default class HttpServiceBase { this.setUserName(config.userName); } - if (config.baseUrl) { - this.setBaseUrl(config.baseUrl); + if (config.fileExplorerServiceBaseUrl) { + this.setFileExplorerServiceBaseUrl(config.fileExplorerServiceBaseUrl); } if (config.httpClient) { this.setHttpClient(config.httpClient); } + if (config.loadBalancerBaseUrl) { + this.setLoadBalancerBaseUrl(config.loadBalancerBaseUrl); + } + + if (config.metadataManagementServiceBaseURl) { + this.setLoadBalancerBaseUrl(config.metadataManagementServiceBaseURl); + } + if (config.pathSuffix) { this.pathSuffix = config.pathSuffix; } @@ -269,34 +276,20 @@ export default class HttpServiceBase { return new RestServiceResponse(response.data); } - public setAicsLoadBalancerBaseUrl( - aicsLoadBalancerBaseUrl: string | keyof typeof AicsLoadBalancerBaseUrl - ) { - if (this.aicsLoadBalancerBaseUrl !== aicsLoadBalancerBaseUrl) { - // bust cache when base url changes - this.urlToResponseDataCache.reset(); - } - - this.aicsLoadBalancerBaseUrl = aicsLoadBalancerBaseUrl; - } - public setApplicationVersion(applicationVersion: string) { this.applicationVersion = applicationVersion; this.setHeaders(); } - public setUserName(userName: string) { - this.userName = userName; - this.setHeaders(); - } - - public setBaseUrl(baseUrl: string | keyof typeof FileExplorerServiceBaseUrl) { - if (this.baseUrl !== baseUrl) { + public setFileExplorerServiceBaseUrl( + fileExplorerServiceBaseUrl: string | keyof typeof FESBaseUrlMap + ) { + if (this.fileExplorerServiceBaseUrl !== fileExplorerServiceBaseUrl) { // bust cache when base url changes this.urlToResponseDataCache.reset(); } - this.baseUrl = baseUrl; + this.fileExplorerServiceBaseUrl = fileExplorerServiceBaseUrl; } public setHttpClient(client: AxiosInstance) { @@ -319,4 +312,31 @@ export default class HttpServiceBase { delete this.httpClient.defaults.headers.common["X-User-Id"]; } } + + public setLoadBalancerBaseUrl( + loadBalancerBaseUrl: string | keyof typeof LoadBalancerBaseUrlMap + ) { + if (this.loadBalancerBaseUrl !== loadBalancerBaseUrl) { + // bust cache when base url changes + this.urlToResponseDataCache.reset(); + } + + this.loadBalancerBaseUrl = loadBalancerBaseUrl; + } + + public setMetadataManagementServiceBaseURl( + metadataManagementServiceBaseURl: string | keyof typeof MMSBaseUrlMap + ) { + if (this.metadataManagementServiceBaseURl !== metadataManagementServiceBaseURl) { + // bust cache when base url changes + this.urlToResponseDataCache.reset(); + } + + this.metadataManagementServiceBaseURl = metadataManagementServiceBaseURl; + } + + public setUserName(userName: string) { + this.userName = userName; + this.setHeaders(); + } } diff --git a/packages/core/state/interaction/actions.ts b/packages/core/state/interaction/actions.ts index e5f98007..eca0a8c5 100644 --- a/packages/core/state/interaction/actions.ts +++ b/packages/core/state/interaction/actions.ts @@ -260,10 +260,7 @@ export interface InitializeApp { payload: string; } -export const initializeApp = (payload: { - aicsLoadBalancerBaseUrl: string; - fileExplorerServiceBaseUrl: string; -}) => ({ +export const initializeApp = (payload: { environment: string }) => ({ type: INITIALIZE_APP, payload, }); diff --git a/packages/core/state/interaction/reducer.ts b/packages/core/state/interaction/reducer.ts index f8bea820..fde4558e 100644 --- a/packages/core/state/interaction/reducer.ts +++ b/packages/core/state/interaction/reducer.ts @@ -35,15 +35,15 @@ import { PlatformDependentServices } from "../../services"; import ApplicationInfoServiceNoop from "../../services/ApplicationInfoService/ApplicationInfoServiceNoop"; import FileDownloadServiceNoop from "../../services/FileDownloadService/FileDownloadServiceNoop"; import FileViewerServiceNoop from "../../services/FileViewerService/FileViewerServiceNoop"; -import { DEFAULT_CONNECTION_CONFIG } from "../../services/HttpServiceBase"; import ExecutionEnvServiceNoop from "../../services/ExecutionEnvService/ExecutionEnvServiceNoop"; import { UserSelectedApplication } from "../../services/PersistentConfigService"; import NotificationServiceNoop from "../../services/NotificationService/NotificationServiceNoop"; import DatabaseServiceNoop from "../../services/DatabaseService/DatabaseServiceNoop"; import PublicDataset from "../../../web/src/entity/PublicDataset"; +import { Environment } from "../../constants"; + export interface InteractionStateBranch { - aicsLoadBalancerBaseUrl: string; applicationVersion?: string; contextMenuIsVisible: boolean; contextMenuItems: ContextMenuItem[]; @@ -52,9 +52,9 @@ export interface InteractionStateBranch { csvColumns?: string[]; dataSourceInfoForVisibleModal?: DataSourcePromptInfo; datasetDetailsPanelIsVisible: boolean; - fileExplorerServiceBaseUrl: string; fileTypeForVisibleModal: "csv" | "json" | "parquet"; fileFiltersForVisibleModal: FileFilter[]; + environment: "LOCALHOST" | "STAGING" | "PRODUCTION" | "TEST"; hasDismissedSmallScreenWarning: boolean; hasUsedApplicationBefore: boolean; isAicsEmployee?: boolean; @@ -68,16 +68,11 @@ export interface InteractionStateBranch { } export const initialState: InteractionStateBranch = { - aicsLoadBalancerBaseUrl: DEFAULT_CONNECTION_CONFIG.aicsLoadBalancerBaseUrl, + environment: Environment.PRODUCTION, contextMenuIsVisible: false, contextMenuItems: [], - // Passed to `ContextualMenu` as `target`. From the "@fluentui/react" docs: - // "The target that ContextualMenu should try to position itself based on. - // It can be either an element, a query selector string resolving to a valid element, or a MouseEvent. - // If a MouseEvent is given, the origin point of the event will be used." contextMenuPositionReference: null, datasetDetailsPanelIsVisible: false, - fileExplorerServiceBaseUrl: DEFAULT_CONNECTION_CONFIG.baseUrl, fileFiltersForVisibleModal: [], fileTypeForVisibleModal: "csv", hasDismissedSmallScreenWarning: false, @@ -90,7 +85,6 @@ export const initialState: InteractionStateBranch = { fileViewerService: new FileViewerServiceNoop(), frontendInsights: new FrontendInsights({ application: { - // Kept old name to compare usage more easily in Amplitude UI name: "FMS File Explorer", version: "0.0.0-noop", }, @@ -168,8 +162,7 @@ export default makeReducer( }), [INITIALIZE_APP]: (state, action) => ({ ...state, - aicsLoadBalancerBaseUrl: action.payload.aicsLoadBalancerBaseUrl, - fileExplorerServiceBaseUrl: action.payload.fileExplorerServiceBaseUrl, + environment: action.payload.environment, }), [SET_VISIBLE_MODAL]: (state, action) => ({ ...state, diff --git a/packages/core/state/interaction/selectors.ts b/packages/core/state/interaction/selectors.ts index 75c852b6..5808b288 100644 --- a/packages/core/state/interaction/selectors.ts +++ b/packages/core/state/interaction/selectors.ts @@ -14,11 +14,15 @@ import DatabaseFileService from "../../services/FileService/DatabaseFileService" import HttpAnnotationService from "../../services/AnnotationService/HttpAnnotationService"; import HttpFileService from "../../services/FileService/HttpFileService"; import { ModalType } from "../../components/Modal"; -import { AICS_FMS_DATA_SOURCE_NAME } from "../../constants"; +import { + AICS_FMS_DATA_SOURCE_NAME, + FESBaseUrlMap, + MMSBaseUrlMap, + LoadBalancerBaseUrlMap, +} from "../../constants"; // BASIC SELECTORS -export const getAicsLoadBalancerBaseUrl = (state: State) => - state.interaction.aicsLoadBalancerBaseUrl; +export const getEnvironment = (state: State) => state.interaction.environment; export const getContextMenuVisibility = (state: State) => state.interaction.contextMenuIsVisible; export const getContextMenuItems = (state: State) => state.interaction.contextMenuItems; export const getContextMenuPositionReference = (state: State) => @@ -30,8 +34,6 @@ export const getDataSourceInfoForVisibleModal = (state: State) => export const getDatasetDetailsVisibility = (state: State) => state.interaction.datasetDetailsPanelIsVisible; export const getSelectedPublicDataset = (state: State) => state.interaction.selectedPublicDataset; -export const getFileExplorerServiceBaseUrl = (state: State) => - state.interaction.fileExplorerServiceBaseUrl; export const getFileFiltersForVisibleModal = (state: State) => state.interaction.fileFiltersForVisibleModal; export const getFileTypeForVisibleModal = (state: State) => @@ -50,6 +52,22 @@ export const getUserSelectedApplications = (state: State) => export const getVisibleModal = (state: State) => state.interaction.visibleModal; export const isAicsEmployee = (state: State) => state.interaction.isAicsEmployee; +// URL Mapping Selectors +export const getFileExplorerServiceBaseUrl = createSelector( + [getEnvironment], + (environment) => FESBaseUrlMap[environment] +); + +export const getLoadBalancerBaseUrl = createSelector( + [getEnvironment], + (environment) => LoadBalancerBaseUrlMap[environment] +); + +export const getMetadataManagementServiceBaseUrl = createSelector( + [getEnvironment], + (environment) => MMSBaseUrlMap[environment] +); + // COMPOSED SELECTORS export const getApplicationVersion = createSelector( [getPlatformDependentServices], @@ -103,24 +121,27 @@ export const getUserName = createSelector( export const getHttpFileService = createSelector( [ getApplicationVersion, - getUserName, - getAicsLoadBalancerBaseUrl, getFileExplorerServiceBaseUrl, + getLoadBalancerBaseUrl, + getMetadataManagementServiceBaseUrl, + getUserName, getPlatformDependentServices, getRefreshKey, ], ( applicationVersion, + fileExplorerServiceBaseUrl, + loadBalancerBaseUrl, + metadataManagementServiceBaseURL, userName, - aicsLoadBalancerBaseUrl, - fileExplorerBaseUrl, platformDependentServices ) => new HttpFileService({ applicationVersion, + loadBalancerBaseUrl: loadBalancerBaseUrl, + fileExplorerServiceBaseUrl: fileExplorerServiceBaseUrl, + metadataManagementServiceBaseURl: metadataManagementServiceBaseURL, userName, - aicsLoadBalancerBaseUrl: aicsLoadBalancerBaseUrl, - baseUrl: fileExplorerBaseUrl, downloadService: platformDependentServices.fileDownloadService, }) ); @@ -163,7 +184,6 @@ export const getAnnotationService = createSelector( [ getApplicationVersion, getUserName, - getAicsLoadBalancerBaseUrl, getFileExplorerServiceBaseUrl, getSelectedDataSources, getPlatformDependentServices, @@ -172,8 +192,7 @@ export const getAnnotationService = createSelector( ( applicationVersion, userName, - aicsLoadBalancerBaseUrl, - fileExplorerBaseUrl, + fileExplorerServiceBaseUrl, dataSources, platformDependentServices ): AnnotationService => { @@ -186,26 +205,18 @@ export const getAnnotationService = createSelector( return new HttpAnnotationService({ applicationVersion, userName, - aicsLoadBalancerBaseUrl: aicsLoadBalancerBaseUrl, - baseUrl: fileExplorerBaseUrl, + fileExplorerServiceBaseUrl: fileExplorerServiceBaseUrl, }); } ); export const getDatasetService = createSelector( - [ - getApplicationVersion, - getUserName, - getAicsLoadBalancerBaseUrl, - getFileExplorerServiceBaseUrl, - getRefreshKey, - ], - (applicationVersion, userName, aicsLoadBalancerBaseUrl, fileExplorerBaseUrl) => + [getApplicationVersion, getUserName, getFileExplorerServiceBaseUrl, getRefreshKey], + (applicationVersion, userName, fileExplorerServiceBaseUrl) => new DatasetService({ applicationVersion, userName, - aicsLoadBalancerBaseUrl: aicsLoadBalancerBaseUrl, - baseUrl: fileExplorerBaseUrl, + fileExplorerServiceBaseUrl: fileExplorerServiceBaseUrl, }) ); diff --git a/packages/core/state/interaction/test/logics.test.ts b/packages/core/state/interaction/test/logics.test.ts index 688c5532..5b539173 100644 --- a/packages/core/state/interaction/test/logics.test.ts +++ b/packages/core/state/interaction/test/logics.test.ts @@ -206,7 +206,7 @@ describe("Interaction logics", () => { it("doesn't use selected files when given a specific file folder path", async () => { // arrange - const baseUrl = "test"; + const fileExplorerServiceBaseUrl = "TEST"; const filters = [ new FileFilter("Cell Line", "AICS-12"), new FileFilter("Notes", "Hello"), @@ -215,7 +215,6 @@ describe("Interaction logics", () => { const state = mergeState(initialState, { interaction: { fileFiltersForVisibleModal: filters, - fileExplorerServiceBaseUrl: baseUrl, platformDependentServices: { fileDownloadService: new FileDownloadServiceNoop(), }, @@ -232,7 +231,7 @@ describe("Interaction logics", () => { }; const mockHttpClient = createMockHttpClient(responseStub); const fileService = new HttpFileService({ - baseUrl, + fileExplorerServiceBaseUrl, httpClient: mockHttpClient, downloadService: new FileDownloadServiceNoop(), }); @@ -776,12 +775,12 @@ describe("Interaction logics", () => { describe("refresh", () => { const sandbox = createSandbox(); - const baseUrl = "test"; + const fileExplorerServiceBaseUrl = "test"; const annotations = annotationsJson.map((annotation) => new Annotation(annotation)); const availableAnnotations = [annotations[1].displayName]; const responseStubs = [ { - when: `${baseUrl}/${HttpAnnotationService.BASE_ANNOTATION_URL}`, + when: `${fileExplorerServiceBaseUrl}/${HttpAnnotationService.BASE_ANNOTATION_URL}`, respondWith: { data: { data: annotations }, }, @@ -798,7 +797,7 @@ describe("Interaction logics", () => { ]; const mockHttpClient = createMockHttpClient(responseStubs); const annotationService = new HttpAnnotationService({ - baseUrl, + fileExplorerServiceBaseUrl, httpClient: mockHttpClient, }); @@ -883,7 +882,7 @@ describe("Interaction logics", () => { ], }); } - const baseUrl = "test"; + const fileExplorerServiceBaseUrl = "test"; const responseStub = { when: () => true, respondWith: { @@ -892,7 +891,7 @@ describe("Interaction logics", () => { }; const mockHttpClient = createMockHttpClient(responseStub); const fileService = new HttpFileService({ - baseUrl, + fileExplorerServiceBaseUrl, httpClient: mockHttpClient, downloadService: new FileDownloadServiceNoop(), }); @@ -1099,16 +1098,16 @@ describe("Interaction logics", () => { }); } const files = [...csvFiles, ...pngFiles]; - const baseUrl = "test"; + const fileExplorerServiceBaseUrl = "test"; const responseStub = { - when: `${baseUrl}/${HttpFileService.BASE_FILES_URL}?from=0&limit=101`, + when: `${fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}?from=0&limit=101`, respondWith: { data: { data: files }, }, }; const mockHttpClient = createMockHttpClient(responseStub); const fileService = new HttpFileService({ - baseUrl, + fileExplorerServiceBaseUrl, httpClient: mockHttpClient, downloadService: new FileDownloadServiceNoop(), }); @@ -1214,16 +1213,16 @@ describe("Interaction logics", () => { for (let i = 0; i <= 100; i++) { files.push({ file_path: `/allen/file_${i}.ext` }); } - const baseUrl = "test"; + const fileExplorerServiceBaseUrl = "test"; const responseStub = { - when: `${baseUrl}/${HttpFileService.BASE_FILES_URL}?from=0&limit=101`, + when: `${fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}?from=0&limit=101`, respondWith: { data: { data: files }, }, }; const mockHttpClient = createMockHttpClient(responseStub); const fileService = new HttpFileService({ - baseUrl, + fileExplorerServiceBaseUrl, httpClient: mockHttpClient, downloadService: new FileDownloadServiceNoop(), }); diff --git a/packages/core/state/metadata/test/logics.test.ts b/packages/core/state/metadata/test/logics.test.ts index 9edb3717..163f2a25 100644 --- a/packages/core/state/metadata/test/logics.test.ts +++ b/packages/core/state/metadata/test/logics.test.ts @@ -19,11 +19,7 @@ describe("Metadata logics", () => { describe("requestAnnotations", () => { it("Fires RECEIVE_ANNOTATIONS action after processing REQUEST_ANNOTATIONS action", async () => { // arrange - const state = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: "test", - }, - }); + const state = mergeState(initialState, {}); const responseStub = { when: () => true, diff --git a/packages/core/state/selection/test/logics.test.ts b/packages/core/state/selection/test/logics.test.ts index 662ff2b0..387062f4 100644 --- a/packages/core/state/selection/test/logics.test.ts +++ b/packages/core/state/selection/test/logics.test.ts @@ -328,10 +328,10 @@ describe("Selection logics", () => { }, }, ]; - const baseUrl = "test"; + const fileExplorerServiceBaseUrl = "test"; const mockHttpClient = createMockHttpClient(responseStubs); const fileService = new HttpFileService({ - baseUrl, + fileExplorerServiceBaseUrl, httpClient: mockHttpClient, downloadService: new FileDownloadServiceNoop(), }); @@ -352,9 +352,6 @@ describe("Selection logics", () => { it("selects file above current focused row", async () => { // Arrange const state = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: baseUrl, - }, selection: { fileSelection: new FileSelection() .select({ @@ -396,9 +393,6 @@ describe("Selection logics", () => { sortOrder: 1, }); const state = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: baseUrl, - }, selection: { fileSelection: new FileSelection().select({ fileSet, @@ -460,9 +454,6 @@ describe("Selection logics", () => { it("adds a new annotation to the end of the hierarchy", async () => { // setup const state = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: "test", - }, metadata: { annotations: [...annotations], }, @@ -492,9 +483,6 @@ describe("Selection logics", () => { it("moves an annotation within the hierarchy to a new position", async () => { // setup const state = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: "test", - }, metadata: { annotations: [...annotations], }, @@ -538,9 +526,6 @@ describe("Selection logics", () => { // ones to test proper comparison using annotationName const annotationHierarchy = annotations.slice(0, 4).map((a) => a.name); const state = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: "test", - }, metadata: { annotations: [...annotations], }, @@ -578,9 +563,6 @@ describe("Selection logics", () => { new FileFolder(["AICS-0", "false"]), ]; const state = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: "test", - }, metadata: { annotations: [...annotations], }, @@ -610,9 +592,6 @@ describe("Selection logics", () => { it("determines which paths can still be opened after annotation is added", async () => { // setup const state = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: "test", - }, metadata: { annotations: [...annotations], }, @@ -645,9 +624,6 @@ describe("Selection logics", () => { it("determines which paths can still be opened after annotation is removed", async () => { // setup const state = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: "test", - }, metadata: { annotations: [...annotations], }, @@ -709,9 +685,6 @@ describe("Selection logics", () => { it("sets available annotations", async () => { // Arrange const state = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: "test", - }, metadata: { annotations: [...annotations], }, @@ -750,9 +723,6 @@ describe("Selection logics", () => { it("sets all annotations as available when actual cannot be found", async () => { // Arrange const state = mergeState(initialState, { - interaction: { - fileExplorerServiceBaseUrl: "test", - }, metadata: { annotations: [...annotations], }, diff --git a/packages/core/state/selection/test/reducer.test.ts b/packages/core/state/selection/test/reducer.test.ts index 232e1ca5..155b83a1 100644 --- a/packages/core/state/selection/test/reducer.test.ts +++ b/packages/core/state/selection/test/reducer.test.ts @@ -17,8 +17,7 @@ describe("Selection reducer", () => { [ selection.actions.setAnnotationHierarchy([]), interaction.actions.initializeApp({ - fileExplorerServiceBaseUrl: "base", - aicsLoadBalancerBaseUrl: "loadBalancerBaseUrl", + environment: "TEST", }), ].forEach((expectedAction) => it(`clears selected file state when ${expectedAction.type} is fired`, () => { diff --git a/packages/desktop/src/main/global.d.ts b/packages/desktop/src/main/global.d.ts index c28d2c8b..335ad1fa 100644 --- a/packages/desktop/src/main/global.d.ts +++ b/packages/desktop/src/main/global.d.ts @@ -1,4 +1,3 @@ /*eslint no-var: "off"*/ -// necessary in order to do: global.fileExplorerServiceBaseUrl = "..." -declare var aicsLoadBalancerBaseUrl: string; -declare var fileExplorerServiceBaseUrl: string; +// necessary in order to do: global.environment = "..." +declare var environment: string; diff --git a/packages/desktop/src/main/menu/data-source.ts b/packages/desktop/src/main/menu/data-source.ts index 9b73a358..930575fa 100644 --- a/packages/desktop/src/main/menu/data-source.ts +++ b/packages/desktop/src/main/menu/data-source.ts @@ -1,14 +1,9 @@ import { MenuItemConstructorOptions } from "electron"; -import { - GlobalVariableChannels, - AicsLoadBalancerBaseUrl, - FileExplorerServiceBaseUrl, -} from "../../util/constants"; +import { GlobalVariableChannels, Environment } from "../../util/constants"; // Least effort state management accessible to both the main and renderer processes. -global.aicsLoadBalancerBaseUrl = AicsLoadBalancerBaseUrl.PRODUCTION; -global.fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.PRODUCTION; +global.environment = Environment.PRODUCTION; const dataSourceMenu: MenuItemConstructorOptions = { label: "Data Source", @@ -16,16 +11,12 @@ const dataSourceMenu: MenuItemConstructorOptions = { { label: "Localhost", type: "radio", - checked: - global.aicsLoadBalancerBaseUrl === AicsLoadBalancerBaseUrl.LOCALHOST && - global.fileExplorerServiceBaseUrl === FileExplorerServiceBaseUrl.LOCALHOST, + checked: global.environment === Environment.LOCALHOST, click: (_, focusedWindow) => { if (focusedWindow) { - global.aicsLoadBalancerBaseUrl = AicsLoadBalancerBaseUrl.LOCALHOST; - global.fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.LOCALHOST; + global.environment = Environment.LOCALHOST; focusedWindow.webContents.send(GlobalVariableChannels.BaseUrl, { - aicsLoadBalancerBaseUrl: AicsLoadBalancerBaseUrl.LOCALHOST, - fileExplorerServiceBaseUrl: FileExplorerServiceBaseUrl.LOCALHOST, + environment: Environment.LOCALHOST, }); } }, @@ -33,16 +24,12 @@ const dataSourceMenu: MenuItemConstructorOptions = { { label: "Staging", type: "radio", - checked: - global.aicsLoadBalancerBaseUrl === AicsLoadBalancerBaseUrl.STAGING && - global.fileExplorerServiceBaseUrl === FileExplorerServiceBaseUrl.STAGING, + checked: global.environment === Environment.STAGING, click: (_, focusedWindow) => { if (focusedWindow) { - global.aicsLoadBalancerBaseUrl = AicsLoadBalancerBaseUrl.STAGING; - global.fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.STAGING; + global.environment = Environment.STAGING; focusedWindow.webContents.send(GlobalVariableChannels.BaseUrl, { - aicsLoadBalancerBaseUrl: AicsLoadBalancerBaseUrl.STAGING, - fileExplorerServiceBaseUrl: FileExplorerServiceBaseUrl.STAGING, + environment: Environment.STAGING, }); } }, @@ -50,16 +37,12 @@ const dataSourceMenu: MenuItemConstructorOptions = { { label: "Production", type: "radio", - checked: - global.aicsLoadBalancerBaseUrl === AicsLoadBalancerBaseUrl.PRODUCTION && - global.fileExplorerServiceBaseUrl === FileExplorerServiceBaseUrl.PRODUCTION, + checked: global.environment === Environment.PRODUCTION, click: (_, focusedWindow) => { if (focusedWindow) { - global.aicsLoadBalancerBaseUrl = AicsLoadBalancerBaseUrl.PRODUCTION; - global.fileExplorerServiceBaseUrl = FileExplorerServiceBaseUrl.PRODUCTION; + global.environment = Environment.PRODUCTION; focusedWindow.webContents.send(GlobalVariableChannels.BaseUrl, { - aicsLoadBalancerBaseUrl: AicsLoadBalancerBaseUrl.PRODUCTION, - fileExplorerServiceBaseUrl: FileExplorerServiceBaseUrl.PRODUCTION, + environment: Environment.PRODUCTION, }); } }, diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index 36c27fd6..ce92c5c3 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -120,10 +120,7 @@ function renderFmsFileExplorer() { - + , document.getElementById(APP_ID) @@ -131,13 +128,9 @@ function renderFmsFileExplorer() { } // Listen for IPC updates to global variables -ipcRenderer.addListener( - GlobalVariableChannels.BaseUrl, - (_, { aicsLoadBalancerBaseUrl, fileExplorerServiceBaseUrl }) => { - global.aicsLoadBalancerBaseUrl = aicsLoadBalancerBaseUrl; - global.fileExplorerServiceBaseUrl = fileExplorerServiceBaseUrl; - renderFmsFileExplorer(); - } -); +ipcRenderer.addListener(GlobalVariableChannels.BaseUrl, (_, { environment }) => { + global.environment = environment; + renderFmsFileExplorer(); +}); renderFmsFileExplorer(); diff --git a/packages/desktop/src/util/constants.ts b/packages/desktop/src/util/constants.ts index 0edb8023..3316dc71 100644 --- a/packages/desktop/src/util/constants.ts +++ b/packages/desktop/src/util/constants.ts @@ -3,17 +3,12 @@ // pattern used in the npm script used to invoke electron-mocha. export const RUN_IN_RENDERER = "@renderer"; -export enum AicsLoadBalancerBaseUrl { - LOCALHOST = "http://localhost:8080", - STAGING = "http://stg-aics.corp.alleninstitute.org", - PRODUCTION = "http://aics.corp.alleninstitute.org", -} - -export enum FileExplorerServiceBaseUrl { - LOCALHOST = "http://localhost:9081", - STAGING = "https://staging.int.allencell.org", - PRODUCTION = "https://production.int.allencell.org", -} +export const Environment = { + LOCALHOST: "LOCALHOST", + STAGING: "STAGING", + PRODUCTION: "PRODUCTION", + TEST: "TEST", +}; // Channels global variables can be modified on / listen to export enum GlobalVariableChannels { From cf02d0b8b1bf0ed577f70e3ecee1a88a8103ee74 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Tue, 19 Nov 2024 16:13:42 -0800 Subject: [PATCH 23/56] add back LazilyRenderedThumbnail test --- .../test/LazilyRenderedThumbnail.test.tsx | 402 +++++++++--------- 1 file changed, 201 insertions(+), 201 deletions(-) diff --git a/packages/core/components/FileList/test/LazilyRenderedThumbnail.test.tsx b/packages/core/components/FileList/test/LazilyRenderedThumbnail.test.tsx index 0175fccc..0a78a08e 100644 --- a/packages/core/components/FileList/test/LazilyRenderedThumbnail.test.tsx +++ b/packages/core/components/FileList/test/LazilyRenderedThumbnail.test.tsx @@ -1,201 +1,201 @@ -// import { configureMockStore, mergeState } from "@aics/redux-utils"; -// import { render } from "@testing-library/react"; -// import { expect } from "chai"; -// import * as React from "react"; -// import { Provider } from "react-redux"; -// import * as sinon from "sinon"; - -// import LazilyRenderedThumbnail from "../LazilyRenderedThumbnail"; -// import { initialState } from "../../../state"; -// import FileSet from "../../../entity/FileSet"; -// import FileDetail from "../../../entity/FileDetail"; - -// describe("", () => { -// function makeItemData() { -// const fileSet = new FileSet(); -// sinon.stub(fileSet, "getFileByIndex").callsFake((index) => { -// if (index === 0) { -// return new FileDetail({ -// annotations: [], -// file_id: "abc1230", -// file_name: "my_image0.czi", -// file_path: "some/path/to/my_image0.czi", -// file_size: 1, -// thumbnail: "some/path/to/my_image0.jpg", -// uploaded: new Date().toISOString(), -// }); -// } -// if (index === 9) { -// return new FileDetail({ -// annotations: [], -// file_id: "abc1239", -// file_name: "my_image9.jpg", -// file_path: "some/path/to/my_image9.jpg", -// file_size: 1, -// uploaded: new Date().toISOString(), -// }); -// } -// if (index === 25) { -// return new FileDetail({ -// annotations: [], -// file_id: "abc12325", -// file_name: "my_image25.czi", -// file_path: "some/path/to/my_image25.czi", -// file_size: 1, -// uploaded: new Date().toISOString(), -// }); -// } -// }); - -// return { -// fileSet, -// measuredWidth: 600, -// itemCount: 100, -// onContextMenu: sinon.spy(), -// onSelect: sinon.spy(), -// }; -// } - -// it("renders thumbnail when file has one specified", async () => { -// // Arrange -// const state = mergeState(initialState, {}); -// const { store } = configureMockStore({ state }); - -// // Act -// const { getAllByText, findByRole } = render( -// -// -// -// ); - -// // Assert -// // Also checking for proper row/col indexing -// const thumbnail = await findByRole("img"); -// expect(thumbnail.getAttribute("src")).to.include("some/path/to/my_image0.jpg"); -// expect(getAllByText("my_image0.czi")).to.not.be.empty; -// }); - -// it("renders file as thumbnail if file is renderable type", async () => { -// // Arrange -// const { store } = configureMockStore({ state: initialState }); - -// // Act -// const { getAllByText, findByRole } = render( -// -// -// -// ); - -// // Assert -// // Also confirms proper row/col indexing -// const thumbnail = await findByRole("img"); -// expect(thumbnail.getAttribute("src")).to.include("some/path/to/my_image9.jpg"); -// expect(getAllByText("my_image9.jpg")).to.not.be.empty; -// }); - -// it("renders svg as thumbnail if file has no renderable thumbnail", () => { -// // Arrange -// const { store } = configureMockStore({ state: initialState }); - -// // Act -// const { getAllByText, queryByRole } = render( -// -// -// -// ); - -// // Assert -// // Also confirms proper row/col indexing -// expect(".no-thumbnail").to.exist; -// expect(".svg").to.exist; -// expect(queryByRole("img")).not.to.exist; -// expect(getAllByText("my_image25.czi")).to.not.be.empty; -// }); - -// it("renders a loading indicator when data is not available", () => { -// // Arrange -// const { store } = configureMockStore({ state: initialState }); - -// // Act -// const { queryByText, queryAllByTestId } = render( -// -// -// -// ); - -// // Assert -// expect(queryByText("my_image")).to.equal(null); -// expect(queryAllByTestId("loading-spinner")).to.not.be.empty; -// }); - -// // We want to be able to render empty cells past the total item count in order to fill the grid -// it("renders an empty cell if the index is past the total item count", () => { -// // Arrange -// const { store } = configureMockStore({ state: initialState }); - -// // Act -// const { queryByText, queryAllByTestId } = render( -// -// -// -// ); - -// // Assert -// expect(queryByText("my_image")).to.equal(null); -// expect(queryAllByTestId("loading-spinner")).to.be.empty; -// }); - -// it("renders and indexes correctly with different number of columns", () => { -// // Arrange -// const state = { -// ...initialState, -// selection: { -// ...initialState.selection, -// fileGridColumnCount: 10, -// }, -// }; -// const { store } = configureMockStore({ state }); - -// // Act -// const { getAllByText } = render( -// -// -// -// ); - -// // Assert -// expect(".no-thumbnail").to.exist; -// expect(".svg").to.exist; -// expect(getAllByText("my_image25.czi")).to.not.be.empty; -// }); -// }); +import { configureMockStore, mergeState } from "@aics/redux-utils"; +import { render } from "@testing-library/react"; +import { expect } from "chai"; +import * as React from "react"; +import { Provider } from "react-redux"; +import * as sinon from "sinon"; + +import LazilyRenderedThumbnail from "../LazilyRenderedThumbnail"; +import { initialState } from "../../../state"; +import FileSet from "../../../entity/FileSet"; +import FileDetail from "../../../entity/FileDetail"; + +describe("", () => { + function makeItemData() { + const fileSet = new FileSet(); + sinon.stub(fileSet, "getFileByIndex").callsFake((index) => { + if (index === 0) { + return new FileDetail({ + annotations: [], + file_id: "abc1230", + file_name: "my_image0.czi", + file_path: "some/path/to/my_image0.czi", + file_size: 1, + thumbnail: "some/path/to/my_image0.jpg", + uploaded: new Date().toISOString(), + }); + } + if (index === 9) { + return new FileDetail({ + annotations: [], + file_id: "abc1239", + file_name: "my_image9.jpg", + file_path: "some/path/to/my_image9.jpg", + file_size: 1, + uploaded: new Date().toISOString(), + }); + } + if (index === 25) { + return new FileDetail({ + annotations: [], + file_id: "abc12325", + file_name: "my_image25.czi", + file_path: "some/path/to/my_image25.czi", + file_size: 1, + uploaded: new Date().toISOString(), + }); + } + }); + + return { + fileSet, + measuredWidth: 600, + itemCount: 100, + onContextMenu: sinon.spy(), + onSelect: sinon.spy(), + }; + } + + it("renders thumbnail when file has one specified", async () => { + // Arrange + const state = mergeState(initialState, {}); + const { store } = configureMockStore({ state }); + + // Act + const { getAllByText, findByRole } = render( + + + + ); + + // Assert + // Also checking for proper row/col indexing + const thumbnail = await findByRole("img"); + expect(thumbnail.getAttribute("src")).to.include("some/path/to/my_image0.jpg"); + expect(getAllByText("my_image0.czi")).to.not.be.empty; + }); + + it("renders file as thumbnail if file is renderable type", async () => { + // Arrange + const { store } = configureMockStore({ state: initialState }); + + // Act + const { getAllByText, findByRole } = render( + + + + ); + + // Assert + // Also confirms proper row/col indexing + const thumbnail = await findByRole("img"); + expect(thumbnail.getAttribute("src")).to.include("some/path/to/my_image9.jpg"); + expect(getAllByText("my_image9.jpg")).to.not.be.empty; + }); + + it("renders svg as thumbnail if file has no renderable thumbnail", () => { + // Arrange + const { store } = configureMockStore({ state: initialState }); + + // Act + const { getAllByText, queryByRole } = render( + + + + ); + + // Assert + // Also confirms proper row/col indexing + expect(".no-thumbnail").to.exist; + expect(".svg").to.exist; + expect(queryByRole("img")).not.to.exist; + expect(getAllByText("my_image25.czi")).to.not.be.empty; + }); + + it("renders a loading indicator when data is not available", () => { + // Arrange + const { store } = configureMockStore({ state: initialState }); + + // Act + const { queryByText, queryAllByTestId } = render( + + + + ); + + // Assert + expect(queryByText("my_image")).to.equal(null); + expect(queryAllByTestId("loading-spinner")).to.not.be.empty; + }); + + // We want to be able to render empty cells past the total item count in order to fill the grid + it("renders an empty cell if the index is past the total item count", () => { + // Arrange + const { store } = configureMockStore({ state: initialState }); + + // Act + const { queryByText, queryAllByTestId } = render( + + + + ); + + // Assert + expect(queryByText("my_image")).to.equal(null); + expect(queryAllByTestId("loading-spinner")).to.be.empty; + }); + + it("renders and indexes correctly with different number of columns", () => { + // Arrange + const state = { + ...initialState, + selection: { + ...initialState.selection, + fileGridColumnCount: 10, + }, + }; + const { store } = configureMockStore({ state }); + + // Act + const { getAllByText } = render( + + + + ); + + // Assert + expect(".no-thumbnail").to.exist; + expect(".svg").to.exist; + expect(getAllByText("my_image25.czi")).to.not.be.empty; + }); +}); From 07a27155752327761b2e69939c1a7a4e09154fa3 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Fri, 22 Nov 2024 13:13:18 -0800 Subject: [PATCH 24/56] refactor enum and persisted env --- packages/core/App.tsx | 2 +- .../test/AnnotationFilterForm.test.tsx | 25 +++++----- .../DirectoryTree/test/DirectoryTree.test.tsx | 4 +- .../test/MetadataManifest.test.tsx | 3 +- packages/core/constants/index.ts | 48 +++++++++---------- .../FileSelection/test/FileSelection.test.ts | 3 +- .../core/entity/FileSet/test/FileSet.test.ts | 3 +- .../test/HttpAnnotationService.test.ts | 34 ++++++------- .../FileService/HttpFileService/index.ts | 2 +- .../test/HttpFileService.test.ts | 9 ++-- .../core/services/HttpServiceBase/index.ts | 28 +++++------ .../services/PersistentConfigService/index.ts | 3 ++ packages/core/state/interaction/reducer.ts | 3 +- packages/core/state/interaction/selectors.ts | 14 +++--- .../state/interaction/test/logics.test.ts | 11 +++-- .../core/state/selection/test/logics.test.ts | 3 +- packages/desktop/src/main/global.d.ts | 6 ++- packages/desktop/src/renderer/index.tsx | 12 ++++- .../PersistentConfigServiceElectron.test.ts | 9 +++- packages/desktop/src/util/constants.ts | 12 ++--- 20 files changed, 129 insertions(+), 105 deletions(-) diff --git a/packages/core/App.tsx b/packages/core/App.tsx index bd795f2d..197ce2f4 100644 --- a/packages/core/App.tsx +++ b/packages/core/App.tsx @@ -39,7 +39,7 @@ interface AppProps { // Localhost: "https://localhost:9081" // Stage: "http://stg-aics-api.corp.alleninstitute.org" // From the web (behind load balancer): "/" - environment?: string; + environment?: Environment; } export default function App(props: AppProps) { diff --git a/packages/core/components/AnnotationFilterForm/test/AnnotationFilterForm.test.tsx b/packages/core/components/AnnotationFilterForm/test/AnnotationFilterForm.test.tsx index 50e5112f..705b24ee 100644 --- a/packages/core/components/AnnotationFilterForm/test/AnnotationFilterForm.test.tsx +++ b/packages/core/components/AnnotationFilterForm/test/AnnotationFilterForm.test.tsx @@ -10,6 +10,7 @@ import Annotation from "../../../entity/Annotation"; import FileFilter from "../../../entity/FileFilter"; import { initialState, reducer, reduxLogics, interaction, selection } from "../../../state"; import HttpAnnotationService from "../../../services/AnnotationService/HttpAnnotationService"; +import { FESBaseUrl } from "../../../constants"; describe("", () => { const LISTROW_TESTID_PREFIX = "default-button-"; @@ -31,14 +32,14 @@ describe("", () => { it("shows all values as unchecked at first", async () => { // arrange const responseStub = { - when: `test/file-explorer-service/1.0/annotations/${fooAnnotation.name}/values`, + when: `${FESBaseUrl.TEST}/file-explorer-service/1.0/annotations/${fooAnnotation.name}/values`, respondWith: { data: { data: ["a", "b", "c", "d"] }, }, }; const mockHttpClient = createMockHttpClient(responseStub); const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient: mockHttpClient, }); sandbox.stub(interaction.selectors, "getAnnotationService").returns(annotationService); @@ -68,14 +69,14 @@ describe("", () => { it("deselects and selects a value", async () => { // arrange const responseStub = { - when: `test/file-explorer-service/1.0/annotations/${fooAnnotation.name}/values`, + when: `${FESBaseUrl.TEST}/file-explorer-service/1.0/annotations/${fooAnnotation.name}/values`, respondWith: { data: { data: ["a", "b", "c", "d"] }, }, }; const mockHttpClient = createMockHttpClient(responseStub); const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient: mockHttpClient, }); sandbox.stub(interaction.selectors, "getAnnotationService").returns(annotationService); @@ -124,14 +125,14 @@ describe("", () => { it("naturally sorts values", async () => { // arrange const responseStub = { - when: `test/file-explorer-service/1.0/annotations/${fooAnnotation.name}/values`, + when: `${FESBaseUrl.TEST}/file-explorer-service/1.0/annotations/${fooAnnotation.name}/values`, respondWith: { data: { data: ["AICS-24", "AICS-0", "aics-32", "aICs-2"] }, }, }; const mockHttpClient = createMockHttpClient(responseStub); const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient: mockHttpClient, }); sandbox.stub(interaction.selectors, "getAnnotationService").returns(annotationService); @@ -172,14 +173,14 @@ describe("", () => { }); const responseStub = { - when: `test/file-explorer-service/1.0/annotations/${fooAnnotation.name}/values`, + when: `${FESBaseUrl.TEST}/file-explorer-service/1.0/annotations/${fooAnnotation.name}/values`, respondWith: { data: { data: [true, false] }, }, }; const mockHttpClient = createMockHttpClient(responseStub); const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient: mockHttpClient, }); @@ -279,14 +280,14 @@ describe("", () => { it("naturally sorts values", async () => { // arrange const responseStub = { - when: `test/file-explorer-service/1.0/annotations/${fooAnnotation.name}/values`, + when: `${FESBaseUrl.TEST}/file-explorer-service/1.0/annotations/${fooAnnotation.name}/values`, respondWith: { data: { data: [5, 8, 6.3, -12, 10000000000, 0] }, }, }; const mockHttpClient = createMockHttpClient(responseStub); const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient: mockHttpClient, }); sandbox.stub(interaction.selectors, "getAnnotationService").returns(annotationService); @@ -334,14 +335,14 @@ describe("", () => { it("naturally sorts values", async () => { // arrange const responseStub = { - when: `test/file-explorer-service/1.0/annotations/${fooAnnotation.name}/values`, + when: `${FESBaseUrl.TEST}/file-explorer-service/1.0/annotations/${fooAnnotation.name}/values`, respondWith: { data: { data: [446582220, 125, 10845000, 86400000] }, }, }; const mockHttpClient = createMockHttpClient(responseStub); const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient: mockHttpClient, }); sandbox.stub(interaction.selectors, "getAnnotationService").returns(annotationService); diff --git a/packages/core/components/DirectoryTree/test/DirectoryTree.test.tsx b/packages/core/components/DirectoryTree/test/DirectoryTree.test.tsx index 6d7f968d..7e32f6a4 100644 --- a/packages/core/components/DirectoryTree/test/DirectoryTree.test.tsx +++ b/packages/core/components/DirectoryTree/test/DirectoryTree.test.tsx @@ -19,7 +19,7 @@ import { import { Provider } from "react-redux"; import { createSandbox } from "sinon"; -import { TOP_LEVEL_FILE_ANNOTATIONS } from "../../../constants"; +import { FESBaseUrl, TOP_LEVEL_FILE_ANNOTATIONS } from "../../../constants"; import Annotation from "../../../entity/Annotation"; import AnnotationName from "../../../entity/Annotation/AnnotationName"; import { FmsFileAnnotation } from "../../../services/FileService"; @@ -184,7 +184,7 @@ describe("", () => { }, ]; const mockHttpClient = createMockHttpClient(responseStubs); - const fileExplorerServiceBaseUrl = "http://test.int.allencell.org"; + const fileExplorerServiceBaseUrl = FESBaseUrl.TEST; const annotationService = new HttpAnnotationService({ fileExplorerServiceBaseUrl: fileExplorerServiceBaseUrl, httpClient: mockHttpClient, diff --git a/packages/core/components/Modal/MetadataManifest/test/MetadataManifest.test.tsx b/packages/core/components/Modal/MetadataManifest/test/MetadataManifest.test.tsx index 20978993..c0fed64a 100644 --- a/packages/core/components/Modal/MetadataManifest/test/MetadataManifest.test.tsx +++ b/packages/core/components/Modal/MetadataManifest/test/MetadataManifest.test.tsx @@ -12,6 +12,7 @@ import { Provider } from "react-redux"; import { createSandbox } from "sinon"; import Modal, { ModalType } from "../.."; +import { FESBaseUrl } from "../../../../constants"; import Annotation from "../../../../entity/Annotation"; import FileFilter from "../../../../entity/FileFilter"; import { initialState, interaction, reduxLogics } from "../../../../state"; @@ -19,7 +20,7 @@ import HttpFileService from "../../../../services/FileService/HttpFileService"; import FileDownloadServiceNoop from "../../../../services/FileDownloadService/FileDownloadServiceNoop"; describe("", () => { - const fileExplorerServiceBaseUrl = "TEST"; + const fileExplorerServiceBaseUrl = FESBaseUrl.TEST; const environment = "TEST"; const visibleDialogState = mergeState(initialState, { interaction: { diff --git a/packages/core/constants/index.ts b/packages/core/constants/index.ts index a571df5f..929b628d 100644 --- a/packages/core/constants/index.ts +++ b/packages/core/constants/index.ts @@ -5,12 +5,12 @@ import { AnnotationType } from "../entity/AnnotationFormatter"; export const APP_ID = "fms-file-explorer-core"; // Refer to packages/fms-file-explorer-electron/src/main/menu -export const Environment = { - LOCALHOST: "LOCALHOST", - STAGING: "STAGING", - PRODUCTION: "PRODUCTION", - TEST: "TEST", -} as const; +export enum Environment { + LOCALHOST = "LOCALHOST", + STAGING = "STAGING", + PRODUCTION = "PRODUCTION", + TEST = "TEST", +} export const TOP_LEVEL_FILE_ANNOTATIONS = [ new Annotation({ @@ -61,23 +61,23 @@ export const THUMBNAIL_SIZE_TO_NUM_COLUMNS = { export const AICS_FMS_DATA_SOURCE_NAME = "AICS FMS"; -export const FESBaseUrlMap = { - LOCALHOST: "http://localhost:9081", - STAGING: "https://staging.int.allencell.org", - PRODUCTION: "https://production.int.allencell.org", - TEST: "http://test.int.allencell.org", -}; +export enum FESBaseUrl { + LOCALHOST = "http://localhost:9081", + STAGING = "https://staging.int.allencell.org", + PRODUCTION = "https://production.int.allencell.org", + TEST = "http://test.int.allencell.org", +} -export const MMSBaseUrlMap = { - LOCALHOST: "http://localhost:9060", - STAGING: "http://stg-aics-api", - PRODUCTION: "http://prod-aics-api", - TEST: "http://test-aics-api", -}; +export enum MMSBaseUrl { + LOCALHOST = "http://localhost:9060", + STAGING = "http://stg-aics-api", + PRODUCTION = "http://prod-aics-api", + TEST = "http://test-aics-api", +} -export const LoadBalancerBaseUrlMap = { - LOCALHOST: "http://localhost:8080", - STAGING: "http://stg-aics.corp.alleninstitute.org", - PRODUCTION: "http://aics.corp.alleninstitute.org", - TEST: "http://test-aics.corp.alleninstitute.org", -}; +export enum LoadBalancerBaseUrl { + LOCALHOST = "http://localhost:8080", + STAGING = "http://stg-aics.corp.alleninstitute.org", + PRODUCTION = "http://aics.corp.alleninstitute.org", + TEST = "http://test-aics.corp.alleninstitute.org", +} diff --git a/packages/core/entity/FileSelection/test/FileSelection.test.ts b/packages/core/entity/FileSelection/test/FileSelection.test.ts index a835dafb..ba369db9 100644 --- a/packages/core/entity/FileSelection/test/FileSelection.test.ts +++ b/packages/core/entity/FileSelection/test/FileSelection.test.ts @@ -6,6 +6,7 @@ import FileSet from "../../FileSet"; import NumericRange from "../../NumericRange"; import FileSelection, { FocusDirective } from ".."; +import { FESBaseUrl } from "../../../constants"; import FileDetail from "../../FileDetail"; import FileFilter from "../../FileFilter"; import FuzzyFilter from "../../FileFilter/FuzzyFilter"; @@ -344,7 +345,7 @@ describe("FileSelection", () => { describe("fetchAllDetails", () => { it("returns file details for each selected item", async () => { // Arrange - const fileExplorerServiceBaseUrl = "test"; + const fileExplorerServiceBaseUrl = FESBaseUrl.TEST; const queryResult = []; for (let i = 0; i < 31; i++) { queryResult.push(i); diff --git a/packages/core/entity/FileSet/test/FileSet.test.ts b/packages/core/entity/FileSet/test/FileSet.test.ts index dd80c522..702a73a3 100644 --- a/packages/core/entity/FileSet/test/FileSet.test.ts +++ b/packages/core/entity/FileSet/test/FileSet.test.ts @@ -3,6 +3,7 @@ import { expect } from "chai"; import { createSandbox } from "sinon"; import FileSet from "../"; +import { FESBaseUrl } from "../../../constants"; import FileFilter from "../../FileFilter"; import FileSort, { SortOrder } from "../../FileSort"; import { makeFileDetailMock } from "../../FileDetail/mocks"; @@ -148,7 +149,7 @@ describe("FileSet", () => { }); it("turns indicies for requested data into a properly formed pagination query", async () => { - const fileExplorerServiceBaseUrl = "test"; + const fileExplorerServiceBaseUrl = FESBaseUrl.TEST; const spec = [ { expectedUrl: `${fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}?from=1&limit=28`, diff --git a/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts b/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts index c23ae799..9d9db25d 100644 --- a/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts +++ b/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts @@ -2,7 +2,7 @@ import { createMockHttpClient } from "@aics/redux-utils"; import { expect } from "chai"; import { spy } from "sinon"; -import { TOP_LEVEL_FILE_ANNOTATION_NAMES } from "../../../../constants"; +import { TOP_LEVEL_FILE_ANNOTATION_NAMES, FESBaseUrl } from "../../../../constants"; import Annotation from "../../../../entity/Annotation"; import { annotationsJson } from "../../../../entity/Annotation/mocks"; import FileFilter from "../../../../entity/FileFilter"; @@ -12,7 +12,7 @@ import HttpAnnotationService from ".."; describe("HttpAnnotationService", () => { describe("fetchAnnotations", () => { const httpClient = createMockHttpClient({ - when: `test/${HttpAnnotationService.BASE_ANNOTATION_URL}`, + when: `${FESBaseUrl.TEST}/${HttpAnnotationService.BASE_ANNOTATION_URL}`, respondWith: { data: { data: annotationsJson, @@ -22,7 +22,7 @@ describe("HttpAnnotationService", () => { it("issues request for all available Annotations", async () => { const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient, }); const annotations = await annotationService.fetchAnnotations(); @@ -38,7 +38,7 @@ describe("HttpAnnotationService", () => { const annotation = "foo"; const values = ["a", "b", "c"]; const httpClient = createMockHttpClient({ - when: `test/${HttpAnnotationService.BASE_ANNOTATION_URL}/${annotation}/values`, + when: `${FESBaseUrl.TEST}/${HttpAnnotationService.BASE_ANNOTATION_URL}/${annotation}/values`, respondWith: { data: { data: values, @@ -47,7 +47,7 @@ describe("HttpAnnotationService", () => { }); const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient, }); const actualValues = await annotationService.fetchValues(annotation); @@ -60,7 +60,7 @@ describe("HttpAnnotationService", () => { it("issues a request for annotation values for the first level of the annotation hierarchy", async () => { const expectedValues = ["foo", "bar", "baz"]; const httpClient = createMockHttpClient({ - when: `test/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_ROOT_URL}?order=foo`, + when: `${FESBaseUrl.TEST}/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_ROOT_URL}?order=foo`, respondWith: { data: { data: expectedValues, @@ -69,7 +69,7 @@ describe("HttpAnnotationService", () => { }); const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient, }); const values = await annotationService.fetchRootHierarchyValues(["foo"], []); @@ -80,7 +80,7 @@ describe("HttpAnnotationService", () => { const expectedValues = ["foo", "bar", "baz"]; const httpClient = createMockHttpClient({ // note order of query params - when: `test/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_ROOT_URL}?order=z&order=a&order=b&order=c`, + when: `${FESBaseUrl.TEST}/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_ROOT_URL}?order=z&order=a&order=b&order=c`, respondWith: { data: { data: expectedValues, @@ -90,7 +90,7 @@ describe("HttpAnnotationService", () => { const getSpy = spy(httpClient, "get"); const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient, }); @@ -117,7 +117,7 @@ describe("HttpAnnotationService", () => { it("issues a request for annotation values for the first level of the annotation hierarchy with filters", async () => { const expectedValues = ["foo", "barValue", "baz"]; const httpClient = createMockHttpClient({ - when: `test/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_ROOT_URL}?order=foo&filter=bar=barValue`, + when: `${FESBaseUrl.TEST}/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_ROOT_URL}?order=foo&filter=bar=barValue`, respondWith: { data: { data: expectedValues, @@ -126,7 +126,7 @@ describe("HttpAnnotationService", () => { }); const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient, }); const filter = new FileFilter("bar", "barValue"); @@ -139,7 +139,7 @@ describe("HttpAnnotationService", () => { it("issues request for hierarchy values under a specific path within the hierarchy", async () => { const expectedValues = [1, 2, 3]; const httpClient = createMockHttpClient({ - when: `test/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_UNDER_PATH_URL}?order=foo&order=bar&path=baz`, + when: `${FESBaseUrl.TEST}/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_UNDER_PATH_URL}?order=foo&order=bar&path=baz`, respondWith: { data: { data: expectedValues, @@ -148,7 +148,7 @@ describe("HttpAnnotationService", () => { }); const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient, }); const values = await annotationService.fetchHierarchyValuesUnderPath( @@ -162,7 +162,7 @@ describe("HttpAnnotationService", () => { it("issues request for hierarchy values under a specific path within the hierarchy with filters", async () => { const expectedValues = [1, "barValue", 3]; const httpClient = createMockHttpClient({ - when: `test/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_UNDER_PATH_URL}?order=foo&order=bar&path=baz&filter=bar=barValue`, + when: `${FESBaseUrl.TEST}/${HttpAnnotationService.BASE_ANNOTATION_HIERARCHY_UNDER_PATH_URL}?order=foo&order=bar&path=baz&filter=bar=barValue`, respondWith: { data: { data: expectedValues, @@ -171,7 +171,7 @@ describe("HttpAnnotationService", () => { }); const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient, }); const filter = new FileFilter("bar", "barValue"); @@ -188,7 +188,7 @@ describe("HttpAnnotationService", () => { it("issues request for annotations that can be combined with current hierarchy", async () => { const annotationsFromServer = ["cell_dead", "date_created"]; const httpClient = createMockHttpClient({ - when: `test/${HttpAnnotationService.BASE_ANNOTATION_URL}/hierarchy/available?hierarchy=cas9&hierarchy=cell_line`, + when: `${FESBaseUrl.TEST}/${HttpAnnotationService.BASE_ANNOTATION_URL}/hierarchy/available?hierarchy=cas9&hierarchy=cell_line`, respondWith: { data: { data: annotationsFromServer, @@ -203,7 +203,7 @@ describe("HttpAnnotationService", () => { ...hierarchy, ]; const annotationService = new HttpAnnotationService({ - fileExplorerServiceBaseUrl: "test", + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient, }); const values = await annotationService.fetchAvailableAnnotationsForHierarchy(hierarchy); diff --git a/packages/core/services/FileService/HttpFileService/index.ts b/packages/core/services/FileService/HttpFileService/index.ts index 3289dcb0..d917c048 100644 --- a/packages/core/services/FileService/HttpFileService/index.ts +++ b/packages/core/services/FileService/HttpFileService/index.ts @@ -145,7 +145,7 @@ export default class HttpFileService extends HttpServiceBase implements FileServ "Content-Type": "application/json", "X-User-Id": username || "anonymous", }; - console.log(headers); + console.log(requestUrl, requestBody, headers); try { const cacheStatuses = await this.rawPut<{ cacheFileStatuses: { [fileId: string]: string }; diff --git a/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts b/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts index 37db4304..adec881d 100644 --- a/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts +++ b/packages/core/services/FileService/HttpFileService/test/HttpFileService.test.ts @@ -2,14 +2,15 @@ import { createMockHttpClient } from "@aics/redux-utils"; import { expect } from "chai"; import HttpFileService from ".."; +import { FESBaseUrl, LoadBalancerBaseUrl } from "../../../../constants"; import FileSelection from "../../../../entity/FileSelection"; import FileSet from "../../../../entity/FileSet"; import NumericRange from "../../../../entity/NumericRange"; import FileDownloadServiceNoop from "../../../FileDownloadService/FileDownloadServiceNoop"; describe("HttpFileService", () => { - const fileExplorerServiceBaseUrl = "TEST"; - const loadBalancerBaseUrlMock = "http://loadbalancer-test.aics.corp.alleninstitute.org"; + const fileExplorerServiceBaseUrl = FESBaseUrl.TEST; + const loadBalancerBaseUrl = LoadBalancerBaseUrl.TEST; const fileIds = ["abc123", "def456", "ghi789", "jkl012"]; const files = fileIds.map((file_id) => ({ file_id, @@ -81,7 +82,7 @@ describe("HttpFileService", () => { describe("cacheFiles", () => { const httpClient = createMockHttpClient({ - when: `${loadBalancerBaseUrlMock}/${HttpFileService.BASE_FILE_CACHE_URL}`, + when: `${loadBalancerBaseUrl}/${HttpFileService.BASE_FILE_CACHE_URL}`, respondWith: { data: { cacheFileStatuses: { @@ -95,7 +96,7 @@ describe("HttpFileService", () => { it("sends file IDs to be cached and returns their statuses", async () => { // Arrange const fileService = new HttpFileService({ - loadBalancerBaseUrl: loadBalancerBaseUrlMock, + loadBalancerBaseUrl: loadBalancerBaseUrl, httpClient, downloadService: new FileDownloadServiceNoop(), }); diff --git a/packages/core/services/HttpServiceBase/index.ts b/packages/core/services/HttpServiceBase/index.ts index 48b62112..220ec42a 100644 --- a/packages/core/services/HttpServiceBase/index.ts +++ b/packages/core/services/HttpServiceBase/index.ts @@ -2,24 +2,24 @@ import axios, { AxiosInstance } from "axios"; import { Policy } from "cockatiel"; import LRUCache from "lru-cache"; -import { FESBaseUrlMap, LoadBalancerBaseUrlMap, MMSBaseUrlMap } from "../../constants"; +import { FESBaseUrl, LoadBalancerBaseUrl, MMSBaseUrl } from "../../constants"; import RestServiceResponse from "../../entity/RestServiceResponse"; export interface ConnectionConfig { applicationVersion?: string; - fileExplorerServiceBaseUrl?: string | keyof typeof FESBaseUrlMap; + fileExplorerServiceBaseUrl?: FESBaseUrl; httpClient?: AxiosInstance; - loadBalancerBaseUrl?: string | keyof typeof LoadBalancerBaseUrlMap; - metadataManagementServiceBaseURl?: string | keyof typeof MMSBaseUrlMap; + loadBalancerBaseUrl?: LoadBalancerBaseUrl; + metadataManagementServiceBaseURl?: MMSBaseUrl; pathSuffix?: string; userName?: string; } export const DEFAULT_CONNECTION_CONFIG = { - fileExplorerServiceBaseUrl: FESBaseUrlMap.PRODUCTION, + fileExplorerServiceBaseUrl: FESBaseUrl.PRODUCTION, httpClient: axios.create(), - loadBalancerBaseUrl: LoadBalancerBaseUrlMap.PRODUCTION, - metadataManagementServiceBaseURl: MMSBaseUrlMap.PRODUCTION, + loadBalancerBaseUrl: LoadBalancerBaseUrl.PRODUCTION, + metadataManagementServiceBaseURl: MMSBaseUrl.PRODUCTION, }; const CHARACTER_TO_ENCODING_MAP: { [index: string]: string } = { @@ -135,7 +135,7 @@ export default class HttpServiceBase { } if (config.metadataManagementServiceBaseURl) { - this.setLoadBalancerBaseUrl(config.metadataManagementServiceBaseURl); + this.setMetadataManagementServiceBaseURl(config.metadataManagementServiceBaseURl); } if (config.pathSuffix) { @@ -281,9 +281,7 @@ export default class HttpServiceBase { this.setHeaders(); } - public setFileExplorerServiceBaseUrl( - fileExplorerServiceBaseUrl: string | keyof typeof FESBaseUrlMap - ) { + public setFileExplorerServiceBaseUrl(fileExplorerServiceBaseUrl: FESBaseUrl) { if (this.fileExplorerServiceBaseUrl !== fileExplorerServiceBaseUrl) { // bust cache when base url changes this.urlToResponseDataCache.reset(); @@ -313,9 +311,7 @@ export default class HttpServiceBase { } } - public setLoadBalancerBaseUrl( - loadBalancerBaseUrl: string | keyof typeof LoadBalancerBaseUrlMap - ) { + public setLoadBalancerBaseUrl(loadBalancerBaseUrl: LoadBalancerBaseUrl) { if (this.loadBalancerBaseUrl !== loadBalancerBaseUrl) { // bust cache when base url changes this.urlToResponseDataCache.reset(); @@ -324,9 +320,7 @@ export default class HttpServiceBase { this.loadBalancerBaseUrl = loadBalancerBaseUrl; } - public setMetadataManagementServiceBaseURl( - metadataManagementServiceBaseURl: string | keyof typeof MMSBaseUrlMap - ) { + public setMetadataManagementServiceBaseURl(metadataManagementServiceBaseURl: MMSBaseUrl) { if (this.metadataManagementServiceBaseURl !== metadataManagementServiceBaseURl) { // bust cache when base url changes this.urlToResponseDataCache.reset(); diff --git a/packages/core/services/PersistentConfigService/index.ts b/packages/core/services/PersistentConfigService/index.ts index 86a02bc2..e9a0bc62 100644 --- a/packages/core/services/PersistentConfigService/index.ts +++ b/packages/core/services/PersistentConfigService/index.ts @@ -1,5 +1,6 @@ import { AnnotationResponse } from "../../entity/Annotation"; import { Query } from "../../state/selection/actions"; +import { Environment } from "../../constants"; /** * Keys for the data saved by this service @@ -13,6 +14,7 @@ export enum PersistedConfigKeys { UserSelectedApplications = "USER_SELECTED_APPLICATIONS", Queries = "QUERIES", RecentAnnotations = "RECENT_ANNOTATIONS", + Environment = "ENVIRONMENT", } export interface UserSelectedApplication { @@ -29,6 +31,7 @@ export interface PersistedConfig { [PersistedConfigKeys.Queries]?: Query[]; [PersistedConfigKeys.RecentAnnotations]?: string[]; [PersistedConfigKeys.UserSelectedApplications]?: UserSelectedApplication[]; + [PersistedConfigKeys.Environment]?: Environment; } /** diff --git a/packages/core/state/interaction/reducer.ts b/packages/core/state/interaction/reducer.ts index fde4558e..3b563554 100644 --- a/packages/core/state/interaction/reducer.ts +++ b/packages/core/state/interaction/reducer.ts @@ -30,6 +30,7 @@ import { } from "./actions"; import { ContextMenuItem, PositionReference } from "../../components/ContextMenu"; import { ModalType } from "../../components/Modal"; +import { Environment } from "../../constants"; import FileFilter from "../../entity/FileFilter"; import { PlatformDependentServices } from "../../services"; import ApplicationInfoServiceNoop from "../../services/ApplicationInfoService/ApplicationInfoServiceNoop"; @@ -41,8 +42,6 @@ import NotificationServiceNoop from "../../services/NotificationService/Notifica import DatabaseServiceNoop from "../../services/DatabaseService/DatabaseServiceNoop"; import PublicDataset from "../../../web/src/entity/PublicDataset"; -import { Environment } from "../../constants"; - export interface InteractionStateBranch { applicationVersion?: string; contextMenuIsVisible: boolean; diff --git a/packages/core/state/interaction/selectors.ts b/packages/core/state/interaction/selectors.ts index 5808b288..553de3f4 100644 --- a/packages/core/state/interaction/selectors.ts +++ b/packages/core/state/interaction/selectors.ts @@ -16,9 +16,9 @@ import HttpFileService from "../../services/FileService/HttpFileService"; import { ModalType } from "../../components/Modal"; import { AICS_FMS_DATA_SOURCE_NAME, - FESBaseUrlMap, - MMSBaseUrlMap, - LoadBalancerBaseUrlMap, + FESBaseUrl, + MMSBaseUrl, + LoadBalancerBaseUrl, } from "../../constants"; // BASIC SELECTORS @@ -55,17 +55,17 @@ export const isAicsEmployee = (state: State) => state.interaction.isAicsEmployee // URL Mapping Selectors export const getFileExplorerServiceBaseUrl = createSelector( [getEnvironment], - (environment) => FESBaseUrlMap[environment] + (environment) => FESBaseUrl[environment] ); export const getLoadBalancerBaseUrl = createSelector( [getEnvironment], - (environment) => LoadBalancerBaseUrlMap[environment] + (environment) => LoadBalancerBaseUrl[environment] ); export const getMetadataManagementServiceBaseUrl = createSelector( [getEnvironment], - (environment) => MMSBaseUrlMap[environment] + (environment) => MMSBaseUrl[environment] ); // COMPOSED SELECTORS @@ -138,8 +138,8 @@ export const getHttpFileService = createSelector( ) => new HttpFileService({ applicationVersion, - loadBalancerBaseUrl: loadBalancerBaseUrl, fileExplorerServiceBaseUrl: fileExplorerServiceBaseUrl, + loadBalancerBaseUrl: loadBalancerBaseUrl, metadataManagementServiceBaseURl: metadataManagementServiceBaseURL, userName, downloadService: platformDependentServices.fileDownloadService, diff --git a/packages/core/state/interaction/test/logics.test.ts b/packages/core/state/interaction/test/logics.test.ts index 5b539173..f9dc58c1 100644 --- a/packages/core/state/interaction/test/logics.test.ts +++ b/packages/core/state/interaction/test/logics.test.ts @@ -24,6 +24,7 @@ import { } from "../../../services/ExecutionEnvService"; import ExecutionEnvServiceNoop from "../../../services/ExecutionEnvService/ExecutionEnvServiceNoop"; import interactionLogics from "../logics"; +import { FESBaseUrl } from "../../../constants"; import Annotation from "../../../entity/Annotation"; import AnnotationName from "../../../entity/Annotation/AnnotationName"; import { AnnotationType } from "../../../entity/AnnotationFormatter"; @@ -206,7 +207,7 @@ describe("Interaction logics", () => { it("doesn't use selected files when given a specific file folder path", async () => { // arrange - const fileExplorerServiceBaseUrl = "TEST"; + const fileExplorerServiceBaseUrl = FESBaseUrl.TEST; const filters = [ new FileFilter("Cell Line", "AICS-12"), new FileFilter("Notes", "Hello"), @@ -775,7 +776,7 @@ describe("Interaction logics", () => { describe("refresh", () => { const sandbox = createSandbox(); - const fileExplorerServiceBaseUrl = "test"; + const fileExplorerServiceBaseUrl = FESBaseUrl.TEST; const annotations = annotationsJson.map((annotation) => new Annotation(annotation)); const availableAnnotations = [annotations[1].displayName]; const responseStubs = [ @@ -882,7 +883,7 @@ describe("Interaction logics", () => { ], }); } - const fileExplorerServiceBaseUrl = "test"; + const fileExplorerServiceBaseUrl = FESBaseUrl.TEST; const responseStub = { when: () => true, respondWith: { @@ -1098,7 +1099,7 @@ describe("Interaction logics", () => { }); } const files = [...csvFiles, ...pngFiles]; - const fileExplorerServiceBaseUrl = "test"; + const fileExplorerServiceBaseUrl = FESBaseUrl.TEST; const responseStub = { when: `${fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}?from=0&limit=101`, respondWith: { @@ -1213,7 +1214,7 @@ describe("Interaction logics", () => { for (let i = 0; i <= 100; i++) { files.push({ file_path: `/allen/file_${i}.ext` }); } - const fileExplorerServiceBaseUrl = "test"; + const fileExplorerServiceBaseUrl = FESBaseUrl.TEST; const responseStub = { when: `${fileExplorerServiceBaseUrl}/${HttpFileService.BASE_FILES_URL}?from=0&limit=101`, respondWith: { diff --git a/packages/core/state/selection/test/logics.test.ts b/packages/core/state/selection/test/logics.test.ts index 685b31d8..697ec38a 100644 --- a/packages/core/state/selection/test/logics.test.ts +++ b/packages/core/state/selection/test/logics.test.ts @@ -27,6 +27,7 @@ import { changeSourceMetadata, } from "../actions"; import { initialState, interaction } from "../../"; +import { FESBaseUrl } from "../../../constants"; import Annotation from "../../../entity/Annotation"; import AnnotationName from "../../../entity/Annotation/AnnotationName"; import FileFilter from "../../../entity/FileFilter"; @@ -330,7 +331,7 @@ describe("Selection logics", () => { }, }, ]; - const fileExplorerServiceBaseUrl = "test"; + const fileExplorerServiceBaseUrl = FESBaseUrl.TEST; const mockHttpClient = createMockHttpClient(responseStubs); const fileService = new HttpFileService({ fileExplorerServiceBaseUrl, diff --git a/packages/desktop/src/main/global.d.ts b/packages/desktop/src/main/global.d.ts index 335ad1fa..cbf69964 100644 --- a/packages/desktop/src/main/global.d.ts +++ b/packages/desktop/src/main/global.d.ts @@ -1,3 +1,7 @@ /*eslint no-var: "off"*/ // necessary in order to do: global.environment = "..." -declare var environment: string; +import { Environment } from "./util/constants"; + +declare global { + var environment: Environment; +} diff --git a/packages/desktop/src/renderer/index.tsx b/packages/desktop/src/renderer/index.tsx index ce92c5c3..5ceded4e 100644 --- a/packages/desktop/src/renderer/index.tsx +++ b/packages/desktop/src/renderer/index.tsx @@ -34,12 +34,20 @@ const KeyDownHandler: React.FC<{ clearStore: () => void }> = ({ clearStore }) => return null; }; -// Function to clear the persistent store +// Clears the persistent store but retains `Environment` to prevent misalignment +// between the data source and the selected menu item in the app. const clearPersistentStore = () => { + const currentEnvironment = global.environment; persistentConfigService.clear(); + persistentConfigService.persist({ [PersistedConfigKeys.Environment]: currentEnvironment }); window.location.reload(); }; +const initializeEnvironment = () => { + const savedEnvironment = persistentConfigService.get(PersistedConfigKeys.Environment); + global.environment = savedEnvironment || "PRODUCTION"; +}; + // Application analytics/metrics const frontendInsights = new FrontendInsights( { @@ -130,7 +138,9 @@ function renderFmsFileExplorer() { // Listen for IPC updates to global variables ipcRenderer.addListener(GlobalVariableChannels.BaseUrl, (_, { environment }) => { global.environment = environment; + persistentConfigService.persist({ [PersistedConfigKeys.Environment]: environment }); renderFmsFileExplorer(); }); +initializeEnvironment(); renderFmsFileExplorer(); diff --git a/packages/desktop/src/services/test/PersistentConfigServiceElectron.test.ts b/packages/desktop/src/services/test/PersistentConfigServiceElectron.test.ts index 7d1c8efa..c547a758 100644 --- a/packages/desktop/src/services/test/PersistentConfigServiceElectron.test.ts +++ b/packages/desktop/src/services/test/PersistentConfigServiceElectron.test.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import { PersistedConfigKeys } from "../../../../core/services"; -import { RUN_IN_RENDERER } from "../../util/constants"; +import { Environment, RUN_IN_RENDERER } from "../../util/constants"; import PersistentConfigServiceElectron from "../PersistentConfigServiceElectron"; describe(`${RUN_IN_RENDERER} PersistentConfigServiceElectron`, () => { @@ -61,6 +61,7 @@ describe(`${RUN_IN_RENDERER} PersistentConfigServiceElectron`, () => { units: "string", }, ]; + const expectedEnvironment = Environment.TEST; service.persist(PersistedConfigKeys.AllenMountPoint, expectedAllenMountPoint); service.persist(PersistedConfigKeys.CsvColumns, expectedCsvColumns); @@ -73,6 +74,7 @@ describe(`${RUN_IN_RENDERER} PersistentConfigServiceElectron`, () => { service.persist(PersistedConfigKeys.UserSelectedApplications, expectedUserSelectedApps); service.persist(PersistedConfigKeys.DisplayAnnotations, expectedDisplayAnnotations); service.persist(PersistedConfigKeys.RecentAnnotations, expectedRecentAnnotations); + service.persist(PersistedConfigKeys.Environment, expectedEnvironment); const expectedConfig = { [PersistedConfigKeys.AllenMountPoint]: expectedAllenMountPoint, @@ -83,6 +85,7 @@ describe(`${RUN_IN_RENDERER} PersistentConfigServiceElectron`, () => { [PersistedConfigKeys.UserSelectedApplications]: expectedUserSelectedApps, [PersistedConfigKeys.DisplayAnnotations]: expectedDisplayAnnotations, [PersistedConfigKeys.RecentAnnotations]: expectedRecentAnnotations, + [PersistedConfigKeys.Environment]: expectedEnvironment, }; // Act @@ -120,6 +123,7 @@ describe(`${RUN_IN_RENDERER} PersistentConfigServiceElectron`, () => { units: "string", }, ], + [PersistedConfigKeys.Environment]: Environment.TEST, }; // Act @@ -159,10 +163,13 @@ describe(`${RUN_IN_RENDERER} PersistentConfigServiceElectron`, () => { service.persist(PersistedConfigKeys.CsvColumns, ["Cell Line"]); service.persist(PersistedConfigKeys.CsvColumns, expected); service.persist(PersistedConfigKeys.AllenMountPoint, "/my/path/allen"); + service.persist(PersistedConfigKeys.Environment, Environment.TEST); // Assert const actual = service.get(PersistedConfigKeys.CsvColumns); + const actualEnvironment = service.get(PersistedConfigKeys.Environment); expect(actual).to.be.deep.equal(expected); + expect(actualEnvironment).to.equal(Environment.TEST); }); }); }); diff --git a/packages/desktop/src/util/constants.ts b/packages/desktop/src/util/constants.ts index 3316dc71..bda836ed 100644 --- a/packages/desktop/src/util/constants.ts +++ b/packages/desktop/src/util/constants.ts @@ -3,12 +3,12 @@ // pattern used in the npm script used to invoke electron-mocha. export const RUN_IN_RENDERER = "@renderer"; -export const Environment = { - LOCALHOST: "LOCALHOST", - STAGING: "STAGING", - PRODUCTION: "PRODUCTION", - TEST: "TEST", -}; +export enum Environment { + LOCALHOST = "LOCALHOST", + STAGING = "STAGING", + PRODUCTION = "PRODUCTION", + TEST = "TEST", +} // Channels global variables can be modified on / listen to export enum GlobalVariableChannels { From ccca39f13fd7e1bab1e594823432afe592854f02 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Fri, 22 Nov 2024 13:24:58 -0800 Subject: [PATCH 25/56] add back dlt comment --- packages/core/state/interaction/reducer.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/state/interaction/reducer.ts b/packages/core/state/interaction/reducer.ts index 3b563554..735b8663 100644 --- a/packages/core/state/interaction/reducer.ts +++ b/packages/core/state/interaction/reducer.ts @@ -70,6 +70,10 @@ export const initialState: InteractionStateBranch = { environment: Environment.PRODUCTION, contextMenuIsVisible: false, contextMenuItems: [], + // Passed to `ContextualMenu` as `target`. From the "@fluentui/react" docs: + // "The target that ContextualMenu should try to position itself based on. + // It can be either an element, a query selector string resolving to a valid element, or a MouseEvent. + // If a MouseEvent is given, the origin point of the event will be used." contextMenuPositionReference: null, datasetDetailsPanelIsVisible: false, fileFiltersForVisibleModal: [], @@ -84,6 +88,7 @@ export const initialState: InteractionStateBranch = { fileViewerService: new FileViewerServiceNoop(), frontendInsights: new FrontendInsights({ application: { + // Kept old name to compare usage more easily in Amplitude UI name: "FMS File Explorer", version: "0.0.0-noop", }, From 44b3f168a061bf7397716f4d325e0b5a2fde6db0 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Fri, 22 Nov 2024 16:05:19 -0800 Subject: [PATCH 26/56] comment resolution --- packages/core/services/FileService/HttpFileService/index.ts | 2 +- packages/core/state/interaction/logics.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/services/FileService/HttpFileService/index.ts b/packages/core/services/FileService/HttpFileService/index.ts index d917c048..ba29e972 100644 --- a/packages/core/services/FileService/HttpFileService/index.ts +++ b/packages/core/services/FileService/HttpFileService/index.ts @@ -145,7 +145,7 @@ export default class HttpFileService extends HttpServiceBase implements FileServ "Content-Type": "application/json", "X-User-Id": username || "anonymous", }; - console.log(requestUrl, requestBody, headers); + try { const cacheStatuses = await this.rawPut<{ cacheFileStatuses: { [fileId: string]: string }; diff --git a/packages/core/state/interaction/logics.ts b/packages/core/state/interaction/logics.ts index 58e0d466..3bcacc95 100644 --- a/packages/core/state/interaction/logics.ts +++ b/packages/core/state/interaction/logics.ts @@ -592,7 +592,7 @@ const moveFilesLogic = createLogic({ // TODO: What to do with the status console.log("Cache statuses:", cacheStatuses); } catch (err) { - console.error(`Error encountered while moving files: ${err}`); + throw new Error(`Error encountered while moving files: ${err}`); } finally { done(); } From f04b7c61b1972acf0096ed6d4991d20ff4cef9a8 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Mon, 25 Nov 2024 13:11:55 -0800 Subject: [PATCH 27/56] feature/toggle_status_report --- packages/core/state/interaction/logics.ts | 78 +++++++++++++++++++++-- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/packages/core/state/interaction/logics.ts b/packages/core/state/interaction/logics.ts index 3bcacc95..ca64f506 100644 --- a/packages/core/state/interaction/logics.ts +++ b/packages/core/state/interaction/logics.ts @@ -2,7 +2,7 @@ import { isEmpty, sumBy, throttle, uniq, uniqueId } from "lodash"; import { AnyAction } from "redux"; import { createLogic } from "redux-logic"; -import { metadata, ReduxLogicDeps, selection } from "../"; +import { interaction, metadata, ReduxLogicDeps, selection } from "../"; import { DOWNLOAD_MANIFEST, DownloadManifestAction, @@ -582,17 +582,81 @@ const setIsSmallScreen = createLogic({ * Logs details of files that are being moved. */ const moveFilesLogic = createLogic({ - async process({ action, getState }: ReduxLogicDeps, _dispatch, done) { + async process({ action, getState }: ReduxLogicDeps, dispatch, done) { try { const httpFileService = interactionSelectors.getHttpFileService(getState()); const username = interactionSelectors.getUserName(getState()); - const fileIds = (action as MoveFilesAction).payload.fileDetails.map((file) => file.id); - const cacheStatuses = await httpFileService.cacheFiles(fileIds, username); - // TODO: What to do with the status - console.log("Cache statuses:", cacheStatuses); + const fileDetails = (action as MoveFilesAction).payload.fileDetails; + + // Map file IDs to file names for easy lookup + const fileIdToNameMap = Object.fromEntries( + fileDetails.map((file) => [file.id, file.name]) + ); + + // Extract file IDs + const fileIds = fileDetails.map((file) => file.id); + + const response = await httpFileService.cacheFiles(fileIds, username); + const cacheStatuses = response.cacheFileStatuses; + + // Check if the response is empty. + if (!cacheStatuses || Object.keys(cacheStatuses).length === 0) { + dispatch( + interaction.actions.processError( + "moveFilesNoFilesProcessed", + "No files were processed. Please check the request or try again." + ) + ); + return; + } + + const successfulFiles: string[] = []; + const failedFiles: string[] = []; + + Object.entries(cacheStatuses).forEach(([fileId, status]) => { + if ( + status === "DOWNLOAD_COMPLETE" || + status === "DOWNLOAD_IN_PROGRESS" || + status === "DOWNLOAD_STARTED" + ) { + successfulFiles.push(fileId); + } else if ( + status === "FILE_RECORD_NOT_FOUND" || + status === "FILE_NOT_FOUND_ON_CLOUD" + ) { + failedFiles.push(fileId); + } + }); + + // Dispatch net success message. (Assuming some files were successful) + if (successfulFiles.length > 0) { + dispatch( + interaction.actions.processSuccess( + "moveFilesSuccess", + `${successfulFiles.length} out of ${ + Object.keys(cacheStatuses).length + } files were successfully cached.` + ) + ); + } + + // Dispatch individual errors for each failed file. + failedFiles.forEach((fileId) => { + const fileName = fileIdToNameMap[fileId] || "Unknown File"; + dispatch( + interaction.actions.processError( + `moveFileFailure_${fileId}`, + `File "${fileName}" failed to cache. Status: ${cacheStatuses[fileId]}` + ) + ); + }); } catch (err) { - throw new Error(`Error encountered while moving files: ${err}`); + // Service call itself fails + console.log(err); + dispatch( + interaction.actions.processError("moveFilesFailure", `Failed to cache files.`) + ); } finally { done(); } From 3726ec57d03eed9085095800cc3a287af3a29398 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Tue, 26 Nov 2024 14:41:27 -0800 Subject: [PATCH 28/56] comment res --- packages/core/state/interaction/logics.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/state/interaction/logics.ts b/packages/core/state/interaction/logics.ts index ca64f506..6c9e3634 100644 --- a/packages/core/state/interaction/logics.ts +++ b/packages/core/state/interaction/logics.ts @@ -636,7 +636,7 @@ const moveFilesLogic = createLogic({ "moveFilesSuccess", `${successfulFiles.length} out of ${ Object.keys(cacheStatuses).length - } files were successfully cached.` + } files were successfully cached. Files will be available in the NAS (VAST) after downloads finish asynchronously` ) ); } @@ -653,7 +653,6 @@ const moveFilesLogic = createLogic({ }); } catch (err) { // Service call itself fails - console.log(err); dispatch( interaction.actions.processError("moveFilesFailure", `Failed to cache files.`) ); From 60cf69a2a3c544e763e29152194146d5022d176e Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Tue, 26 Nov 2024 15:01:28 -0800 Subject: [PATCH 29/56] adjust error case --- packages/core/state/interaction/logics.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/core/state/interaction/logics.ts b/packages/core/state/interaction/logics.ts index 6c9e3634..6041cf2b 100644 --- a/packages/core/state/interaction/logics.ts +++ b/packages/core/state/interaction/logics.ts @@ -621,10 +621,7 @@ const moveFilesLogic = createLogic({ status === "DOWNLOAD_STARTED" ) { successfulFiles.push(fileId); - } else if ( - status === "FILE_RECORD_NOT_FOUND" || - status === "FILE_NOT_FOUND_ON_CLOUD" - ) { + } else { failedFiles.push(fileId); } }); @@ -636,7 +633,8 @@ const moveFilesLogic = createLogic({ "moveFilesSuccess", `${successfulFiles.length} out of ${ Object.keys(cacheStatuses).length - } files were successfully cached. Files will be available in the NAS (VAST) after downloads finish asynchronously` + } files were successfully queued for download to NAS (Vast) from cloud. + Files will be available in the NAS after downloads finish asynchronously` ) ); } From 422c8dee400d09ad57fe4e414a9854298da1733c Mon Sep 17 00:00:00 2001 From: Brian Whitney <94479316+BrianWhitneyAI@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:13:16 -0800 Subject: [PATCH 30/56] Update packages/core/state/interaction/logics.ts Co-authored-by: Sean LeRoy <41307451+SeanLeRoy@users.noreply.github.com> --- packages/core/state/interaction/logics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/state/interaction/logics.ts b/packages/core/state/interaction/logics.ts index 6041cf2b..ad07d649 100644 --- a/packages/core/state/interaction/logics.ts +++ b/packages/core/state/interaction/logics.ts @@ -652,7 +652,7 @@ const moveFilesLogic = createLogic({ } catch (err) { // Service call itself fails dispatch( - interaction.actions.processError("moveFilesFailure", `Failed to cache files.`) + interaction.actions.processError("moveFilesFailure", `Failed to cache files, details: ${(err as Error).message}.`) ); } finally { done(); From 921421e9540cc2072747a941dc973daf817103c3 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Wed, 18 Dec 2024 11:58:46 -0800 Subject: [PATCH 31/56] 369 Fix broken FileAnnotationList test --- .../FileDetails/test/FileAnnotationList.test.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx index dbf1c8a0..91ca91d3 100644 --- a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx +++ b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx @@ -24,6 +24,9 @@ describe("", () => { const { store } = configureMockStore({ state: mergeState(initialState, { + metadata: { + annotations: TOP_LEVEL_FILE_ANNOTATIONS, + }, interaction: { platformDependentServices: { executionEnvService: new FakeExecutionEnvService(), @@ -33,10 +36,9 @@ describe("", () => { }); const filePathInsideAllenDrive = "path/to/MyFile.txt"; - - const canonicalFilePath = `/allen/${filePathInsideAllenDrive}`; + const filePath = `/allen/${filePathInsideAllenDrive}`; const fileDetails = new FileDetail({ - file_path: canonicalFilePath, + file_path: filePath, file_id: "abc123", file_name: "MyFile.txt", file_size: 7, @@ -54,14 +56,14 @@ describe("", () => { ); // Assert - [ + for (const cellText of [ "File Path (Canonical)", - canonicalFilePath, + filePath, "File Path (Local)", expectedLocalPath, - ].forEach(async (cellText) => { + ]) { expect(await findByText(cellText)).to.not.be.undefined; - }); + } }); it("has only canonical file path when no allen mount point is found", () => { From 656b8eaca4feb5d36f48a7b7d27eab4ba4dd6df7 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Wed, 18 Dec 2024 12:04:06 -0800 Subject: [PATCH 32/56] 368 Update arrange step of test to match cloud-first FMS (failing) --- .../FileDetails/test/FileAnnotationList.test.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx index 91ca91d3..b2c7640e 100644 --- a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx +++ b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx @@ -36,18 +36,17 @@ describe("", () => { }); const filePathInsideAllenDrive = "path/to/MyFile.txt"; - const filePath = `/allen/${filePathInsideAllenDrive}`; + const filePath = `production.allencell.org/${filePathInsideAllenDrive}`; + const localPath = `${hostMountPoint}/${filePathInsideAllenDrive}`; const fileDetails = new FileDetail({ file_path: filePath, file_id: "abc123", file_name: "MyFile.txt", file_size: 7, uploaded: "01/01/01", - annotations: [], + annotations: [{ name: "Local File Path", values: [localPath] }], }); - const expectedLocalPath = `${hostMountPoint}/${filePathInsideAllenDrive}`; - // Act const { findByText } = render( @@ -60,7 +59,7 @@ describe("", () => { "File Path (Canonical)", filePath, "File Path (Local)", - expectedLocalPath, + localPath, ]) { expect(await findByText(cellText)).to.not.be.undefined; } From a9a2940391a005e376b6cf619482b607a184cd55 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Wed, 18 Dec 2024 12:08:21 -0800 Subject: [PATCH 33/56] 368 Display Local File Path annotation as File Path (Local) (test passing) --- packages/core/constants/index.ts | 6 ++++++ packages/core/entity/Annotation/AnnotationName.ts | 1 + 2 files changed, 7 insertions(+) diff --git a/packages/core/constants/index.ts b/packages/core/constants/index.ts index 75d289a4..c3bd5ce4 100644 --- a/packages/core/constants/index.ts +++ b/packages/core/constants/index.ts @@ -31,6 +31,12 @@ export const TOP_LEVEL_FILE_ANNOTATIONS = [ description: "Path to file in storage.", type: AnnotationType.STRING, }), + new Annotation({ + annotationDisplayName: "File Path (Local)", + annotationName: AnnotationName.LOCAL_FILE_PATH, + description: "Path to the file on-premises.", + type: AnnotationType.STRING, + }), new Annotation({ annotationDisplayName: "Size", annotationName: AnnotationName.FILE_SIZE, diff --git a/packages/core/entity/Annotation/AnnotationName.ts b/packages/core/entity/Annotation/AnnotationName.ts index 1de12bd4..f9f328bf 100644 --- a/packages/core/entity/Annotation/AnnotationName.ts +++ b/packages/core/entity/Annotation/AnnotationName.ts @@ -6,6 +6,7 @@ export default { FILE_NAME: "file_name", // a file attribute (top-level prop on file documents in MongoDb) FILE_SIZE: "file_size", // a file attribute (top-level prop on file documents in MongoDb) FILE_PATH: "file_path", // a file attribute (top-level prop on file documents in MongoDb) + LOCAL_FILE_PATH: "Local File Path", // (optional) annotation for FMS files on the local NAS PLATE_BARCODE: "Plate Barcode", THUMBNAIL_PATH: "thumbnail", // (optional) file attribute (top-level prop on the file documents in MongoDb) TYPE: "Type", // matches an annotation in filemetadata.annotation From dfb73843a3c1869af7de6c0ee1b765d4ecdef16a Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Wed, 18 Dec 2024 12:12:44 -0800 Subject: [PATCH 34/56] 368 Test expects custom /allen mount point from OS --- .../FileDetails/test/FileAnnotationList.test.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx index b2c7640e..84a9c250 100644 --- a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx +++ b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx @@ -37,16 +37,19 @@ describe("", () => { const filePathInsideAllenDrive = "path/to/MyFile.txt"; const filePath = `production.allencell.org/${filePathInsideAllenDrive}`; - const localPath = `${hostMountPoint}/${filePathInsideAllenDrive}`; const fileDetails = new FileDetail({ file_path: filePath, file_id: "abc123", file_name: "MyFile.txt", file_size: 7, uploaded: "01/01/01", - annotations: [{ name: "Local File Path", values: [localPath] }], + annotations: [ + { name: "Local File Path", values: [`/allen/${filePathInsideAllenDrive}`] }, + ], }); + const localPath = `${hostMountPoint}/${filePathInsideAllenDrive}`; + // Act const { findByText } = render( From 7e94c3c04866950986edbd8e892d0527cef2d717 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Wed, 18 Dec 2024 12:27:14 -0800 Subject: [PATCH 35/56] seperate table --- .../Modal/CopyFileManifest/index.tsx | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/packages/core/components/Modal/CopyFileManifest/index.tsx b/packages/core/components/Modal/CopyFileManifest/index.tsx index bd29c960..3d998acc 100644 --- a/packages/core/components/Modal/CopyFileManifest/index.tsx +++ b/packages/core/components/Modal/CopyFileManifest/index.tsx @@ -49,18 +49,31 @@ export default function CopyFileManifest({ onDismiss }: ModalProps) { fetchDetails(); }, [fileSelection, fileService]); + // Handler for moving files to NAS cache const onMove = () => { dispatch(interaction.actions.copyFiles(fileDetails)); onDismiss(); }; - const body = ( -
-

- Files copied to the local NAS cache (VAST) are stored with a 180-day lease, after - which they revert to cloud-only. To renew the lease, simply reselect the files and - confirm the copy. -

+ // Separate files by "Should Be in Local Cache" + 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 + ) + ); + + // Reusable function to render a table for files + const renderTable = (files: FileDetail[], title: string) => ( +
+

{title}

@@ -70,7 +83,7 @@ export default function CopyFileManifest({ onDismiss }: ModalProps) { - {fileDetails.map((file) => ( + {files.map((file) => ( @@ -79,6 +92,21 @@ export default function CopyFileManifest({ onDismiss }: ModalProps) {
{clipFileName(file.name)} {filesize(file.size || 0)}
+
+ ); + + const body = ( +
+

+ Files copied to the local NAS cache (VAST) are stored with a 180-day lease, after + which they revert to cloud-only. To renew the lease, simply reselect the files and + confirm the copy. +

+ {renderTable( + filesInLocalCache, + "Files that are already in Local Cache (VAST) to renew lease for" + )} + {renderTable(filesNotInLocalCache, "Files to Download to Local Cache (VAST)")}
{isLoading ? "Calculating..." : totalSize || "0 B"} From 4932dc8073eba8d0c9c263e92d91ac17305006e6 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Wed, 18 Dec 2024 12:38:48 -0800 Subject: [PATCH 36/56] 368 Set custom /allen mount point for File Path (Local) --- .../FileDetails/FileAnnotationList.tsx | 35 ++++++++++--------- packages/core/entity/FileDetail/index.ts | 9 +++++ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/core/components/FileDetails/FileAnnotationList.tsx b/packages/core/components/FileDetails/FileAnnotationList.tsx index abe6d96e..f5dea66f 100644 --- a/packages/core/components/FileDetails/FileAnnotationList.tsx +++ b/packages/core/components/FileDetails/FileAnnotationList.tsx @@ -37,7 +37,14 @@ export default function FileAnnotationList(props: FileAnnotationListProps) { return; } - const path = await executionEnvService.formatPathForHost(fileDetails.path); + let path; + if (fileDetails.localPath === null) { + // The Local File Path annotation is not defined because the file is not available + // on-premises + path = fileDetails.localPath; + } else { + path = await executionEnvService.formatPathForHost(fileDetails.localPath); + } if (!active) { return; } @@ -65,12 +72,22 @@ export default function FileAnnotationList(props: FileAnnotationListProps) { return accum; } - const annotationValue = annotation.extractFromFile(fileDetails); + let annotationValue = annotation.extractFromFile(fileDetails); if (annotationValue === Annotation.MISSING_VALUE) { // Nothing to show for this annotation -- skip return accum; } + if (annotation.name === AnnotationName.LOCAL_FILE_PATH) { + if (localPath === null) { + // localPath hasn't loaded yet + return accum; + } else { + // Use the user's /allen mount point, if known + annotationValue = localPath; + } + } + const ret = [ ...accum, ); } - - // In certain circumstances (i.e., linux), the path at which a file is accessible is === the canonical path - if (localPath && localPath !== annotationValue && !localPath.startsWith("http")) { - ret.splice( - -1, // Insert before the "canonical" path so that it is the first path-like row to be seen - 0, // ...don't delete the "canonical" path - - ); - } } return ret; diff --git a/packages/core/entity/FileDetail/index.ts b/packages/core/entity/FileDetail/index.ts index 0decf267..f973d70a 100644 --- a/packages/core/entity/FileDetail/index.ts +++ b/packages/core/entity/FileDetail/index.ts @@ -119,6 +119,15 @@ export default class FileDetail { return path as string; } + public get localPath(): string | null { + const localPath = this.getFirstAnnotationValue("Local File Path"); + console.log("local path is ", localPath); + if (localPath === undefined) { + return null; + } + return localPath as string; + } + public get cloudPath(): string { // Can retrieve a cloud like path for AICS FMS files if (this.path.startsWith("/allen")) { From a94c95ec654332708ddf91c18015248ce4ad3f80 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Wed, 18 Dec 2024 12:48:38 -0800 Subject: [PATCH 37/56] update wording --- .../components/Modal/CopyFileManifest/index.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/core/components/Modal/CopyFileManifest/index.tsx b/packages/core/components/Modal/CopyFileManifest/index.tsx index 3d998acc..3e8b345f 100644 --- a/packages/core/components/Modal/CopyFileManifest/index.tsx +++ b/packages/core/components/Modal/CopyFileManifest/index.tsx @@ -98,15 +98,12 @@ export default function CopyFileManifest({ onDismiss }: ModalProps) { const body = (

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

- {renderTable( - filesInLocalCache, - "Files that are already in Local Cache (VAST) to renew lease for" - )} - {renderTable(filesNotInLocalCache, "Files to Download to Local Cache (VAST)")} + {renderTable(filesInLocalCache, "Files that are already on Vast: Extend expiration")} + {renderTable(filesNotInLocalCache, "Files to download to Vast")}
{isLoading ? "Calculating..." : totalSize || "0 B"} @@ -139,7 +136,7 @@ export default function CopyFileManifest({ onDismiss }: ModalProps) {
} onDismiss={onDismiss} - title="Copy Files to NAS Cache (VAST)" + title="Copy Files to Local NAS (Vast)" /> ); } From 1c54d471311a9417853636d31fb05313d4a03999 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Thu, 19 Dec 2024 15:06:37 -0800 Subject: [PATCH 38/56] 368 Remove File Path (Local) top-level annotation to avoid duplication --- packages/core/constants/index.ts | 6 ------ packages/core/entity/FileDetail/index.ts | 1 - 2 files changed, 7 deletions(-) diff --git a/packages/core/constants/index.ts b/packages/core/constants/index.ts index c3bd5ce4..75d289a4 100644 --- a/packages/core/constants/index.ts +++ b/packages/core/constants/index.ts @@ -31,12 +31,6 @@ export const TOP_LEVEL_FILE_ANNOTATIONS = [ description: "Path to file in storage.", type: AnnotationType.STRING, }), - new Annotation({ - annotationDisplayName: "File Path (Local)", - annotationName: AnnotationName.LOCAL_FILE_PATH, - description: "Path to the file on-premises.", - type: AnnotationType.STRING, - }), new Annotation({ annotationDisplayName: "Size", annotationName: AnnotationName.FILE_SIZE, diff --git a/packages/core/entity/FileDetail/index.ts b/packages/core/entity/FileDetail/index.ts index f973d70a..ac944805 100644 --- a/packages/core/entity/FileDetail/index.ts +++ b/packages/core/entity/FileDetail/index.ts @@ -121,7 +121,6 @@ export default class FileDetail { public get localPath(): string | null { const localPath = this.getFirstAnnotationValue("Local File Path"); - console.log("local path is ", localPath); if (localPath === undefined) { return null; } From 1126cd96dc02385e31841e8d7ec206e84b319c8a Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Thu, 19 Dec 2024 15:27:14 -0800 Subject: [PATCH 39/56] 368 Rename annotation at the HTTPAnnotationService level --- .../FMSAnnotationPresentationLayer.ts | 13 +++++++++++++ .../HttpAnnotationService/index.ts | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 packages/core/services/AnnotationService/HttpAnnotationService/FMSAnnotationPresentationLayer.ts diff --git a/packages/core/services/AnnotationService/HttpAnnotationService/FMSAnnotationPresentationLayer.ts b/packages/core/services/AnnotationService/HttpAnnotationService/FMSAnnotationPresentationLayer.ts new file mode 100644 index 00000000..9cadab47 --- /dev/null +++ b/packages/core/services/AnnotationService/HttpAnnotationService/FMSAnnotationPresentationLayer.ts @@ -0,0 +1,13 @@ +import { AnnotationResponse } from "../../../entity/Annotation"; +import AnnotationName from "../../../entity/Annotation/AnnotationName"; + +export default function renameAnnotation(annotation: AnnotationResponse): AnnotationResponse { + if (annotation.annotationName === AnnotationName.LOCAL_FILE_PATH) { + return { + ...annotation, + annotationDisplayName: "File Path (Local)", + } as AnnotationResponse; + } else { + return annotation; + } +} diff --git a/packages/core/services/AnnotationService/HttpAnnotationService/index.ts b/packages/core/services/AnnotationService/HttpAnnotationService/index.ts index 0fdb9ff1..a21647c0 100644 --- a/packages/core/services/AnnotationService/HttpAnnotationService/index.ts +++ b/packages/core/services/AnnotationService/HttpAnnotationService/index.ts @@ -4,6 +4,7 @@ import AnnotationService, { AnnotationValue } from ".."; import HttpServiceBase from "../../HttpServiceBase"; import Annotation, { AnnotationResponse } from "../../../entity/Annotation"; import FileFilter from "../../../entity/FileFilter"; +import renameAnnotation from "./FMSAnnotationPresentationLayer"; import { TOP_LEVEL_FILE_ANNOTATIONS, TOP_LEVEL_FILE_ANNOTATION_NAMES } from "../../../constants"; enum QueryParam { @@ -35,7 +36,10 @@ export default class HttpAnnotationService extends HttpServiceBase implements An const response = await this.get(requestUrl); return [ ...TOP_LEVEL_FILE_ANNOTATIONS, - ...map(response.data, (annotationResponse) => new Annotation(annotationResponse)), + ...map( + response.data, + (annotationResponse) => new Annotation(renameAnnotation(annotationResponse)) + ), ]; } From 022377f9bf85647ac2adc67d5789cf4acd56d409 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Thu, 19 Dec 2024 15:49:50 -0800 Subject: [PATCH 40/56] 368 Test expects specific annotation from LabKey (not top-level annotation) --- .../FileDetails/test/FileAnnotationList.test.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx index 84a9c250..f283fcfd 100644 --- a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx +++ b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx @@ -9,6 +9,8 @@ import FileDetail from "../../../entity/FileDetail"; import ExecutionEnvServiceNoop from "../../../services/ExecutionEnvService/ExecutionEnvServiceNoop"; import { initialState } from "../../../state"; import { TOP_LEVEL_FILE_ANNOTATIONS } from "../../../constants"; +import Annotation from "../../../entity/Annotation"; +import { AnnotationType } from "../../../entity/AnnotationFormatter"; describe("", () => { describe("file path representation", () => { @@ -25,7 +27,15 @@ describe("", () => { const { store } = configureMockStore({ state: mergeState(initialState, { metadata: { - annotations: TOP_LEVEL_FILE_ANNOTATIONS, + annotations: [ + ...TOP_LEVEL_FILE_ANNOTATIONS, + new Annotation({ + annotationName: "Local File Path", + annotationDisplayName: "File Path (Local)", + description: "Path to file in on-premises storage.", + type: AnnotationType.STRING, + }), + ], }, interaction: { platformDependentServices: { From 1642a6858a92b1ab25e81c814451ca47252a7f58 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Thu, 19 Dec 2024 15:54:00 -0800 Subject: [PATCH 41/56] update table gradient and summeries --- .../CopyFileManifest.module.css | 41 +++- .../Modal/CopyFileManifest/index.tsx | 178 ++++++++++-------- 2 files changed, 136 insertions(+), 83 deletions(-) diff --git a/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css b/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css index 624ed4b1..48482b9b 100644 --- a/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css +++ b/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css @@ -2,16 +2,49 @@ 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; - margin-bottom: 0.5em; - background: linear-gradient(to bottom, transparent, var(--secondary-background-color)); + 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, @@ -20,13 +53,14 @@ 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: 1; + z-index: 3; } .file-table td:first-child { @@ -57,4 +91,3 @@ .fileCount { text-align: right; } - diff --git a/packages/core/components/Modal/CopyFileManifest/index.tsx b/packages/core/components/Modal/CopyFileManifest/index.tsx index 3e8b345f..c18e1bd8 100644 --- a/packages/core/components/Modal/CopyFileManifest/index.tsx +++ b/packages/core/components/Modal/CopyFileManifest/index.tsx @@ -11,51 +11,101 @@ 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 operations. */ export default function CopyFileManifest({ 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); - - // Utility function to clip file names - const clipFileName = (filename: string) => { - if (filename.length > 20) { - return filename.slice(0, 9) + "..." + filename.slice(-8); - } - return filename; - }; 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]); + }, [fileSelection]); - // Handler for moving files to NAS cache const onMove = () => { dispatch(interaction.actions.copyFiles(fileDetails)); onDismiss(); }; - // Separate files by "Should Be in Local Cache" const filesInLocalCache = fileDetails.filter((file) => file.annotations.some( (annotation) => @@ -63,63 +113,39 @@ export default function CopyFileManifest({ onDismiss }: ModalProps) { ) ); - const filesNotInLocalCache = fileDetails.filter((file) => - file.annotations.some( - (annotation) => - annotation.name === "Should Be in Local Cache" && annotation.values[0] === false - ) - ); - - // Reusable function to render a table for files - const renderTable = (files: FileDetail[], title: string) => ( -
-

{title}

-
- - - - - - - - - {files.map((file) => ( - - - - - ))} - -
File NameFile Size
{clipFileName(file.name)}{filesize(file.size || 0)}
-
-
- ); - - const body = ( -
-

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

- {renderTable(filesInLocalCache, "Files that are already on Vast: Extend expiration")} - {renderTable(filesNotInLocalCache, "Files to download to Vast")} -
- - {isLoading ? "Calculating..." : totalSize || "0 B"} - - - {fileDetails.length.toLocaleString()} files - -
-
+ 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 NAS (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 NAS (Vast)" + title="Copy files to local NAS (VAST)" /> ); } From 083456f42c8a8a3a27e0e6503df236d33c0bcd7a Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Thu, 19 Dec 2024 15:56:02 -0800 Subject: [PATCH 42/56] 368 Test for new HTTPAnnotationService special case --- packages/core/entity/Annotation/mocks.ts | 6 ++++++ .../test/HttpAnnotationService.test.ts | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/core/entity/Annotation/mocks.ts b/packages/core/entity/Annotation/mocks.ts index 4c85c304..46064f07 100644 --- a/packages/core/entity/Annotation/mocks.ts +++ b/packages/core/entity/Annotation/mocks.ts @@ -29,4 +29,10 @@ export const annotationsJson = [ description: "Imaging objective", type: "Number", }, + { + annotationName: "Local File Path", + annotationDisplayName: "Local File Path", + description: "Path to file in on-premises storage.", + type: "Text", + }, ]; diff --git a/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts b/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts index 9d9db25d..e5e31af5 100644 --- a/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts +++ b/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts @@ -31,6 +31,16 @@ describe("HttpAnnotationService", () => { ); expect(annotations[0]).to.be.instanceOf(Annotation); }); + + it("renames Local File Path to File Path (Local)", async () => { + const annotationService = new HttpAnnotationService({ + fileExplorerServiceBaseUrl: FESBaseUrl.TEST, + httpClient, + }); + const annotations = await annotationService.fetchAnnotations(); + const localPathAnnotation = annotations.find((a) => a.name === "Local File Path"); + expect(localPathAnnotation?.displayName).to.equal("File Path (Local)"); + }); }); describe("fetchAnnotationValues", () => { From da16e2354755f33a1285bc684b9e68a01ed3b3d8 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Thu, 19 Dec 2024 16:00:55 -0800 Subject: [PATCH 43/56] File Path (Vast) instead of File Path (Local) --- .../components/FileDetails/test/FileAnnotationList.test.tsx | 6 +++--- .../HttpAnnotationService/FMSAnnotationPresentationLayer.ts | 2 +- .../test/HttpAnnotationService.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx index f283fcfd..22db100c 100644 --- a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx +++ b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx @@ -31,7 +31,7 @@ describe("", () => { ...TOP_LEVEL_FILE_ANNOTATIONS, new Annotation({ annotationName: "Local File Path", - annotationDisplayName: "File Path (Local)", + annotationDisplayName: "File Path (Vast)", description: "Path to file in on-premises storage.", type: AnnotationType.STRING, }), @@ -71,7 +71,7 @@ describe("", () => { for (const cellText of [ "File Path (Canonical)", filePath, - "File Path (Local)", + "File Path (Vast)", localPath, ]) { expect(await findByText(cellText)).to.not.be.undefined; @@ -118,7 +118,7 @@ describe("", () => { ); // Assert - expect(() => getByText("File Path (Local)")).to.throw(); + expect(() => getByText("File Path (Vast)")).to.throw(); ["File Path (Canonical)", filePath].forEach((cellText) => { expect(getByText(cellText)).to.not.be.undefined; }); diff --git a/packages/core/services/AnnotationService/HttpAnnotationService/FMSAnnotationPresentationLayer.ts b/packages/core/services/AnnotationService/HttpAnnotationService/FMSAnnotationPresentationLayer.ts index 9cadab47..dcd8fede 100644 --- a/packages/core/services/AnnotationService/HttpAnnotationService/FMSAnnotationPresentationLayer.ts +++ b/packages/core/services/AnnotationService/HttpAnnotationService/FMSAnnotationPresentationLayer.ts @@ -5,7 +5,7 @@ export default function renameAnnotation(annotation: AnnotationResponse): Annota if (annotation.annotationName === AnnotationName.LOCAL_FILE_PATH) { return { ...annotation, - annotationDisplayName: "File Path (Local)", + annotationDisplayName: "File Path (Vast)", } as AnnotationResponse; } else { return annotation; diff --git a/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts b/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts index e5e31af5..281870a8 100644 --- a/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts +++ b/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts @@ -32,14 +32,14 @@ describe("HttpAnnotationService", () => { expect(annotations[0]).to.be.instanceOf(Annotation); }); - it("renames Local File Path to File Path (Local)", async () => { + it("renames Local File Path to File Path (Vast)", async () => { const annotationService = new HttpAnnotationService({ fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient, }); const annotations = await annotationService.fetchAnnotations(); const localPathAnnotation = annotations.find((a) => a.name === "Local File Path"); - expect(localPathAnnotation?.displayName).to.equal("File Path (Local)"); + expect(localPathAnnotation?.displayName).to.equal("File Path (Vast)"); }); }); From 6215d9ec35b0c75ed3fc31a27adaa1a379a7c220 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Wed, 18 Dec 2024 09:56:04 -0800 Subject: [PATCH 44/56] 367 FMS file_path is the cloud path (tests failing) --- .../FileDetails/FileAnnotationList.tsx | 16 -------------- packages/core/constants/index.ts | 4 ++-- packages/core/entity/FileDetail/index.ts | 22 +++++++++++++------ 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/packages/core/components/FileDetails/FileAnnotationList.tsx b/packages/core/components/FileDetails/FileAnnotationList.tsx index f5dea66f..0c3846d6 100644 --- a/packages/core/components/FileDetails/FileAnnotationList.tsx +++ b/packages/core/components/FileDetails/FileAnnotationList.tsx @@ -98,22 +98,6 @@ export default function FileAnnotationList(props: FileAnnotationListProps) { />, ]; - // Special case for file paths: we want to display both the "canonical" FMS path - // (i.e. POSIX path held in the database; what we have an annotation for) - // as well as the path at which the file is *actually* accessible on _this_ computer ("local" file path) - if (annotation.name === AnnotationName.FILE_PATH) { - if (fileDetails.path !== fileDetails.cloudPath) { - ret.push( - - ); - } - } - return ret; }, [] as JSX.Element[]); }, [annotations, fileDetails, isLoading, localPath]); diff --git a/packages/core/constants/index.ts b/packages/core/constants/index.ts index 75d289a4..ff53eb69 100644 --- a/packages/core/constants/index.ts +++ b/packages/core/constants/index.ts @@ -26,9 +26,9 @@ export const TOP_LEVEL_FILE_ANNOTATIONS = [ type: AnnotationType.STRING, }), new Annotation({ - annotationDisplayName: "File Path (Canonical)", + annotationDisplayName: "File Path (Cloud)", annotationName: AnnotationName.FILE_PATH, - description: "Path to file in storage.", + description: "Path to file in the cloud.", type: AnnotationType.STRING, }), new Annotation({ diff --git a/packages/core/entity/FileDetail/index.ts b/packages/core/entity/FileDetail/index.ts index ac944805..5e59cb5b 100644 --- a/packages/core/entity/FileDetail/index.ts +++ b/packages/core/entity/FileDetail/index.ts @@ -4,6 +4,8 @@ import { renderZarrThumbnailURL } from "./RenderZarrThumbnailURL"; const RENDERABLE_IMAGE_FORMATS = [".jpg", ".jpeg", ".png", ".gif"]; +const AICS_FMS_S3_BUCKET = "production.files.allencell.org"; + /** * Expected JSON response of a file detail returned from the query service. Example: * { @@ -79,7 +81,11 @@ export default class FileDetail { const pathWithoutDrive = path.replace("/allen/programs/allencell/data/proj0", ""); // Should probably record this somewhere we can dynamically adjust to, or perhaps just in the file // document itself, alas for now this will do. - return `https://s3.us-west-2.amazonaws.com/production.files.allencell.org${pathWithoutDrive}`; + return `https://s3.us-west-2.amazonaws.com/${AICS_FMS_S3_BUCKET}${pathWithoutDrive}`; + } + + private static convertAicsS3PathToHttpUrl(path: string): string { + return `https://s3.us-west-2.amazonaws.com/${path}`; } constructor(fileDetail: FmsFile, uniqueId?: string) { @@ -116,6 +122,13 @@ export default class FileDetail { if (path === undefined) { throw new Error("File Path is not defined"); } + + // AICS FMS files have paths like staging.files.allencell.org/130/b23/bfe/117/2a4/71b/746/002/064/db4/1a/danny_int_test_4.txt + if (typeof path === "string" && path.startsWith(AICS_FMS_S3_BUCKET)) { + return FileDetail.convertAicsS3PathToHttpUrl(path) as string; + } + + // Otherwise just return the path as is and hope for the best return path as string; } @@ -128,12 +141,7 @@ export default class FileDetail { } public get cloudPath(): string { - // Can retrieve a cloud like path for AICS FMS files - if (this.path.startsWith("/allen")) { - return FileDetail.convertAicsDrivePathToAicsS3Path(this.path); - } - - // Otherwise just return the path as is and hope for the best + // AICS FMS files' paths are cloud paths return this.path; } From 160c0fe382132c28bcc8cfb59241dea3f7b54353 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Thu, 19 Dec 2024 17:41:43 -0800 Subject: [PATCH 45/56] 367 Read cloud path from FileDetail object with http://... prefix --- .../FileDetails/FileAnnotationList.tsx | 9 ++++--- .../test/FileAnnotationList.test.tsx | 24 +++++++++---------- packages/core/entity/FileDetail/index.ts | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/core/components/FileDetails/FileAnnotationList.tsx b/packages/core/components/FileDetails/FileAnnotationList.tsx index 0c3846d6..42600287 100644 --- a/packages/core/components/FileDetails/FileAnnotationList.tsx +++ b/packages/core/components/FileDetails/FileAnnotationList.tsx @@ -88,7 +88,12 @@ export default function FileAnnotationList(props: FileAnnotationListProps) { } } - const ret = [ + if (annotation.name === AnnotationName.FILE_PATH) { + // Display the full http://... URL + annotationValue = fileDetails.cloudPath; + } + + return [ ...accum, , ]; - - return ret; }, [] as JSX.Element[]); }, [annotations, fileDetails, isLoading, localPath]); diff --git a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx index 22db100c..3a2be2b4 100644 --- a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx +++ b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx @@ -14,7 +14,7 @@ import { AnnotationType } from "../../../entity/AnnotationFormatter"; describe("", () => { describe("file path representation", () => { - it("has both canonical file path and file path adjusted to OS & allen mount point", async () => { + it("has both cloud file path and local file path adjusted to OS & allen mount point", async () => { // Arrange const hostMountPoint = "/some/path"; @@ -46,7 +46,7 @@ describe("", () => { }); const filePathInsideAllenDrive = "path/to/MyFile.txt"; - const filePath = `production.allencell.org/${filePathInsideAllenDrive}`; + const filePath = `production.files.allencell.org/${filePathInsideAllenDrive}`; const fileDetails = new FileDetail({ file_path: filePath, file_id: "abc123", @@ -58,8 +58,6 @@ describe("", () => { ], }); - const localPath = `${hostMountPoint}/${filePathInsideAllenDrive}`; - // Act const { findByText } = render( @@ -69,16 +67,16 @@ describe("", () => { // Assert for (const cellText of [ - "File Path (Canonical)", - filePath, + "File Path (Cloud)", + `https://s3.us-west-2.amazonaws.com/${filePath}`, "File Path (Vast)", - localPath, + `${hostMountPoint}/${filePathInsideAllenDrive}`, ]) { expect(await findByText(cellText)).to.not.be.undefined; } }); - it("has only canonical file path when no allen mount point is found", () => { + it("has only cloud file path when no allen mount point is found", () => { // Arrange class FakeExecutionEnvService extends ExecutionEnvServiceNoop { public formatPathForHost(posixPath: string): Promise { @@ -100,7 +98,7 @@ describe("", () => { }); const filePathInsideAllenDrive = "path/to/MyFile.txt"; - const filePath = `/allen/${filePathInsideAllenDrive}`; + const filePath = `production.files.allencell.org/${filePathInsideAllenDrive}`; const fileDetails = new FileDetail({ file_path: filePath, file_id: "abc123", @@ -119,9 +117,11 @@ describe("", () => { // Assert expect(() => getByText("File Path (Vast)")).to.throw(); - ["File Path (Canonical)", filePath].forEach((cellText) => { - expect(getByText(cellText)).to.not.be.undefined; - }); + ["File Path (Cloud)", `https://s3.us-west-2.amazonaws.com/${filePath}`].forEach( + (cellText) => { + expect(getByText(cellText)).to.not.be.undefined; + } + ); }); }); }); diff --git a/packages/core/entity/FileDetail/index.ts b/packages/core/entity/FileDetail/index.ts index 5e59cb5b..dd7da28d 100644 --- a/packages/core/entity/FileDetail/index.ts +++ b/packages/core/entity/FileDetail/index.ts @@ -81,7 +81,7 @@ export default class FileDetail { const pathWithoutDrive = path.replace("/allen/programs/allencell/data/proj0", ""); // Should probably record this somewhere we can dynamically adjust to, or perhaps just in the file // document itself, alas for now this will do. - return `https://s3.us-west-2.amazonaws.com/${AICS_FMS_S3_BUCKET}${pathWithoutDrive}`; + return FileDetail.convertAicsS3PathToHttpUrl(`${AICS_FMS_S3_BUCKET}${pathWithoutDrive}`); } private static convertAicsS3PathToHttpUrl(path: string): string { From 66f572c2f0f8bc49b926c514dc270a587bc3f334 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Fri, 20 Dec 2024 11:01:52 -0800 Subject: [PATCH 46/56] 367 Hoist AICS_FMS_S3_URL_PREFIX to top-level constant --- packages/core/entity/FileDetail/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/entity/FileDetail/index.ts b/packages/core/entity/FileDetail/index.ts index dd7da28d..ab2a8f1e 100644 --- a/packages/core/entity/FileDetail/index.ts +++ b/packages/core/entity/FileDetail/index.ts @@ -5,6 +5,7 @@ import { renderZarrThumbnailURL } from "./RenderZarrThumbnailURL"; const RENDERABLE_IMAGE_FORMATS = [".jpg", ".jpeg", ".png", ".gif"]; const AICS_FMS_S3_BUCKET = "production.files.allencell.org"; +const AICS_FMS_S3_URL_PREFIX = "https://s3.us-west-2.amazonaws.com/"; /** * Expected JSON response of a file detail returned from the query service. Example: @@ -85,7 +86,7 @@ export default class FileDetail { } private static convertAicsS3PathToHttpUrl(path: string): string { - return `https://s3.us-west-2.amazonaws.com/${path}`; + return `${AICS_FMS_S3_URL_PREFIX}${path}`; } constructor(fileDetail: FmsFile, uniqueId?: string) { From 88173323832b26611f456ca2a1aaf2638264255f Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Fri, 20 Dec 2024 11:04:09 -0800 Subject: [PATCH 47/56] 367 More detailed file_path comment --- packages/core/entity/FileDetail/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/entity/FileDetail/index.ts b/packages/core/entity/FileDetail/index.ts index ab2a8f1e..a21a22fd 100644 --- a/packages/core/entity/FileDetail/index.ts +++ b/packages/core/entity/FileDetail/index.ts @@ -124,7 +124,8 @@ export default class FileDetail { throw new Error("File Path is not defined"); } - // AICS FMS files have paths like staging.files.allencell.org/130/b23/bfe/117/2a4/71b/746/002/064/db4/1a/danny_int_test_4.txt + // AICS FMS files have paths like this in fileDetail.file_path: + // staging.files.allencell.org/130/b23/bfe/117/2a4/71b/746/002/064/db4/1a/danny_int_test_4.txt if (typeof path === "string" && path.startsWith(AICS_FMS_S3_BUCKET)) { return FileDetail.convertAicsS3PathToHttpUrl(path) as string; } From 3cab1ac89cb76cfdda64731da3c211084f378422 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Fri, 20 Dec 2024 11:07:20 -0800 Subject: [PATCH 48/56] 368 import ordering --- .../services/AnnotationService/HttpAnnotationService/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/services/AnnotationService/HttpAnnotationService/index.ts b/packages/core/services/AnnotationService/HttpAnnotationService/index.ts index a21647c0..8fbbde90 100644 --- a/packages/core/services/AnnotationService/HttpAnnotationService/index.ts +++ b/packages/core/services/AnnotationService/HttpAnnotationService/index.ts @@ -1,10 +1,10 @@ import { map } from "lodash"; +import renameAnnotation from "./FMSAnnotationPresentationLayer"; import AnnotationService, { AnnotationValue } from ".."; import HttpServiceBase from "../../HttpServiceBase"; import Annotation, { AnnotationResponse } from "../../../entity/Annotation"; import FileFilter from "../../../entity/FileFilter"; -import renameAnnotation from "./FMSAnnotationPresentationLayer"; import { TOP_LEVEL_FILE_ANNOTATIONS, TOP_LEVEL_FILE_ANNOTATION_NAMES } from "../../../constants"; enum QueryParam { From efccc6c78bd5038685b43fbb9713721bfde8b374 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Fri, 20 Dec 2024 11:14:30 -0800 Subject: [PATCH 49/56] remove NAS wording --- .../core/components/Modal/CopyFileManifest/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/core/components/Modal/CopyFileManifest/index.tsx b/packages/core/components/Modal/CopyFileManifest/index.tsx index c18e1bd8..8deeecd2 100644 --- a/packages/core/components/Modal/CopyFileManifest/index.tsx +++ b/packages/core/components/Modal/CopyFileManifest/index.tsx @@ -82,7 +82,7 @@ function FileTable({ files, title }: { files: FileDetail[]; title: string }) { } /** - * Modal overlay for displaying details of selected files for NAS cache operations. + * Modal overlay for displaying details of selected files for NAS cache (VAST) operations. */ export default function CopyFileManifest({ onDismiss }: ModalProps) { const dispatch = useDispatch(); @@ -127,9 +127,9 @@ export default function CopyFileManifest({ onDismiss }: ModalProps) { body={

- Files copied to the local NAS (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. + 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.

} onDismiss={onDismiss} - title="Copy files to local NAS (VAST)" + title="Copy files to local storage (VAST)" /> ); } From 29423b0518569239c52c6c0b912f6fb661616d01 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Fri, 20 Dec 2024 11:18:24 -0800 Subject: [PATCH 50/56] 368 Clearer comment about loading localPath --- packages/core/components/FileDetails/FileAnnotationList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/components/FileDetails/FileAnnotationList.tsx b/packages/core/components/FileDetails/FileAnnotationList.tsx index f5dea66f..f86fb25d 100644 --- a/packages/core/components/FileDetails/FileAnnotationList.tsx +++ b/packages/core/components/FileDetails/FileAnnotationList.tsx @@ -80,7 +80,8 @@ export default function FileAnnotationList(props: FileAnnotationListProps) { if (annotation.name === AnnotationName.LOCAL_FILE_PATH) { if (localPath === null) { - // localPath hasn't loaded yet + // localPath hasn't loaded yet, but it should eventually because there is an + // annotation named AnnotationName.LOCAL_FILE_PATH return accum; } else { // Use the user's /allen mount point, if known From 2736712d3bd063f60c748ebd4746c462e11bc471 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Fri, 20 Dec 2024 11:28:54 -0800 Subject: [PATCH 51/56] 376 Downloads use labkey URL when file is available on Vast --- packages/core/entity/FileDetail/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/entity/FileDetail/index.ts b/packages/core/entity/FileDetail/index.ts index a21a22fd..28d7ae94 100644 --- a/packages/core/entity/FileDetail/index.ts +++ b/packages/core/entity/FileDetail/index.ts @@ -148,10 +148,10 @@ export default class FileDetail { } public get downloadPath(): string { - // For AICS files we don't have permission to the bucket nor do we expect to have the /allen - // drive mounted on the client machine. So we use the NGINX server to serve the file. - if (this.path.startsWith("/allen")) { - return `http://aics.corp.alleninstitute.org/labkey/fmsfiles/image${this.path}`; + // For AICS files that are available on the Vast, users can use the cloud path, but the + // download will be faster and not incur egress fees if we download via the local network. + if (this.localPath && this.localPath.startsWith("/allen")) { + return `http://aics.corp.alleninstitute.org/labkey/fmsfiles/image${this.localPath}`; } // Otherwise just return the path as is and hope for the best From f706be09d92123aa68b2f644bca5c8583a3f5ec6 Mon Sep 17 00:00:00 2001 From: Philip Garrison Date: Fri, 20 Dec 2024 12:02:21 -0800 Subject: [PATCH 52/56] 376 Test for download path --- .../entity/FileDetail/test/FileDetail.test.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 packages/core/entity/FileDetail/test/FileDetail.test.ts diff --git a/packages/core/entity/FileDetail/test/FileDetail.test.ts b/packages/core/entity/FileDetail/test/FileDetail.test.ts new file mode 100644 index 00000000..a9f4f279 --- /dev/null +++ b/packages/core/entity/FileDetail/test/FileDetail.test.ts @@ -0,0 +1,29 @@ +import { expect } from "chai"; + +import FileDetail from ".."; + +describe("FileDetail", () => { + describe("file path representation", () => { + it("creates downloadPath from /allen path", () => { + // Arrange + const relativePath = "path/to/MyFile.txt"; + + // Act + const fileDetail = new FileDetail({ + file_path: `production.files.allencell.org/${relativePath}`, + annotations: [ + { + name: "Local File Path", + values: [`/allen/programs/allencell/data/proj0/${relativePath}`], + }, + ], + }); + + // Assert + expect(fileDetail.downloadPath).to.equal( + // The downloadPath is HTTP, but will get redirected to HTTPS + `http://aics.corp.alleninstitute.org/labkey/fmsfiles/image/allen/programs/allencell/data/proj0/${relativePath}` + ); + }); + }); +}); From 71dd109e17b00a8976ae0f93ee06e24851231bee Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Thu, 2 Jan 2025 12:05:20 -0800 Subject: [PATCH 53/56] update local path label --- .../components/FileDetails/test/FileAnnotationList.test.tsx | 6 +++--- .../HttpAnnotationService/FMSAnnotationPresentationLayer.ts | 2 +- .../test/HttpAnnotationService.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx index 22db100c..a6c0cf2a 100644 --- a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx +++ b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx @@ -31,7 +31,7 @@ describe("", () => { ...TOP_LEVEL_FILE_ANNOTATIONS, new Annotation({ annotationName: "Local File Path", - annotationDisplayName: "File Path (Vast)", + annotationDisplayName: "File Path (Local VAST)", description: "Path to file in on-premises storage.", type: AnnotationType.STRING, }), @@ -71,7 +71,7 @@ describe("", () => { for (const cellText of [ "File Path (Canonical)", filePath, - "File Path (Vast)", + "File Path (Local VAST)", localPath, ]) { expect(await findByText(cellText)).to.not.be.undefined; @@ -118,7 +118,7 @@ describe("", () => { ); // Assert - expect(() => getByText("File Path (Vast)")).to.throw(); + expect(() => getByText("File Path (Local VAST)")).to.throw(); ["File Path (Canonical)", filePath].forEach((cellText) => { expect(getByText(cellText)).to.not.be.undefined; }); diff --git a/packages/core/services/AnnotationService/HttpAnnotationService/FMSAnnotationPresentationLayer.ts b/packages/core/services/AnnotationService/HttpAnnotationService/FMSAnnotationPresentationLayer.ts index dcd8fede..e249e644 100644 --- a/packages/core/services/AnnotationService/HttpAnnotationService/FMSAnnotationPresentationLayer.ts +++ b/packages/core/services/AnnotationService/HttpAnnotationService/FMSAnnotationPresentationLayer.ts @@ -5,7 +5,7 @@ export default function renameAnnotation(annotation: AnnotationResponse): Annota if (annotation.annotationName === AnnotationName.LOCAL_FILE_PATH) { return { ...annotation, - annotationDisplayName: "File Path (Vast)", + annotationDisplayName: "File Path (Local VAST)", } as AnnotationResponse; } else { return annotation; diff --git a/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts b/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts index 281870a8..a5a3c6b2 100644 --- a/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts +++ b/packages/core/services/AnnotationService/HttpAnnotationService/test/HttpAnnotationService.test.ts @@ -32,14 +32,14 @@ describe("HttpAnnotationService", () => { expect(annotations[0]).to.be.instanceOf(Annotation); }); - it("renames Local File Path to File Path (Vast)", async () => { + it("renames Local File Path to File Path (Local VAST)", async () => { const annotationService = new HttpAnnotationService({ fileExplorerServiceBaseUrl: FESBaseUrl.TEST, httpClient, }); const annotations = await annotationService.fetchAnnotations(); const localPathAnnotation = annotations.find((a) => a.name === "Local File Path"); - expect(localPathAnnotation?.displayName).to.equal("File Path (Vast)"); + expect(localPathAnnotation?.displayName).to.equal("File Path (Local VAST)"); }); }); From 042573650199b84d481dd2062f2e0e9d6cfc3545 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Thu, 2 Jan 2025 12:28:58 -0800 Subject: [PATCH 54/56] update annotation name --- .../components/FileDetails/test/FileAnnotationList.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx index 50327e18..b030e5eb 100644 --- a/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx +++ b/packages/core/components/FileDetails/test/FileAnnotationList.test.tsx @@ -69,7 +69,7 @@ describe("", () => { for (const cellText of [ "File Path (Cloud)", `https://s3.us-west-2.amazonaws.com/${filePath}`, - "File Path (Vast)", + "File Path (Local VAST)", `${hostMountPoint}/${filePathInsideAllenDrive}`, ]) { expect(await findByText(cellText)).to.not.be.undefined; @@ -116,7 +116,7 @@ describe("", () => { ); // Assert - expect(() => getByText("File Path (Vast)")).to.throw(); + expect(() => getByText("File Path (Local VAST)")).to.throw(); ["File Path (Cloud)", `https://s3.us-west-2.amazonaws.com/${filePath}`].forEach( (cellText) => { expect(getByText(cellText)).to.not.be.undefined; From c2d97f49dc4c8d2768e72d9efa3bef6015e52635 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Thu, 2 Jan 2025 15:44:04 -0800 Subject: [PATCH 55/56] feature/backwards-etl-compatibility --- packages/core/entity/FileDetail/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/core/entity/FileDetail/index.ts b/packages/core/entity/FileDetail/index.ts index 28d7ae94..86ec9f95 100644 --- a/packages/core/entity/FileDetail/index.ts +++ b/packages/core/entity/FileDetail/index.ts @@ -135,6 +135,11 @@ export default class FileDetail { } public get localPath(): string | null { + // REMOVE THIS (BACKWARDS COMPAT) + if (this.path.startsWith("/allen")) { + return this.path; + } + const localPath = this.getFirstAnnotationValue("Local File Path"); if (localPath === undefined) { return null; @@ -143,11 +148,21 @@ export default class FileDetail { } public get cloudPath(): string { + // REMOVE THIS (BACKWARDS COMPAT) + if (this.path.startsWith("/allen")) { + return FileDetail.convertAicsDrivePathToAicsS3Path(this.path); + } + // AICS FMS files' paths are cloud paths return this.path; } public get downloadPath(): string { + // REMOVE THIS (BACKWARDS COMPAT) + if (this.path.startsWith("/allen")) { + return `http://aics.corp.alleninstitute.org/labkey/fmsfiles/image${this.path}`; + } + // For AICS files that are available on the Vast, users can use the cloud path, but the // download will be faster and not incur egress fees if we download via the local network. if (this.localPath && this.localPath.startsWith("/allen")) { From b482dcfc948a9bdd213afcb0021f679ba1bb6da8 Mon Sep 17 00:00:00 2001 From: Brian Whitney Date: Fri, 3 Jan 2025 12:27:41 -0800 Subject: [PATCH 56/56] comment res --- .../CopyFileManifest.module.css | 18 +++++++++--------- packages/core/components/Modal/index.tsx | 8 ++++---- .../FileSelection/test/FileSelection.test.ts | 2 +- .../services/PersistentConfigService/index.ts | 2 +- packages/core/state/interaction/reducer.ts | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css b/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css index 48482b9b..8b6a2734 100644 --- a/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css +++ b/packages/core/components/Modal/CopyFileManifest/CopyFileManifest.module.css @@ -1,4 +1,4 @@ -.bodyContainer { +.body-container { position: relative; } @@ -7,21 +7,21 @@ margin-bottom: 1em; } -.tableContainer { +.table-container { margin-bottom: 2em; } -.tableTitle { +.table-title { font-size: 16px; font-weight: 600; margin: 4px 0 8px 0; } -.tableWrapper { +.table-wrapper { position: relative; } -.fileTableContainer { +.file-table-container { max-height: 300px; overflow-y: auto; background-color: var(--secondary-background-color); @@ -29,7 +29,7 @@ z-index: 1; } -.gradientOverlay { +.gradient-overlay { position: absolute; bottom: 0; left: 0; @@ -67,7 +67,7 @@ border-right: 1px solid var(--border-color); } -.footerButtons { +.footer-buttons { display: flex; justify-content: flex-end; gap: 8px; @@ -84,10 +84,10 @@ height: var(--row-count-intrisic-height-height); } -.totalSize { +.total-size { text-align: right; } -.fileCount { +.file-count { text-align: right; } diff --git a/packages/core/components/Modal/index.tsx b/packages/core/components/Modal/index.tsx index ac37e475..ef6d4479 100644 --- a/packages/core/components/Modal/index.tsx +++ b/packages/core/components/Modal/index.tsx @@ -14,10 +14,10 @@ export interface ModalProps { export enum ModalType { CodeSnippet = 1, - DataSource = 2, - MetadataManifest = 3, - SmallScreenWarning = 4, - CopyFileManifest = 5, + CopyFileManifest = 2, + DataSource = 3, + MetadataManifest = 4, + SmallScreenWarning = 5, } /** diff --git a/packages/core/entity/FileSelection/test/FileSelection.test.ts b/packages/core/entity/FileSelection/test/FileSelection.test.ts index ba369db9..5076cc42 100644 --- a/packages/core/entity/FileSelection/test/FileSelection.test.ts +++ b/packages/core/entity/FileSelection/test/FileSelection.test.ts @@ -6,12 +6,12 @@ import FileSet from "../../FileSet"; import NumericRange from "../../NumericRange"; import FileSelection, { FocusDirective } from ".."; -import { FESBaseUrl } from "../../../constants"; import FileDetail from "../../FileDetail"; import FileFilter from "../../FileFilter"; import FuzzyFilter from "../../FileFilter/FuzzyFilter"; import IncludeFilter from "../../FileFilter/IncludeFilter"; import ExcludeFilter from "../../FileFilter/ExcludeFilter"; +import { FESBaseUrl } from "../../../constants"; import { IndexError, ValueError } from "../../../errors"; import HttpFileService from "../../../services/FileService/HttpFileService"; import FileDownloadServiceNoop from "../../../services/FileDownloadService/FileDownloadServiceNoop"; diff --git a/packages/core/services/PersistentConfigService/index.ts b/packages/core/services/PersistentConfigService/index.ts index 577eb0b5..5e1c0fb2 100644 --- a/packages/core/services/PersistentConfigService/index.ts +++ b/packages/core/services/PersistentConfigService/index.ts @@ -28,12 +28,12 @@ export interface PersistedConfig { [PersistedConfigKeys.Columns]?: Column[]; [PersistedConfigKeys.CsvColumns]?: string[]; [PersistedConfigKeys.DisplayAnnotations]?: AnnotationResponse[]; + [PersistedConfigKeys.Environment]?: Environment; [PersistedConfigKeys.ImageJExecutable]?: string; // Deprecated [PersistedConfigKeys.HasUsedApplicationBefore]?: boolean; [PersistedConfigKeys.Queries]?: Query[]; [PersistedConfigKeys.RecentAnnotations]?: string[]; [PersistedConfigKeys.UserSelectedApplications]?: UserSelectedApplication[]; - [PersistedConfigKeys.Environment]?: Environment; } /** diff --git a/packages/core/state/interaction/reducer.ts b/packages/core/state/interaction/reducer.ts index 4439e70e..8abb5b9f 100644 --- a/packages/core/state/interaction/reducer.ts +++ b/packages/core/state/interaction/reducer.ts @@ -53,7 +53,7 @@ export interface InteractionStateBranch { datasetDetailsPanelIsVisible: boolean; fileTypeForVisibleModal: "csv" | "json" | "parquet"; fileFiltersForVisibleModal: FileFilter[]; - environment: "LOCALHOST" | "STAGING" | "PRODUCTION" | "TEST"; + environment: "LOCALHOST" | "PRODUCTION" | "STAGING" | "TEST"; hasDismissedSmallScreenWarning: boolean; hasUsedApplicationBefore: boolean; isAicsEmployee?: boolean;