|
|
|
@@ -1,7 +1,7 @@
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
import { useParams, useSearchParams, useNavigate, useLocation, Link } from 'react-router-dom';
|
|
|
|
import { useParams, useSearchParams, useNavigate, useLocation, Link } from 'react-router-dom';
|
|
|
|
import { TagDetail, Package, PaginatedResponse, AccessLevel, Dependency, DependentInfo } from '../types';
|
|
|
|
import { PackageArtifact, Package, PaginatedResponse, AccessLevel, Dependency, DependentInfo } from '../types';
|
|
|
|
import { listTags, getDownloadUrl, getPackage, getMyProjectAccess, createTag, getArtifactDependencies, getReverseDependencies, getEnsureFile, UnauthorizedError, ForbiddenError } from '../api';
|
|
|
|
import { listPackageArtifacts, getDownloadUrl, getPackage, getMyProjectAccess, createTag, getArtifactDependencies, getReverseDependencies, getEnsureFile, UnauthorizedError, ForbiddenError } from '../api';
|
|
|
|
import { Breadcrumb } from '../components/Breadcrumb';
|
|
|
|
import { Breadcrumb } from '../components/Breadcrumb';
|
|
|
|
import { Badge } from '../components/Badge';
|
|
|
|
import { Badge } from '../components/Badge';
|
|
|
|
import { SearchInput } from '../components/SearchInput';
|
|
|
|
import { SearchInput } from '../components/SearchInput';
|
|
|
|
@@ -57,7 +57,7 @@ function PackagePage() {
|
|
|
|
const { user } = useAuth();
|
|
|
|
const { user } = useAuth();
|
|
|
|
|
|
|
|
|
|
|
|
const [pkg, setPkg] = useState<Package | null>(null);
|
|
|
|
const [pkg, setPkg] = useState<Package | null>(null);
|
|
|
|
const [tagsData, setTagsData] = useState<PaginatedResponse<TagDetail> | null>(null);
|
|
|
|
const [artifactsData, setArtifactsData] = useState<PaginatedResponse<PackageArtifact> | null>(null);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const [accessDenied, setAccessDenied] = useState(false);
|
|
|
|
const [accessDenied, setAccessDenied] = useState(false);
|
|
|
|
@@ -75,7 +75,7 @@ function PackagePage() {
|
|
|
|
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
|
|
|
|
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
// Dependencies state
|
|
|
|
// Dependencies state
|
|
|
|
const [selectedTag, setSelectedTag] = useState<TagDetail | null>(null);
|
|
|
|
const [selectedArtifact, setSelectedArtifact] = useState<PackageArtifact | null>(null);
|
|
|
|
const [dependencies, setDependencies] = useState<Dependency[]>([]);
|
|
|
|
const [dependencies, setDependencies] = useState<Dependency[]>([]);
|
|
|
|
const [depsLoading, setDepsLoading] = useState(false);
|
|
|
|
const [depsLoading, setDepsLoading] = useState(false);
|
|
|
|
const [depsError, setDepsError] = useState<string | null>(null);
|
|
|
|
const [depsError, setDepsError] = useState<string | null>(null);
|
|
|
|
@@ -138,13 +138,13 @@ function PackagePage() {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
setLoading(true);
|
|
|
|
setLoading(true);
|
|
|
|
setAccessDenied(false);
|
|
|
|
setAccessDenied(false);
|
|
|
|
const [pkgData, tagsResult, accessResult] = await Promise.all([
|
|
|
|
const [pkgData, artifactsResult, accessResult] = await Promise.all([
|
|
|
|
getPackage(projectName, packageName),
|
|
|
|
getPackage(projectName, packageName),
|
|
|
|
listTags(projectName, packageName, { page, search, sort, order }),
|
|
|
|
listPackageArtifacts(projectName, packageName, { page, search, sort, order }),
|
|
|
|
getMyProjectAccess(projectName),
|
|
|
|
getMyProjectAccess(projectName),
|
|
|
|
]);
|
|
|
|
]);
|
|
|
|
setPkg(pkgData);
|
|
|
|
setPkg(pkgData);
|
|
|
|
setTagsData(tagsResult);
|
|
|
|
setArtifactsData(artifactsResult);
|
|
|
|
setAccessLevel(accessResult.access_level);
|
|
|
|
setAccessLevel(accessResult.access_level);
|
|
|
|
setError(null);
|
|
|
|
setError(null);
|
|
|
|
} catch (err) {
|
|
|
|
} catch (err) {
|
|
|
|
@@ -168,25 +168,15 @@ function PackagePage() {
|
|
|
|
loadData();
|
|
|
|
loadData();
|
|
|
|
}, [loadData]);
|
|
|
|
}, [loadData]);
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-select tag when tags are loaded (prefer version from URL, then first tag)
|
|
|
|
// Auto-select artifact when artifacts are loaded (prefer first artifact)
|
|
|
|
// Re-run when package changes to pick up new tags
|
|
|
|
// Re-run when package changes to pick up new artifacts
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
if (tagsData?.items && tagsData.items.length > 0) {
|
|
|
|
if (artifactsData?.items && artifactsData.items.length > 0) {
|
|
|
|
const versionParam = searchParams.get('version');
|
|
|
|
// Fall back to first artifact
|
|
|
|
if (versionParam) {
|
|
|
|
setSelectedArtifact(artifactsData.items[0]);
|
|
|
|
// Find tag matching the version parameter
|
|
|
|
|
|
|
|
const matchingTag = tagsData.items.find(t => t.version === versionParam);
|
|
|
|
|
|
|
|
if (matchingTag) {
|
|
|
|
|
|
|
|
setSelectedTag(matchingTag);
|
|
|
|
|
|
|
|
setDependencies([]);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fall back to first tag
|
|
|
|
|
|
|
|
setSelectedTag(tagsData.items[0]);
|
|
|
|
|
|
|
|
setDependencies([]);
|
|
|
|
setDependencies([]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [tagsData, searchParams, projectName, packageName]);
|
|
|
|
}, [artifactsData, projectName, packageName]);
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch dependencies when selected tag changes
|
|
|
|
// Fetch dependencies when selected tag changes
|
|
|
|
const fetchDependencies = useCallback(async (artifactId: string) => {
|
|
|
|
const fetchDependencies = useCallback(async (artifactId: string) => {
|
|
|
|
@@ -204,10 +194,10 @@ function PackagePage() {
|
|
|
|
}, []);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
if (selectedTag) {
|
|
|
|
if (selectedArtifact) {
|
|
|
|
fetchDependencies(selectedTag.artifact_id);
|
|
|
|
fetchDependencies(selectedArtifact.id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [selectedTag, fetchDependencies]);
|
|
|
|
}, [selectedArtifact, fetchDependencies]);
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch reverse dependencies
|
|
|
|
// Fetch reverse dependencies
|
|
|
|
const fetchReverseDeps = useCallback(async (pageNum: number = 1) => {
|
|
|
|
const fetchReverseDeps = useCallback(async (pageNum: number = 1) => {
|
|
|
|
@@ -254,11 +244,11 @@ function PackagePage() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}, [projectName, packageName]);
|
|
|
|
}, [projectName, packageName]);
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch ensure file for selected tag
|
|
|
|
// Fetch ensure file for selected artifact (if it has tags)
|
|
|
|
const fetchEnsureFile = useCallback(async () => {
|
|
|
|
const fetchEnsureFile = useCallback(async () => {
|
|
|
|
if (!selectedTag) return;
|
|
|
|
if (!selectedArtifact || selectedArtifact.tags.length === 0) return;
|
|
|
|
fetchEnsureFileForTag(selectedTag.name);
|
|
|
|
fetchEnsureFileForTag(selectedArtifact.tags[0]);
|
|
|
|
}, [selectedTag, fetchEnsureFileForTag]);
|
|
|
|
}, [selectedArtifact, fetchEnsureFileForTag]);
|
|
|
|
|
|
|
|
|
|
|
|
// Keyboard navigation - go back with backspace
|
|
|
|
// Keyboard navigation - go back with backspace
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
@@ -331,25 +321,35 @@ function PackagePage() {
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const hasActiveFilters = search !== '';
|
|
|
|
const hasActiveFilters = search !== '';
|
|
|
|
const tags = tagsData?.items || [];
|
|
|
|
const artifacts = artifactsData?.items || [];
|
|
|
|
const pagination = tagsData?.pagination;
|
|
|
|
const pagination = artifactsData?.pagination;
|
|
|
|
|
|
|
|
|
|
|
|
const handleTagSelect = (tag: TagDetail) => {
|
|
|
|
const handleArtifactSelect = (artifact: PackageArtifact) => {
|
|
|
|
setSelectedTag(tag);
|
|
|
|
setSelectedArtifact(artifact);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleMenuOpen = (e: React.MouseEvent, tagId: string) => {
|
|
|
|
const handleMenuOpen = (e: React.MouseEvent, artifactId: string) => {
|
|
|
|
e.stopPropagation();
|
|
|
|
e.stopPropagation();
|
|
|
|
if (openMenuId === tagId) {
|
|
|
|
if (openMenuId === artifactId) {
|
|
|
|
setOpenMenuId(null);
|
|
|
|
setOpenMenuId(null);
|
|
|
|
setMenuPosition(null);
|
|
|
|
setMenuPosition(null);
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
|
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
|
|
setMenuPosition({ top: rect.bottom + 4, left: rect.right - 180 });
|
|
|
|
setMenuPosition({ top: rect.bottom + 4, left: rect.right - 180 });
|
|
|
|
setOpenMenuId(tagId);
|
|
|
|
setOpenMenuId(artifactId);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Helper to get version from artifact metadata
|
|
|
|
|
|
|
|
const getArtifactVersion = (a: PackageArtifact): string | null => {
|
|
|
|
|
|
|
|
return (a.format_metadata?.version as string) || null;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Helper to get download ref - prefer first tag, fallback to artifact ID
|
|
|
|
|
|
|
|
const getDownloadRef = (a: PackageArtifact): string => {
|
|
|
|
|
|
|
|
return a.tags.length > 0 ? a.tags[0] : `artifact:${a.id}`;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// System projects show Version first, regular projects show Tag first
|
|
|
|
// System projects show Version first, regular projects show Tag first
|
|
|
|
const columns = isSystemProject
|
|
|
|
const columns = isSystemProject
|
|
|
|
? [
|
|
|
|
? [
|
|
|
|
@@ -358,44 +358,44 @@ function PackagePage() {
|
|
|
|
key: 'version',
|
|
|
|
key: 'version',
|
|
|
|
header: 'Version',
|
|
|
|
header: 'Version',
|
|
|
|
sortable: true,
|
|
|
|
sortable: true,
|
|
|
|
render: (t: TagDetail) => (
|
|
|
|
render: (a: PackageArtifact) => (
|
|
|
|
<strong
|
|
|
|
<strong
|
|
|
|
className={`tag-name-link ${selectedTag?.id === t.id ? 'selected' : ''}`}
|
|
|
|
className={`tag-name-link ${selectedArtifact?.id === a.id ? 'selected' : ''}`}
|
|
|
|
onClick={() => handleTagSelect(t)}
|
|
|
|
onClick={() => handleArtifactSelect(a)}
|
|
|
|
style={{ cursor: 'pointer' }}
|
|
|
|
style={{ cursor: 'pointer' }}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<span className="version-badge">{t.version || t.name}</span>
|
|
|
|
<span className="version-badge">{getArtifactVersion(a) || a.tags[0] || a.id.slice(0, 12)}</span>
|
|
|
|
</strong>
|
|
|
|
</strong>
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
key: 'artifact_original_name',
|
|
|
|
key: 'original_name',
|
|
|
|
header: 'Filename',
|
|
|
|
header: 'Filename',
|
|
|
|
className: 'cell-truncate',
|
|
|
|
className: 'cell-truncate',
|
|
|
|
render: (t: TagDetail) => (
|
|
|
|
render: (a: PackageArtifact) => (
|
|
|
|
<span title={t.artifact_original_name || t.name}>{t.artifact_original_name || t.name}</span>
|
|
|
|
<span title={a.original_name || a.id}>{a.original_name || a.id.slice(0, 12)}</span>
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
key: 'artifact_size',
|
|
|
|
key: 'size',
|
|
|
|
header: 'Size',
|
|
|
|
header: 'Size',
|
|
|
|
render: (t: TagDetail) => <span>{formatBytes(t.artifact_size)}</span>,
|
|
|
|
render: (a: PackageArtifact) => <span>{formatBytes(a.size)}</span>,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
key: 'created_at',
|
|
|
|
key: 'created_at',
|
|
|
|
header: 'Cached',
|
|
|
|
header: 'Cached',
|
|
|
|
sortable: true,
|
|
|
|
sortable: true,
|
|
|
|
render: (t: TagDetail) => (
|
|
|
|
render: (a: PackageArtifact) => (
|
|
|
|
<span>{new Date(t.created_at).toLocaleDateString()}</span>
|
|
|
|
<span>{new Date(a.created_at).toLocaleDateString()}</span>
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
key: 'actions',
|
|
|
|
key: 'actions',
|
|
|
|
header: '',
|
|
|
|
header: '',
|
|
|
|
render: (t: TagDetail) => (
|
|
|
|
render: (a: PackageArtifact) => (
|
|
|
|
<div className="action-buttons">
|
|
|
|
<div className="action-buttons">
|
|
|
|
<a
|
|
|
|
<a
|
|
|
|
href={getDownloadUrl(projectName!, packageName!, t.name)}
|
|
|
|
href={getDownloadUrl(projectName!, packageName!, getDownloadRef(a))}
|
|
|
|
className="btn btn-icon"
|
|
|
|
className="btn btn-icon"
|
|
|
|
download
|
|
|
|
download
|
|
|
|
title="Download"
|
|
|
|
title="Download"
|
|
|
|
@@ -408,7 +408,7 @@ function PackagePage() {
|
|
|
|
</a>
|
|
|
|
</a>
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
className="btn btn-icon"
|
|
|
|
className="btn btn-icon"
|
|
|
|
onClick={(e) => handleMenuOpen(e, t.id)}
|
|
|
|
onClick={(e) => handleMenuOpen(e, a.id)}
|
|
|
|
title="More actions"
|
|
|
|
title="More actions"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
|
|
@@ -422,56 +422,56 @@ function PackagePage() {
|
|
|
|
},
|
|
|
|
},
|
|
|
|
]
|
|
|
|
]
|
|
|
|
: [
|
|
|
|
: [
|
|
|
|
// Regular project columns: Tag, Version, Filename
|
|
|
|
// Regular project columns: Tag, Version, Filename, Size, Created
|
|
|
|
{
|
|
|
|
{
|
|
|
|
key: 'name',
|
|
|
|
key: 'tags',
|
|
|
|
header: 'Tag',
|
|
|
|
header: 'Tag',
|
|
|
|
sortable: true,
|
|
|
|
sortable: true,
|
|
|
|
render: (t: TagDetail) => (
|
|
|
|
render: (a: PackageArtifact) => (
|
|
|
|
<strong
|
|
|
|
<strong
|
|
|
|
className={`tag-name-link ${selectedTag?.id === t.id ? 'selected' : ''}`}
|
|
|
|
className={`tag-name-link ${selectedArtifact?.id === a.id ? 'selected' : ''}`}
|
|
|
|
onClick={() => handleTagSelect(t)}
|
|
|
|
onClick={() => handleArtifactSelect(a)}
|
|
|
|
style={{ cursor: 'pointer' }}
|
|
|
|
style={{ cursor: 'pointer' }}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
{t.name}
|
|
|
|
{a.tags.length > 0 ? a.tags[0] : '—'}
|
|
|
|
</strong>
|
|
|
|
</strong>
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
key: 'version',
|
|
|
|
key: 'version',
|
|
|
|
header: 'Version',
|
|
|
|
header: 'Version',
|
|
|
|
render: (t: TagDetail) => (
|
|
|
|
render: (a: PackageArtifact) => (
|
|
|
|
<span className="version-badge">{t.version || '—'}</span>
|
|
|
|
<span className="version-badge">{getArtifactVersion(a) || '—'}</span>
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
key: 'artifact_original_name',
|
|
|
|
key: 'original_name',
|
|
|
|
header: 'Filename',
|
|
|
|
header: 'Filename',
|
|
|
|
className: 'cell-truncate',
|
|
|
|
className: 'cell-truncate',
|
|
|
|
render: (t: TagDetail) => (
|
|
|
|
render: (a: PackageArtifact) => (
|
|
|
|
<span title={t.artifact_original_name || undefined}>{t.artifact_original_name || '—'}</span>
|
|
|
|
<span title={a.original_name || undefined}>{a.original_name || '—'}</span>
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
key: 'artifact_size',
|
|
|
|
key: 'size',
|
|
|
|
header: 'Size',
|
|
|
|
header: 'Size',
|
|
|
|
render: (t: TagDetail) => <span>{formatBytes(t.artifact_size)}</span>,
|
|
|
|
render: (a: PackageArtifact) => <span>{formatBytes(a.size)}</span>,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
key: 'created_at',
|
|
|
|
key: 'created_at',
|
|
|
|
header: 'Created',
|
|
|
|
header: 'Created',
|
|
|
|
sortable: true,
|
|
|
|
sortable: true,
|
|
|
|
render: (t: TagDetail) => (
|
|
|
|
render: (a: PackageArtifact) => (
|
|
|
|
<span title={`by ${t.created_by}`}>{new Date(t.created_at).toLocaleDateString()}</span>
|
|
|
|
<span title={`by ${a.created_by}`}>{new Date(a.created_at).toLocaleDateString()}</span>
|
|
|
|
),
|
|
|
|
),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
{
|
|
|
|
key: 'actions',
|
|
|
|
key: 'actions',
|
|
|
|
header: '',
|
|
|
|
header: '',
|
|
|
|
render: (t: TagDetail) => (
|
|
|
|
render: (a: PackageArtifact) => (
|
|
|
|
<div className="action-buttons">
|
|
|
|
<div className="action-buttons">
|
|
|
|
<a
|
|
|
|
<a
|
|
|
|
href={getDownloadUrl(projectName!, packageName!, t.name)}
|
|
|
|
href={getDownloadUrl(projectName!, packageName!, getDownloadRef(a))}
|
|
|
|
className="btn btn-icon"
|
|
|
|
className="btn btn-icon"
|
|
|
|
download
|
|
|
|
download
|
|
|
|
title="Download"
|
|
|
|
title="Download"
|
|
|
|
@@ -484,7 +484,7 @@ function PackagePage() {
|
|
|
|
</a>
|
|
|
|
</a>
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
className="btn btn-icon"
|
|
|
|
className="btn btn-icon"
|
|
|
|
onClick={(e) => handleMenuOpen(e, t.id)}
|
|
|
|
onClick={(e) => handleMenuOpen(e, a.id)}
|
|
|
|
title="More actions"
|
|
|
|
title="More actions"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
|
|
@@ -498,8 +498,8 @@ function PackagePage() {
|
|
|
|
},
|
|
|
|
},
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// Find the tag for the open menu
|
|
|
|
// Find the artifact for the open menu
|
|
|
|
const openMenuTag = tags.find(t => t.id === openMenuId);
|
|
|
|
const openMenuArtifact = artifacts.find(a => a.id === openMenuId);
|
|
|
|
|
|
|
|
|
|
|
|
// Close menu when clicking outside
|
|
|
|
// Close menu when clicking outside
|
|
|
|
const handleClickOutside = () => {
|
|
|
|
const handleClickOutside = () => {
|
|
|
|
@@ -511,8 +511,8 @@ function PackagePage() {
|
|
|
|
|
|
|
|
|
|
|
|
// Render dropdown menu as a portal-like element
|
|
|
|
// Render dropdown menu as a portal-like element
|
|
|
|
const renderActionMenu = () => {
|
|
|
|
const renderActionMenu = () => {
|
|
|
|
if (!openMenuId || !menuPosition || !openMenuTag) return null;
|
|
|
|
if (!openMenuId || !menuPosition || !openMenuArtifact) return null;
|
|
|
|
const t = openMenuTag;
|
|
|
|
const a = openMenuArtifact;
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
<div
|
|
|
|
className="action-menu-backdrop"
|
|
|
|
className="action-menu-backdrop"
|
|
|
|
@@ -523,21 +523,23 @@ function PackagePage() {
|
|
|
|
style={{ top: menuPosition.top, left: menuPosition.left }}
|
|
|
|
style={{ top: menuPosition.top, left: menuPosition.left }}
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
<button onClick={() => { setViewArtifactId(t.artifact_id); setShowArtifactIdModal(true); setOpenMenuId(null); setMenuPosition(null); }}>
|
|
|
|
<button onClick={() => { setViewArtifactId(a.id); setShowArtifactIdModal(true); setOpenMenuId(null); setMenuPosition(null); }}>
|
|
|
|
View Artifact ID
|
|
|
|
View Artifact ID
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
<button onClick={() => { navigator.clipboard.writeText(t.artifact_id); setOpenMenuId(null); setMenuPosition(null); }}>
|
|
|
|
<button onClick={() => { navigator.clipboard.writeText(a.id); setOpenMenuId(null); setMenuPosition(null); }}>
|
|
|
|
Copy Artifact ID
|
|
|
|
Copy Artifact ID
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
<button onClick={() => { fetchEnsureFileForTag(t.name); setOpenMenuId(null); setMenuPosition(null); }}>
|
|
|
|
{a.tags.length > 0 && (
|
|
|
|
|
|
|
|
<button onClick={() => { fetchEnsureFileForTag(a.tags[0]); setOpenMenuId(null); setMenuPosition(null); }}>
|
|
|
|
View Ensure File
|
|
|
|
View Ensure File
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
|
|
|
|
)}
|
|
|
|
{canWrite && !isSystemProject && (
|
|
|
|
{canWrite && !isSystemProject && (
|
|
|
|
<button onClick={() => { setCreateTagArtifactId(t.artifact_id); setShowCreateTagModal(true); setOpenMenuId(null); setMenuPosition(null); }}>
|
|
|
|
<button onClick={() => { setCreateTagArtifactId(a.id); setShowCreateTagModal(true); setOpenMenuId(null); setMenuPosition(null); }}>
|
|
|
|
Create/Update Tag
|
|
|
|
Create/Update Tag
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
<button onClick={() => { handleTagSelect(t); setShowDepsModal(true); setOpenMenuId(null); setMenuPosition(null); }}>
|
|
|
|
<button onClick={() => { handleArtifactSelect(a); setShowDepsModal(true); setOpenMenuId(null); setMenuPosition(null); }}>
|
|
|
|
View Dependencies
|
|
|
|
View Dependencies
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
@@ -545,7 +547,7 @@ function PackagePage() {
|
|
|
|
);
|
|
|
|
);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (loading && !tagsData) {
|
|
|
|
if (loading && !artifactsData) {
|
|
|
|
return <div className="loading">Loading...</div>;
|
|
|
|
return <div className="loading">Loading...</div>;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -653,7 +655,7 @@ function PackagePage() {
|
|
|
|
<SearchInput
|
|
|
|
<SearchInput
|
|
|
|
value={search}
|
|
|
|
value={search}
|
|
|
|
onChange={handleSearchChange}
|
|
|
|
onChange={handleSearchChange}
|
|
|
|
placeholder="Filter tags..."
|
|
|
|
placeholder="Filter artifacts..."
|
|
|
|
className="list-controls__search"
|
|
|
|
className="list-controls__search"
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
@@ -666,13 +668,13 @@ function PackagePage() {
|
|
|
|
|
|
|
|
|
|
|
|
<div className="data-table--responsive">
|
|
|
|
<div className="data-table--responsive">
|
|
|
|
<DataTable
|
|
|
|
<DataTable
|
|
|
|
data={tags}
|
|
|
|
data={artifacts}
|
|
|
|
columns={columns}
|
|
|
|
columns={columns}
|
|
|
|
keyExtractor={(t) => t.id}
|
|
|
|
keyExtractor={(a) => a.id}
|
|
|
|
emptyMessage={
|
|
|
|
emptyMessage={
|
|
|
|
hasActiveFilters
|
|
|
|
hasActiveFilters
|
|
|
|
? 'No tags match your filters. Try adjusting your search.'
|
|
|
|
? 'No artifacts match your filters. Try adjusting your search.'
|
|
|
|
: 'No tags yet. Upload an artifact with a tag to create one!'
|
|
|
|
: 'No artifacts yet. Upload a file to get started!'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
onSort={handleSortChange}
|
|
|
|
onSort={handleSortChange}
|
|
|
|
sortKey={sort}
|
|
|
|
sortKey={sort}
|
|
|
|
@@ -752,11 +754,11 @@ function PackagePage() {
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Dependency Graph Modal */}
|
|
|
|
{/* Dependency Graph Modal */}
|
|
|
|
{showGraph && selectedTag && (
|
|
|
|
{showGraph && selectedArtifact && (
|
|
|
|
<DependencyGraph
|
|
|
|
<DependencyGraph
|
|
|
|
projectName={projectName!}
|
|
|
|
projectName={projectName!}
|
|
|
|
packageName={packageName!}
|
|
|
|
packageName={packageName!}
|
|
|
|
tagName={selectedTag.name}
|
|
|
|
tagName={selectedArtifact.tags.length > 0 ? selectedArtifact.tags[0] : `artifact:${selectedArtifact.id}`}
|
|
|
|
onClose={() => setShowGraph(false)}
|
|
|
|
onClose={() => setShowGraph(false)}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
@@ -916,11 +918,11 @@ function PackagePage() {
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Dependencies Modal */}
|
|
|
|
{/* Dependencies Modal */}
|
|
|
|
{showDepsModal && selectedTag && (
|
|
|
|
{showDepsModal && selectedArtifact && (
|
|
|
|
<div className="modal-overlay" onClick={() => setShowDepsModal(false)}>
|
|
|
|
<div className="modal-overlay" onClick={() => setShowDepsModal(false)}>
|
|
|
|
<div className="deps-modal" onClick={(e) => e.stopPropagation()}>
|
|
|
|
<div className="deps-modal" onClick={(e) => e.stopPropagation()}>
|
|
|
|
<div className="modal-header">
|
|
|
|
<div className="modal-header">
|
|
|
|
<h3>Dependencies for {selectedTag.version || selectedTag.name}</h3>
|
|
|
|
<h3>Dependencies for {selectedArtifact.original_name || selectedArtifact.id.slice(0, 12)}</h3>
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
className="modal-close"
|
|
|
|
className="modal-close"
|
|
|
|
onClick={() => setShowDepsModal(false)}
|
|
|
|
onClick={() => setShowDepsModal(false)}
|
|
|
|
@@ -934,6 +936,7 @@ function PackagePage() {
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="modal-body">
|
|
|
|
<div className="modal-body">
|
|
|
|
<div className="deps-modal-controls">
|
|
|
|
<div className="deps-modal-controls">
|
|
|
|
|
|
|
|
{selectedArtifact?.tags && selectedArtifact.tags.length > 0 && (
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
className="btn btn-secondary btn-small"
|
|
|
|
className="btn btn-secondary btn-small"
|
|
|
|
onClick={fetchEnsureFile}
|
|
|
|
onClick={fetchEnsureFile}
|
|
|
|
@@ -941,6 +944,7 @@ function PackagePage() {
|
|
|
|
>
|
|
|
|
>
|
|
|
|
View Ensure File
|
|
|
|
View Ensure File
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
|
|
|
|
)}
|
|
|
|
<button
|
|
|
|
<button
|
|
|
|
className="btn btn-secondary btn-small"
|
|
|
|
className="btn btn-secondary btn-small"
|
|
|
|
onClick={() => { setShowDepsModal(false); setShowGraph(true); }}
|
|
|
|
onClick={() => { setShowDepsModal(false); setShowGraph(true); }}
|
|
|
|
|