From 7727c74a1370d557effaa5a1c743bf6c62d3add0 Mon Sep 17 00:00:00 2001 From: dtm2451 Date: Fri, 28 Feb 2025 17:17:40 -0500 Subject: [PATCH 1/5] Add reset & refresh cache button to Metis File/Folder Selection UI --- .../etna-js/components/metis_exploration.tsx | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/etna/packages/etna-js/components/metis_exploration.tsx b/etna/packages/etna-js/components/metis_exploration.tsx index 98064432f7..0b138b0c52 100644 --- a/etna/packages/etna-js/components/metis_exploration.tsx +++ b/etna/packages/etna-js/components/metis_exploration.tsx @@ -1,7 +1,11 @@ import React, { useState, useCallback, useEffect, useMemo } from 'react'; import {metisPath} from 'etna-js/api/metis_api.js' import Autocomplete from '@material-ui/lab/Autocomplete'; -import { Grid, IconButton, TextField } from '@material-ui/core'; +import Grid from '@material-ui/core/Grid'; +import IconButton from '@material-ui/core/IconButton'; +import TextField from '@material-ui/core/TextField'; +import Tooltip from '@material-ui/core/Tooltip'; +import AutorenewIcon from '@material-ui/icons/Autorenew'; import { json_get } from 'etna-js/utils/fetch'; import SubdirectoryArrowRightIcon from '@material-ui/icons/SubdirectoryArrowRight'; import AddIcon from '@material-ui/icons/Add'; @@ -176,6 +180,7 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set className?: string; disablePortal?: boolean; }) { + const [firstBucketPath, setFirstBucketPath] = useState([bucket,path]); const [pathArray, setPathArray] = useState(pathToArray(path, basePath)); const [targetType, setTargetType] = useState(null as 'folder' | 'file' | null) // null = not yet determined const labelUse = label!==undefined ? label : allowFiles ? "File or Folder" : "Folder" @@ -184,6 +189,7 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set const [firstRender, setFirstRender] = useState(true) // tracked so we can leave newly rendered dropdowns closed when not the user's focus const [fetchContents, setFetchContents] = useState([] as string[]) const [fetching, setFetching] = useState(false) + const [reset, setReset] = useState(false); const pathInMetis = useCallback( (folderPath: string) => { return `${project_name}/list/${bucket}/${folderPath}` @@ -285,7 +291,21 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set setPathArray(pathToArray('', basePath)) setFetchContents(['']) } - }, [bucket]) + }, [bucket, reset]) + + useEffect(() => { + if (Object.keys(contentsSeen).length>0) { // Don't run at initialization + setFirstRender(true) + setContentsSeen({}) + setFetchContents(['']) + if (bucket == firstBucketPath[0]) { + setPathArray(pathToArray(firstBucketPath[1], basePath)); + } else { + setPathArray(pathToArray('', basePath)); + } + if (reset) setReset(false); // Double-pass helps to fully clear + } + }, [reset]) // Update main readout (path) whenever anything updates pathArray useEffect(() => { @@ -410,6 +430,17 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set : null } + {index!=0 ? null : + + setReset(true)}> + + + + + } ) })} From e5a3a461340cbac54b18bb89373e486389b2a5d9 Mon Sep 17 00:00:00 2001 From: dtm2451 Date: Fri, 28 Feb 2025 17:18:45 -0500 Subject: [PATCH 2/5] timur - use PickFileOrFolder UI for Brose editing of files and file_collections --- etna/packages/etna-js/actions/file_actions.js | 55 ++++++++++++++----- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/etna/packages/etna-js/actions/file_actions.js b/etna/packages/etna-js/actions/file_actions.js index b0a812e63d..e7b3ccb239 100644 --- a/etna/packages/etna-js/actions/file_actions.js +++ b/etna/packages/etna-js/actions/file_actions.js @@ -1,4 +1,4 @@ -import React, {useRef, useState} from 'react'; +import React, {useRef, useState, useCallback, useEffect} from 'react'; import Modal from 'react-modal'; import ButtonBar from '../components/button_bar'; @@ -6,6 +6,7 @@ import Icon from '../components/icon'; export const STUB = '::blank'; export const TEMP = '::temp'; +import {PickBucket, PickFileOrFolder} from '../components/metis_exploration'; // We don't have a lot of content, so let's get a smaller Modal export const customStyles = { @@ -40,7 +41,28 @@ export const useFileInputActions = ( onChange, onBlur ) => { - const metisPathRef = useRef(null); + const [metisPath, setMetisPath] = useState(''); + + const [bucketName, setBucketName] = useState(''); + const [path, setPath] = useState(''); + const [targetType, setTargetType] = useState(null); + + function changeBucket(e) { + setPath(''); + setTargetType(null); + setBucketName(e); + } + function reset() { + changeBucket(''); + } + + useEffect(() => { + if (targetType=='file') { + setMetisPath(`metis://${CONFIG.project_name}/${bucketName}/${path}`) + } else { + setMetisPath('') + } + }, [bucketName, path, targetType]) return { metisSelector, @@ -71,22 +93,28 @@ export const useFileInputActions = ( appElement={document.querySelector('#root')} >
-

Enter a Metis path

+

Select a Metis file

- - changeBucket(e)} + /> + setPath(e)} + basePath={''} + topLevelPlaceholder={'top-level of bucket'} + path={path} />
selectMetisFile()}, - {type: 'cancel', click: () => setMetis(false)} + {type: 'cancel', click: () => closeModal()} ]} /> {error ? ( @@ -116,11 +144,10 @@ export const useFileInputActions = ( function closeModal() { setMetis(false); + reset(); } function selectMetisFile() { - const metisPath = metisPathRef.current.value; - if (!METIS_PATH_MATCH(metisPath)) { setError(true); return; @@ -132,7 +159,7 @@ export const useFileInputActions = ( onChange(formatFileRevision(metisPath)); onBlur(); - metisPathRef.current.value = null; + reset(); } function formatFileRevision(newValue, files) { From f777936f7e1eb29ced74877b676ad4201f126fc7 Mon Sep 17 00:00:00 2001 From: dtm2451 Date: Fri, 28 Feb 2025 17:23:28 -0500 Subject: [PATCH 3/5] remove tooltip cuz some modals z-level put them in front --- etna/packages/etna-js/components/metis_exploration.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etna/packages/etna-js/components/metis_exploration.tsx b/etna/packages/etna-js/components/metis_exploration.tsx index 0b138b0c52..d8a3af945c 100644 --- a/etna/packages/etna-js/components/metis_exploration.tsx +++ b/etna/packages/etna-js/components/metis_exploration.tsx @@ -431,14 +431,14 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set : null } {index!=0 ? null : - + {/* */} setReset(true)}> - + {/* */} } From 54dbdf536dd1c9cf8547944288dbeca170b9b490 Mon Sep 17 00:00:00 2001 From: dtm2451 Date: Fri, 28 Feb 2025 17:25:13 -0500 Subject: [PATCH 4/5] cleanup --- etna/packages/etna-js/components/metis_exploration.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etna/packages/etna-js/components/metis_exploration.tsx b/etna/packages/etna-js/components/metis_exploration.tsx index d8a3af945c..a167cac2ae 100644 --- a/etna/packages/etna-js/components/metis_exploration.tsx +++ b/etna/packages/etna-js/components/metis_exploration.tsx @@ -291,7 +291,7 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set setPathArray(pathToArray('', basePath)) setFetchContents(['']) } - }, [bucket, reset]) + }, [bucket]) useEffect(() => { if (Object.keys(contentsSeen).length>0) { // Don't run at initialization From 0a5f10837252811323f0062a59c4e6458917e9b3 Mon Sep 17 00:00:00 2001 From: dtm2451 Date: Mon, 3 Mar 2025 13:03:13 -0500 Subject: [PATCH 5/5] PickFileOrFolder - various fixes --- .../etna-js/components/metis_exploration.tsx | 89 +++++++++++++------ 1 file changed, 63 insertions(+), 26 deletions(-) diff --git a/etna/packages/etna-js/components/metis_exploration.tsx b/etna/packages/etna-js/components/metis_exploration.tsx index a167cac2ae..e1e9237405 100644 --- a/etna/packages/etna-js/components/metis_exploration.tsx +++ b/etna/packages/etna-js/components/metis_exploration.tsx @@ -190,6 +190,7 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set const [fetchContents, setFetchContents] = useState([] as string[]) const [fetching, setFetching] = useState(false) const [reset, setReset] = useState(false); + const [reseting, setReseting] = useState(false); const pathInMetis = useCallback( (folderPath: string) => { return `${project_name}/list/${bucket}/${folderPath}` @@ -233,7 +234,18 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set const target = pathArray[pathArray.length-1] // without passing upwards to not trigger extraneous updates in parent component updates at page refresh if (target && target != '') { - useDetermineType(target, stripSlash(arrayToPath(containerPathArray, basePath)), true, false) + const newType = useDetermineType(target, stripSlash(arrayToPath(containerPathArray, basePath)), true, false) + // add + button if needed + if (firstRender && newType=='folder') { + const fullPath = stripSlash(arrayToPath(pathArray, basePath)) + if (!pathSeen(fullPath)) { + fetchFolderContents(fullPath, (f) => { + if (contentUse(f, allowFiles).length > 0) { + setShowAddButton(pathArray.length-1); + } + }) + } + } } if (target && target == '') { useType('folder', false) @@ -255,19 +267,24 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set }, [project_name, bucket, contentsSeen]); + function determineContentsNeeded(pathArray: string[]) { + let contentsNeeded = [''] + for (let i=1; i <= pathArray.length-1; i++) { + const thisPath = stripSlash(arrayToPath(pathArray.slice(0,i), basePath)) + contentsNeeded.push(thisPath) + } + return contentsNeeded + } + // Determine folder contents to grab at very start, or once first valid bucket given useEffect( () => { if (bucket != '' && Object.keys(contentsSeen).length==0 && !fetching) { - let contentsNeeded = [''] - for (let i=1; i <= pathArray.length-1; i++) { - const thisPath = stripSlash(arrayToPath(pathArray.slice(0,i), basePath)) - contentsNeeded.push(thisPath) - } - setFetchContents(contentsNeeded) + setFetchContents(determineContentsNeeded(pathArray)) } }, [bucket]) // Get contents of paths in fetchContents, one at a time. + // Note: Content fetching for a path-targeted folder always happens elsewhere useEffect( ()=>{ if (fetchContents.length>0) { if (pathSeen(fetchContents[0])) { @@ -275,11 +292,7 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set setFetchContents(fetchContents.slice(1)) } else if (!fetching) { setFetching(true) - if (path !='' && fetchContents[0]==path) { - fetchFolderContents(fetchContents[0], (f) => { if (contentUse(f, allowFiles).length > 0) setPathArray([...pathArray, '']) }) - } else { - fetchFolderContents(fetchContents[0]) - } + fetchFolderContents(fetchContents[0]) } } }, [fetchContents, contentsSeen, pathArray]) @@ -290,22 +303,40 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set setFirstRender(true) setPathArray(pathToArray('', basePath)) setFetchContents(['']) + setShowAddButton(-1); } }, [bucket]) + // Reset when 'reset' button clicked + // clear contentsSeen, repopulate fetchContents, set pathArray to original if bucket was same useEffect(() => { - if (Object.keys(contentsSeen).length>0) { // Don't run at initialization - setFirstRender(true) - setContentsSeen({}) - setFetchContents(['']) - if (bucket == firstBucketPath[0]) { - setPathArray(pathToArray(firstBucketPath[1], basePath)); - } else { - setPathArray(pathToArray('', basePath)); + if (reset) { + const resetPathArray = pathToArray(firstBucketPath[1], basePath) + if (!reseting) { // Initial time through + setFirstRender(true); + setContentsSeen({}); + setShowAddButton(-1); + if (bucket == firstBucketPath[0]) { + setReseting(true); // Rely on next renders to fetch contents + setPathArray(resetPathArray); + setFetchContents(determineContentsNeeded(resetPathArray)); + } else { + setReset(false); // Done in this render + setPathArray(pathToArray('', basePath)); + setFetchContents(['']); + } + } else { // next times through + if (fetchContents.length < 1) { + if (resetPathArray.length > 1 || resetPathArray[0]!='') { + const end = resetPathArray.length-1 + updateTarget(resetPathArray[end], resetPathArray.slice(0,-1), end, false, false) + } + setReseting(false); + setReset(false); + } } - if (reset) setReset(false); // Double-pass helps to fully clear } - }, [reset]) + }, [reset, reseting, fetchContents]) // Update main readout (path) whenever anything updates pathArray useEffect(() => { @@ -321,8 +352,8 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set }, [path]); // onChange for inners - const updateTarget = (newPath: string, currentPathSet: string[], depth: number) => { - setFirstRender(false) + const updateTarget = (newPath: string, currentPathSet: string[], depth: number, clearFirstRender = true, autoAddLevel = true) => { + if (clearFirstRender) setFirstRender(false) setShowAddButton(-1) // Also trim at the level just picked in case not actually the deepest @@ -346,9 +377,15 @@ export function PickFileOrFolder({ project_name=CONFIG.project_name, bucket, set if (!pathSeen(targetPath)) { fetchFolderContents(targetPath, (f) => { if (contentUse(f, allowFiles).length > 0) { - nextPathSet = [...nextPathSet, ''] + if (autoAddLevel) { + setPathArray([...nextPathSet, '']); + } else { + setPathArray(nextPathSet); + setShowAddButton(depth); + } + } else { + setPathArray(nextPathSet) } - setPathArray(nextPathSet) }) } else { if (contentUse(contentsSeen[pathInMetis(targetPath)] as folderSummary, allowFiles).length > 0) {