Skip to content

Commit

Permalink
Finalized Front-End for Upload Component, with confirmation page (wor…
Browse files Browse the repository at this point in the history
…king on backend now) (#49)

* began developping basic upload feature

* more design features

* developments

* began developping basic upload feature

* more design features

* developments

* added error box + required data

* developed front end for upload

* renamed submit button

* added required packages

* edited required packages

* fix

* added old yarn lockfile

* test

* fix

* Added more resource types + Description Field

* switched navbar order

* changed to /Users/michaelzengel directory

* developped checkbox system for file selection

* docker action fixes to be compatible with node 20

* style to select all + /Users/michaelzengel instead of ~

* cleaned up file browser

* ignored hidden files and folders
  • Loading branch information
mrzengel authored Aug 6, 2024
1 parent a06c775 commit 1063eae
Show file tree
Hide file tree
Showing 13 changed files with 1,033 additions and 52 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ jobs:
uses: actions/checkout@v4
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- name: Log in to the Container registry
uses: docker/login-action@v2.1.0
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4.3.0
uses: docker/metadata-action@v5.5.0
with:
flavor: |
latest=true
Expand All @@ -49,7 +49,7 @@ jobs:
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v4.0.0
uses: docker/build-push-action@v6.0.0
with:
context: .
push: true
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@
"dependencies": {
"@babel/core": "^7.24.7",
"@babel/types": "^7.24.7",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@jupyterlab/application": "^4.2.2",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.3",
Expand Down
13 changes: 12 additions & 1 deletion src/API/API_functions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { requestAPI } from './handler';

export async function getEnvVariable(varName: string) {
try {
const data = await requestAPI('zenodo-jupyterlab/env?env_var=${encodeURIComponent(varName)}', {
const data = await requestAPI(`zenodo-jupyterlab/env?env_var=${encodeURIComponent(varName)}`, {
method: 'GET'
});
return(data);
Expand Down Expand Up @@ -73,6 +73,17 @@ export async function recordInformation(recordID: number) {
}
}

export async function getServerRootDir() {
try {
const response = await requestAPI('zenodo-jupyterlab/server-info', {
method: 'GET'
});
return response.root_dir;
} catch (error) {
console.error('Error fetching server root directory:', error);
}
}

/* export async function runPythonCode(code: string) {
try {
const data = await requestAPI('zenodo-jupyterlab/code', {
Expand Down
289 changes: 289 additions & 0 deletions src/components/FileBrowser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
import React, { useState, useEffect, useMemo } from 'react';
import { createUseStyles } from 'react-jss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFolder, faFile } from '@fortawesome/free-solid-svg-icons';
import { getServerRootDir } from '../API/API_functions';
import { FileEntry, OnSelectFile } from './type';

const useStyles = createUseStyles({
container: {
display: 'flex',
flexDirection: 'column',
maxWidth: '100%',
maxHeight: '400px', // Set the max height
overflowY: 'auto', // Allow scrolling if content exceeds max height
border: '1px solid #ddd',
borderRadius: '4px',
padding: '10px',
boxSizing: 'border-box',
},
item: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '5px',
cursor: 'pointer',
'&:hover': {
backgroundColor: '#f0f0f0',
},
},
fileDetails: {
display: 'flex',
alignItems: 'center',
overflow: 'hidden', // Ensure text overflow is handled properly
},
fileName: {
marginLeft: '10px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
fileInfo: {
flexShrink: 0,
textAlign: 'right',
marginLeft: '10px',
fontSize: '12px',
color: '#888',
},
folderIcon: {
width: '16px',
height: '16px',
marginRight: '5px',
},
fileIcon: {
width: '16px',
height: '16px',
marginRight: '5px',
},
breadcrumb: {
marginBottom: '10px',
display: 'flex',
alignItems: 'center',
maxWidth: '100%'
},
breadcrumbItem: {
marginRight: '5px',
cursor: 'pointer',
'&:hover': {
textDecoration: 'underline',
},
overflow: 'auto', // Change to auto for overflow handling
textOverflow: 'ellipsis',
},
checkbox: {
marginRight: '10px',
},
selectButton: {
padding: '10px 20px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
'&:hover': {
backgroundColor: '#0056b3',
},
},
buttonContainer: {
marginTop: '10px',
textAlign: 'center',
},
});

interface FileBrowserProps {
onSelectFile: OnSelectFile;
}

const FileBrowser: React.FC<FileBrowserProps> = ({ onSelectFile }) => {
const classes = useStyles();
const [entries, setEntries] = useState<FileEntry[]>([]);
const [currentPath, setCurrentPath] = useState<string>('');
const [rootPath, setRootPath] = useState<string>('');
const [error, setError] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [selectedEntries, setSelectedEntries] = useState<Set<string>>(new Set());
const [selectAll, setSelectAll] = useState<boolean>(false);

useEffect(() => {
const fetchRootPath = async () => {
setLoading(true);
setError('');
try {
const rootDir = await getServerRootDir();
if (rootDir) {
setRootPath(rootDir);
setCurrentPath(rootDir);
} else {
setError('Failed to retrieve the root path.');
}
} catch (error) {
setError('Error fetching root path.');
console.error('Error fetching root path:', error);
} finally {
setLoading(false);
}
};

fetchRootPath();
}, []);

useEffect(() => {
const loadFiles = async () => {
setLoading(true);
setError('');
try {
if (!currentPath) return;

const response = await fetch(`/zenodo-jupyterlab/files?path=${encodeURIComponent(currentPath)}`);
if (response.ok) {
const data = await response.json();
setEntries(data.entries || []);
} else {
setError('Failed to fetch file entries.');
}
} catch (error) {
setError('Error fetching file entries.');
console.error('Error fetching file entries:', error);
} finally {
setLoading(false);
}
};

loadFiles();
}, [currentPath]);

const handleClick = (entry: FileEntry) => {
if (entry.type === 'directory') {
setCurrentPath(entry.path);
}
};

const handleBreadcrumbClick = (path: string) => {
console.log('Breadcrumb clicked, path:', path);
if (path !== currentPath) {
//console.log('Updating currentPath from', currentPath, 'to', path);
setCurrentPath(path);
}
};

const breadcrumbs = useMemo(() => {
if (!rootPath || !currentPath) return null;

// Normalize the current path
const normalizedCurrentPath = currentPath.startsWith(rootPath)
? currentPath
: `${rootPath}${currentPath}`;
const relativePath = normalizedCurrentPath.slice(rootPath.length).replace(/^\/|\/$/g, '');
const parts = relativePath.split('/').filter(part => part);

// Array to hold the breadcrumb items
const breadcrumbItems = [];

// Generate breadcrumbs
for (let i = 0; i < parts.length; i++) {
// Build the path up to the current part
let path = rootPath;
for (let j = 0; j <= i; j++) {
path = `${path}/${parts[j]}`.replace(/\/+/g, '/');
}

// Add breadcrumb item
breadcrumbItems.push(
<React.Fragment key={path}>
<span
className={classes.breadcrumbItem}
onClick={() => handleBreadcrumbClick(path)}
>
{parts[i]}
</span>
{i < parts.length - 1 && ' / '}
</React.Fragment>
);
}

return breadcrumbItems;
}, [currentPath, rootPath, classes.breadcrumbItem]);

const handleSelectChange = (path: string, isChecked: boolean) => {
setSelectedEntries(prev => {
const newEntries = new Set(prev);
if (isChecked) {
newEntries.add(path);
} else {
newEntries.delete(path);
}
setSelectAll(newEntries.size === entries.length)
return newEntries;
});
};

const handleSelectFiles = () => {
selectedEntries.forEach(path => onSelectFile(rootPath + '/'+ path));
setSelectedEntries(new Set()); // Clear selection
};

return (
<div>
<div className={classes.container}>
<div className={classes.breadcrumb}>
{rootPath && (
<>
<span
className={classes.breadcrumbItem}
onClick={() => setCurrentPath(rootPath)}
>
$HOME
</span>
{currentPath !== rootPath && ' / '}
{breadcrumbs}
</>
)}
</div>
<div>
<input
type="checkbox"
checked={selectAll}
onChange={() => {
const newSelectAll = !selectAll;
setSelectAll(newSelectAll);
setSelectedEntries(newSelectAll ? new Set(entries.map(entry => entry.path)) : new Set());
}}
/>
<label style={{'marginLeft': '10px'}}>Select All</label>
</div>
{loading && <p>Loading...</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
{entries.length === 0 && !loading && !error && <p>No items to display.</p>}
{entries.map((entry) => (
<div>
<div key={entry.path} className={classes.item}>
<div className={classes.fileDetails}>
<input
type="checkbox"
checked={selectAll || selectedEntries.has(entry.path)}
onChange={(e) => handleSelectChange(entry.path, e.target.checked)}
className={classes.checkbox}
/>
<FontAwesomeIcon
icon={entry.type === 'directory' ? faFolder : faFile}
className={entry.type === 'directory' ? classes.folderIcon : classes.fileIcon}
onClick={() => handleClick(entry)}
/>
<span className={classes.fileName} onClick={() => handleClick(entry)}>{entry.name}</span>
</div>
<span className={classes.fileInfo} onClick={() => handleClick(entry)}>
{entry.modified && new Date(entry.modified).toLocaleDateString()}<br />
{entry.size && `${entry.size} B`}
</span>
</div>
</div>
))}
</div>
<div className={classes.buttonContainer}>
<button type="button" onClick={handleSelectFiles} className={classes.selectButton}>Select</button>
</div>
</div>
);
};

export default FileBrowser;
Loading

0 comments on commit 1063eae

Please sign in to comment.