Add drag-and-drop upload component with chunked uploads and offline support
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { TagDetail, Package, PaginatedResponse } from '../types';
|
||||
import { listTags, uploadArtifact, getDownloadUrl, getPackage } from '../api';
|
||||
import { listTags, getDownloadUrl, getPackage } from '../api';
|
||||
import { Breadcrumb } from '../components/Breadcrumb';
|
||||
import { Badge } from '../components/Badge';
|
||||
import { SearchInput } from '../components/SearchInput';
|
||||
@@ -9,6 +9,7 @@ import { SortDropdown, SortOption } from '../components/SortDropdown';
|
||||
import { FilterChip, FilterChipGroup } from '../components/FilterChip';
|
||||
import { DataTable } from '../components/DataTable';
|
||||
import { Pagination } from '../components/Pagination';
|
||||
import { DragDropUpload, UploadResult } from '../components/DragDropUpload';
|
||||
import './Home.css';
|
||||
import './PackagePage.css';
|
||||
|
||||
@@ -61,10 +62,9 @@ function PackagePage() {
|
||||
const [tagsData, setTagsData] = useState<PaginatedResponse<TagDetail> | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadResult, setUploadResult] = useState<string | null>(null);
|
||||
const [tag, setTag] = useState('');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [uploadTag, setUploadTag] = useState('');
|
||||
const [uploadSuccess, setUploadSuccess] = useState<string | null>(null);
|
||||
const [artifactIdInput, setArtifactIdInput] = useState('');
|
||||
|
||||
// Get params from URL
|
||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
||||
@@ -122,30 +122,22 @@ function PackagePage() {
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [navigate, projectName]);
|
||||
|
||||
async function handleUpload(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const file = fileInputRef.current?.files?.[0];
|
||||
if (!file) {
|
||||
setError('Please select a file');
|
||||
return;
|
||||
}
|
||||
const handleUploadComplete = useCallback((results: UploadResult[]) => {
|
||||
const count = results.length;
|
||||
const message = count === 1
|
||||
? `Uploaded successfully! Artifact ID: ${results[0].artifact_id}`
|
||||
: `${count} files uploaded successfully!`;
|
||||
setUploadSuccess(message);
|
||||
setUploadTag('');
|
||||
loadData();
|
||||
|
||||
// Auto-dismiss success message after 5 seconds
|
||||
setTimeout(() => setUploadSuccess(null), 5000);
|
||||
}, [loadData]);
|
||||
|
||||
try {
|
||||
setUploading(true);
|
||||
setError(null);
|
||||
const result = await uploadArtifact(projectName!, packageName!, file, tag || undefined);
|
||||
setUploadResult(`Uploaded successfully! Artifact ID: ${result.artifact_id}`);
|
||||
setTag('');
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
loadData();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Upload failed');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}
|
||||
const handleUploadError = useCallback((errorMsg: string) => {
|
||||
setError(errorMsg);
|
||||
}, []);
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
updateParams({ search: value, page: '1' });
|
||||
@@ -292,29 +284,29 @@ function PackagePage() {
|
||||
</div>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
{uploadResult && <div className="success-message">{uploadResult}</div>}
|
||||
{uploadSuccess && <div className="success-message">{uploadSuccess}</div>}
|
||||
|
||||
<div className="upload-section card">
|
||||
<h3>Upload Artifact</h3>
|
||||
<form onSubmit={handleUpload} className="upload-form">
|
||||
<div className="upload-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="file">File</label>
|
||||
<input id="file" type="file" ref={fileInputRef} required />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="tag">Tag (optional)</label>
|
||||
<label htmlFor="upload-tag">Tag (optional)</label>
|
||||
<input
|
||||
id="tag"
|
||||
id="upload-tag"
|
||||
type="text"
|
||||
value={tag}
|
||||
onChange={(e) => setTag(e.target.value)}
|
||||
value={uploadTag}
|
||||
onChange={(e) => setUploadTag(e.target.value)}
|
||||
placeholder="v1.0.0, latest, stable..."
|
||||
/>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary" disabled={uploading}>
|
||||
{uploading ? 'Uploading...' : 'Upload'}
|
||||
</button>
|
||||
</form>
|
||||
<DragDropUpload
|
||||
projectName={projectName!}
|
||||
packageName={packageName!}
|
||||
tag={uploadTag || undefined}
|
||||
onUploadComplete={handleUploadComplete}
|
||||
onUploadError={handleUploadError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section-header">
|
||||
@@ -367,6 +359,34 @@ function PackagePage() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="download-by-id-section card">
|
||||
<h3>Download by Artifact ID</h3>
|
||||
<div className="download-by-id-form">
|
||||
<input
|
||||
type="text"
|
||||
value={artifactIdInput}
|
||||
onChange={(e) => setArtifactIdInput(e.target.value.toLowerCase().replace(/[^a-f0-9]/g, '').slice(0, 64))}
|
||||
placeholder="Enter SHA256 artifact ID (64 hex characters)"
|
||||
className="artifact-id-input"
|
||||
/>
|
||||
<a
|
||||
href={artifactIdInput.length === 64 ? getDownloadUrl(projectName!, packageName!, `artifact:${artifactIdInput}`) : '#'}
|
||||
className={`btn btn-primary ${artifactIdInput.length !== 64 ? 'btn-disabled' : ''}`}
|
||||
download
|
||||
onClick={(e) => {
|
||||
if (artifactIdInput.length !== 64) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
{artifactIdInput.length > 0 && artifactIdInput.length !== 64 && (
|
||||
<p className="validation-hint">Artifact ID must be exactly 64 hex characters ({artifactIdInput.length}/64)</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="usage-section card">
|
||||
<h3>Usage</h3>
|
||||
<p>Download artifacts using:</p>
|
||||
|
||||
Reference in New Issue
Block a user