Add conditional UI based on user access level
ProjectPage: - Display user's access level badge (Owner/Admin/Write/Read) - Hide "New Package" button for read-only users - Show "Read-only access" text for authenticated read-only users PackagePage: - Hide upload form for read-only users - Show message explaining read-only access - Fetch access level along with package data
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { TagDetail, Package, PaginatedResponse } from '../types';
|
||||
import { listTags, getDownloadUrl, getPackage } from '../api';
|
||||
import { TagDetail, Package, PaginatedResponse, AccessLevel } from '../types';
|
||||
import { listTags, getDownloadUrl, getPackage, getMyProjectAccess } from '../api';
|
||||
import { Breadcrumb } from '../components/Breadcrumb';
|
||||
import { Badge } from '../components/Badge';
|
||||
import { SearchInput } from '../components/SearchInput';
|
||||
@@ -10,6 +10,7 @@ import { FilterChip, FilterChipGroup } from '../components/FilterChip';
|
||||
import { DataTable } from '../components/DataTable';
|
||||
import { Pagination } from '../components/Pagination';
|
||||
import { DragDropUpload, UploadResult } from '../components/DragDropUpload';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import './Home.css';
|
||||
import './PackagePage.css';
|
||||
|
||||
@@ -57,6 +58,7 @@ function PackagePage() {
|
||||
const { projectName, packageName } = useParams<{ projectName: string; packageName: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [pkg, setPkg] = useState<Package | null>(null);
|
||||
const [tagsData, setTagsData] = useState<PaginatedResponse<TagDetail> | null>(null);
|
||||
@@ -65,6 +67,10 @@ function PackagePage() {
|
||||
const [uploadTag, setUploadTag] = useState('');
|
||||
const [uploadSuccess, setUploadSuccess] = useState<string | null>(null);
|
||||
const [artifactIdInput, setArtifactIdInput] = useState('');
|
||||
const [accessLevel, setAccessLevel] = useState<AccessLevel | null>(null);
|
||||
|
||||
// Derived permissions
|
||||
const canWrite = accessLevel === 'write' || accessLevel === 'admin';
|
||||
|
||||
// Get params from URL
|
||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
||||
@@ -92,12 +98,14 @@ function PackagePage() {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const [pkgData, tagsResult] = await Promise.all([
|
||||
const [pkgData, tagsResult, accessResult] = await Promise.all([
|
||||
getPackage(projectName, packageName),
|
||||
listTags(projectName, packageName, { page, search, sort, order }),
|
||||
getMyProjectAccess(projectName),
|
||||
]);
|
||||
setPkg(pkgData);
|
||||
setTagsData(tagsResult);
|
||||
setAccessLevel(accessResult.access_level);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
@@ -286,28 +294,35 @@ function PackagePage() {
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
{uploadSuccess && <div className="success-message">{uploadSuccess}</div>}
|
||||
|
||||
<div className="upload-section card">
|
||||
<h3>Upload Artifact</h3>
|
||||
<div className="upload-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="upload-tag">Tag (optional)</label>
|
||||
<input
|
||||
id="upload-tag"
|
||||
type="text"
|
||||
value={uploadTag}
|
||||
onChange={(e) => setUploadTag(e.target.value)}
|
||||
placeholder="v1.0.0, latest, stable..."
|
||||
{canWrite ? (
|
||||
<div className="upload-section card">
|
||||
<h3>Upload Artifact</h3>
|
||||
<div className="upload-form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="upload-tag">Tag (optional)</label>
|
||||
<input
|
||||
id="upload-tag"
|
||||
type="text"
|
||||
value={uploadTag}
|
||||
onChange={(e) => setUploadTag(e.target.value)}
|
||||
placeholder="v1.0.0, latest, stable..."
|
||||
/>
|
||||
</div>
|
||||
<DragDropUpload
|
||||
projectName={projectName!}
|
||||
packageName={packageName!}
|
||||
tag={uploadTag || undefined}
|
||||
onUploadComplete={handleUploadComplete}
|
||||
onUploadError={handleUploadError}
|
||||
/>
|
||||
</div>
|
||||
<DragDropUpload
|
||||
projectName={projectName!}
|
||||
packageName={packageName!}
|
||||
tag={uploadTag || undefined}
|
||||
onUploadComplete={handleUploadComplete}
|
||||
onUploadError={handleUploadError}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : user ? (
|
||||
<div className="upload-section card">
|
||||
<h3>Upload Artifact</h3>
|
||||
<p className="text-muted">You have read-only access to this project and cannot upload artifacts.</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="section-header">
|
||||
<h2>Tags / Versions</h2>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, Link, useSearchParams, useNavigate } from 'react-router-dom';
|
||||
import { Project, Package, PaginatedResponse } from '../types';
|
||||
import { getProject, listPackages, createPackage } from '../api';
|
||||
import { Project, Package, PaginatedResponse, AccessLevel } from '../types';
|
||||
import { getProject, listPackages, createPackage, getMyProjectAccess } from '../api';
|
||||
import { Breadcrumb } from '../components/Breadcrumb';
|
||||
import { Badge } from '../components/Badge';
|
||||
import { SearchInput } from '../components/SearchInput';
|
||||
import { SortDropdown, SortOption } from '../components/SortDropdown';
|
||||
import { FilterChip, FilterChipGroup } from '../components/FilterChip';
|
||||
import { Pagination } from '../components/Pagination';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import './Home.css';
|
||||
|
||||
const SORT_OPTIONS: SortOption[] = [
|
||||
@@ -30,6 +31,7 @@ function ProjectPage() {
|
||||
const { projectName } = useParams<{ projectName: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [project, setProject] = useState<Project | null>(null);
|
||||
const [packagesData, setPackagesData] = useState<PaginatedResponse<Package> | null>(null);
|
||||
@@ -38,6 +40,11 @@ function ProjectPage() {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [newPackage, setNewPackage] = useState({ name: '', description: '', format: 'generic', platform: 'any' });
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [accessLevel, setAccessLevel] = useState<AccessLevel | null>(null);
|
||||
const [isOwner, setIsOwner] = useState(false);
|
||||
|
||||
// Derived permissions
|
||||
const canWrite = accessLevel === 'write' || accessLevel === 'admin';
|
||||
|
||||
// Get params from URL
|
||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
||||
@@ -66,12 +73,15 @@ function ProjectPage() {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const [projectData, packagesResult] = await Promise.all([
|
||||
const [projectData, packagesResult, accessResult] = await Promise.all([
|
||||
getProject(projectName),
|
||||
listPackages(projectName, { page, search, sort, order, format: format || undefined }),
|
||||
getMyProjectAccess(projectName),
|
||||
]);
|
||||
setProject(projectData);
|
||||
setPackagesData(packagesResult);
|
||||
setAccessLevel(accessResult.access_level);
|
||||
setIsOwner(accessResult.is_owner);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load data');
|
||||
@@ -159,6 +169,11 @@ function ProjectPage() {
|
||||
<Badge variant={project.is_public ? 'public' : 'private'}>
|
||||
{project.is_public ? 'Public' : 'Private'}
|
||||
</Badge>
|
||||
{accessLevel && (
|
||||
<Badge variant={accessLevel === 'admin' ? 'success' : accessLevel === 'write' ? 'info' : 'default'}>
|
||||
{isOwner ? 'Owner' : accessLevel.charAt(0).toUpperCase() + accessLevel.slice(1)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{project.description && <p className="description">{project.description}</p>}
|
||||
<div className="page-header__meta">
|
||||
@@ -169,14 +184,20 @@ function ProjectPage() {
|
||||
<span className="meta-item">by {project.created_by}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={() => setShowForm(!showForm)}>
|
||||
{showForm ? 'Cancel' : '+ New Package'}
|
||||
</button>
|
||||
{canWrite ? (
|
||||
<button className="btn btn-primary" onClick={() => setShowForm(!showForm)}>
|
||||
{showForm ? 'Cancel' : '+ New Package'}
|
||||
</button>
|
||||
) : user ? (
|
||||
<span className="text-muted" title="You have read-only access to this project">
|
||||
Read-only access
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
{showForm && (
|
||||
{showForm && canWrite && (
|
||||
<form className="form card" onSubmit={handleCreatePackage}>
|
||||
<h3>Create New Package</h3>
|
||||
<div className="form-row">
|
||||
|
||||
Reference in New Issue
Block a user