From 2334faa59461fde7ba9c59abb0b87831866bf301 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Daniel=20Kuty=C5=82a?= Date: Mon, 30 Aug 2021 17:02:33 +0200 Subject: [PATCH] 17585: Added search fields, optimized requests MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła --- .../collection-panel-files.tsx | 274 +++++++++++++----- 1 file changed, 203 insertions(+), 71 deletions(-) diff --git a/src/components/collection-panel-files/collection-panel-files.tsx b/src/components/collection-panel-files/collection-panel-files.tsx index a6bce0f2..18230f6f 100644 --- a/src/components/collection-panel-files/collection-panel-files.tsx +++ b/src/components/collection-panel-files/collection-panel-files.tsx @@ -5,8 +5,11 @@ import React from 'react'; import classNames from 'classnames'; import { connect } from 'react-redux'; +import { FixedSizeList } from "react-window"; +import AutoSizer from "react-virtualized-auto-sizer"; import { CustomizeTableIcon } from 'components/icon/icon'; -import { ListItemIcon, StyleRulesCallback, Theme, WithStyles, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core'; +import { SearchInput } from 'components/search-input/search-input'; +import { ListItemIcon, StyleRulesCallback, Theme, WithStyles, withStyles, Tooltip, IconButton, Checkbox, CircularProgress } from '@material-ui/core'; import { FileTreeData } from '../file-tree/file-tree-data'; import { TreeItem, TreeItemStatus } from '../tree/tree'; import { RootState } from 'store/store'; @@ -15,6 +18,7 @@ import { AuthState } from 'store/auth/auth-reducer'; import { extractFilesData } from 'services/collection-service/collection-service-files-response'; import { DefaultIcon, DirectoryIcon, FileIcon } from 'components/icon/icon'; import { setCollectionFiles } from 'store/collection-panel/collection-panel-files/collection-panel-files-actions'; +import { sortBy } from 'lodash'; export interface CollectionPanelFilesProps { items: any; @@ -35,23 +39,50 @@ export interface CollectionPanelFilesProps { collectionPanel: any; } -type CssRules = "wrapper" | "row" | "leftPanel" | "rightPanel" | "pathPanel" | "pathPanelItem" | "rowName" | "listItemIcon" | "rowActive" | "pathPanelMenu" | "rowSelection"; +type CssRules = "loader" | "wrapper" | "dataWrapper" | "row" | "rowEmpty" | "leftPanel" | "rightPanel" | "pathPanel" | "pathPanelItem" | "rowName" | "listItemIcon" | "rowActive" | "pathPanelMenu" | "rowSelection" | "leftPanelHidden" | "leftPanelVisible" | "searchWrapper" | "searchWrapperHidden"; const styles: StyleRulesCallback = (theme: Theme) => ({ wrapper: { display: 'flex', + minHeight: '600px', + marginBottom: '1rem' + }, + dataWrapper: { + minHeight: '500px' }, row: { display: 'flex', - margin: '0.5rem', + marginTop: '0.5rem', + marginBottom: '0.5rem', cursor: 'pointer', "&:hover": { backgroundColor: 'rgba(0, 0, 0, 0.08)', } }, + rowEmpty: { + top: '40%', + width: '100%', + textAlign: 'center', + position: 'absolute' + }, + loader: { + top: '50%', + left: '50%', + marginTop: '-15px', + marginLeft: '-15px', + position: 'absolute' + }, rowName: { - paddingTop: '6px', - paddingBottom: '6px', + display: 'inline-flex', + flexDirection: 'column', + justifyContent: 'center' + }, + searchWrapper: { + width: '100%', + marginBottom: '1rem' + }, + searchWrapperHidden: { + width: '0px' }, rowSelection: { padding: '0px', @@ -60,7 +91,9 @@ const styles: StyleRulesCallback = (theme: Theme) => ({ color: `${theme.palette.primary.main} !important`, }, listItemIcon: { - marginTop: '2px', + display: 'inline-flex', + flexDirection: 'column', + justifyContent: 'center' }, pathPanelMenu: { float: 'right', @@ -72,27 +105,52 @@ const styles: StyleRulesCallback = (theme: Theme) => ({ boxShadow: '0px 1px 3px 0px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 2px 1px -1px rgb(0 0 0 / 12%)', }, leftPanel: { - flex: '30%', + flex: 0, padding: '1rem', marginRight: '1rem', + whiteSpace: 'nowrap', + position: 'relative', boxShadow: '0px 1px 3px 0px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 2px 1px -1px rgb(0 0 0 / 12%)', }, + leftPanelVisible: { + opacity: 1, + flex: '30%', + animation: `animateVisible 1000ms ${theme.transitions.easing.easeOut}` + }, + leftPanelHidden: { + opacity: 0, + flex: 'initial', + padding: '0', + marginRight: '0', + }, + "@keyframes animateVisible": { + "0%": { + opacity: 0, + flex: 'initial', + }, + "100%": { + opacity: 1, + flex: '30%', + } + }, rightPanel: { flex: '70%', padding: '1rem', + position: 'relative', boxShadow: '0px 1px 3px 0px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 2px 1px -1px rgb(0 0 0 / 12%)', }, pathPanelItem: { cursor: 'pointer', } - }); -export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState) => ({ +const pathPromise = {}; + +export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState) => ({ auth: state.auth, collectionPanel: state.collectionPanel, collectionPanelFiles: state.collectionPanelFiles, - }))((props: CollectionPanelFilesProps & WithStyles & { auth: AuthState }) => { +}))((props: CollectionPanelFilesProps & WithStyles & { auth: AuthState }) => { const { classes, onItemMenuOpen, isWritable, dispatch, collectionPanelFiles, collectionPanel } = props; const { apiToken, config } = props.auth; @@ -112,10 +170,16 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState const [path, setPath]: any = React.useState([]); const [pathData, setPathData]: any = React.useState({}); const [isLoading, setIsLoading] = React.useState(false); + const [rightClickUsed, setRightClickUsed] = React.useState(false); + const [leftSearch, setLeftSearch] = React.useState(''); + const [rightSearch, setRightSearch] = React.useState(''); const leftKey = (path.length > 1 ? path.slice(0, path.length - 1) : path).join('/'); const rightKey = path.join('/'); + const leftData = (pathData[leftKey] || []).filter(({ type }) => type === 'directory'); + const rightData = pathData[rightKey]; + React.useEffect(() => { if (props.currentItemUuid) { setPathData({}); @@ -123,38 +187,54 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState } }, [props.currentItemUuid]); - React.useEffect(() => { - if (rightKey && !pathData[rightKey] && !isLoading) { + const fetchData = (rightKey, ignoreCache = false) => { + const dataExists = !!pathData[rightKey]; + const runningRequest = pathPromise[rightKey]; + + if ((!dataExists || ignoreCache) && !runningRequest) { + setIsLoading(true); + webdavClient.propfind(`c=${rightKey}`, webDAVRequestConfig) .then((request) => { if (request.responseXML != null) { const result: any = extractFilesData(request.responseXML); - const sortedResult = result.sort((n1: any, n2: any) => n1.name > n2.name ? 1 : -1); + const sortedResult = sortBy(result, (n) => n.name).sort((n1, n2) => { + if (n1.type === 'directory' && n2.type !== 'directory') { + return -1; + } + if (n1.type !== 'directory' && n2.type === 'directory') { + return 1; + } + return 0; + }); const newPathData = { ...pathData, [rightKey]: sortedResult }; setPathData(newPathData); - setIsLoading(false); } + }) + .finally(() => { + setIsLoading(false); + delete pathPromise[rightKey]; }); + + pathPromise[rightKey] = true; } else { - setTimeout(() => setIsLoading(false), 100); + setTimeout(() => setIsLoading(false), 0); } - }, [path, pathData, webdavClient, webDAVRequestConfig, rightKey, isLoading, collectionPanelFiles]); + }; - const leftData = pathData[leftKey]; - const rightData = pathData[rightKey]; + React.useEffect(() => { + if (rightKey) { + fetchData(rightKey); + } + }, [rightKey]); React.useEffect(() => { - webdavClient.propfind(`c=${rightKey}`, webDAVRequestConfig) - .then((request) => { - if (request.responseXML != null) { - const result: any = extractFilesData(request.responseXML); - const sortedResult = result.sort((n1: any, n2: any) => n1.name > n2.name ? 1 : -1); - const newPathData = { ...pathData, [rightKey]: sortedResult }; - setPathData(newPathData); - setIsLoading(false); - } - }); - }, [collectionPanel.item]); + const hash = (collectionPanel.item || {}).portableDataHash; + + if (hash && rightClickUsed) { + fetchData(rightKey, true); + } + }, [(collectionPanel.item || {}).portableDataHash]); React.useEffect(() => { if (rightData) { @@ -166,6 +246,10 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState (event) => { event.preventDefault(); + if (!rightClickUsed) { + setRightClickUsed(true); + } + let elem = event.target; while (elem && elem.dataset && !elem.dataset.item) { @@ -217,8 +301,6 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState if (elem && elem.dataset && !isCheckbox) { const { parentPath, subfolderPath, breadcrumbPath, type } = elem.dataset; - setIsLoading(true); - if (breadcrumbPath) { const index = path.indexOf(breadcrumbPath); setPath([...path.slice(0, index + 1)]); @@ -270,9 +352,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState const getActiveClass = React.useCallback( (name) => { - const index = path.indexOf(name); - - return index === (path.length - 1) ? classes.rowActive : null + return path[path.length - 1] === name ? classes.rowActive : null; }, [path, classes] ); @@ -288,16 +368,17 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
{ - path.map((p: string, index: number) => - {index === 0 ? 'Home' : p} /  - ) + path + .map((p: string, index: number) => + {index === 0 ? 'Home' : p} /  + ) } - + onOptionsMenuOpen(ev, isWritable)}> @@ -306,36 +387,87 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
-
- { - leftData && !!leftData.length ? - leftData.filter(({ type }) => type === 'directory').map(({ name, id, type }: any) =>
{getItemIcon(type, getActiveClass(name))}
{name}
-
) :
Loading...
- } +
1 ? classes.leftPanelVisible : classes.leftPanelHidden)}> +
1 ? classes.searchWrapper : classes.searchWrapperHidden}> + +
+
+ { + leftData ? + + {({ height, width }) => { + const filtered = leftData.filter(({ name }) => name.indexOf(leftSearch) > -1); + + return !!filtered.length ? + { + ({ index, style }) => { + const { id, type, name } = filtered[index]; + + return
{getItemIcon(type, getActiveClass(name))}
{name}
+
; + } + } +
:
No directories available
+ }} +
:
+ } + +
- { - rightData && !isLoading ? - rightData.map(({ name, id, type }: any) =>
-   - {getItemIcon(type, null)}
- {name} -
-
) :
Loading...
- } +
+ +
+
+ { + rightData && !isLoading ? + + {({ height, width }) => { + const filtered = rightData.filter(({ name }) => name.indexOf(rightSearch) > -1); + + return !!filtered.length ? + { + ({ index, style }) => { + const { id, type, name } = filtered[index]; + + return
+   + {getItemIcon(type, null)}
+ {name} +
+
+ } + } +
:
No data available
+ }} +
:
+ } +
-- 2.30.2