Remove tag system, use versions only for artifact references
Tags were mutable aliases that caused confusion alongside the immutable version system. This removes tags entirely, keeping only PackageVersion for artifact references. Changes: - Remove tags and tag_history tables (migration 012) - Remove Tag model, TagRepository, and 6 tag API endpoints - Update cache system to create versions instead of tags - Update frontend to display versions instead of tags - Remove tag-related schemas and types - Update artifact cleanup service for version-based ref_count
This commit is contained in:
@@ -1,14 +1,11 @@
|
||||
import {
|
||||
Project,
|
||||
Package,
|
||||
Tag,
|
||||
TagDetail,
|
||||
ArtifactDetail,
|
||||
PackageArtifact,
|
||||
UploadResponse,
|
||||
PaginatedResponse,
|
||||
ListParams,
|
||||
TagListParams,
|
||||
PackageListParams,
|
||||
ArtifactListParams,
|
||||
ProjectListParams,
|
||||
@@ -240,32 +237,6 @@ export async function createPackage(projectName: string, data: { name: string; d
|
||||
return handleResponse<Package>(response);
|
||||
}
|
||||
|
||||
// Tag API
|
||||
export async function listTags(projectName: string, packageName: string, params: TagListParams = {}): Promise<PaginatedResponse<TagDetail>> {
|
||||
const query = buildQueryString(params as Record<string, unknown>);
|
||||
const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/tags${query}`);
|
||||
return handleResponse<PaginatedResponse<TagDetail>>(response);
|
||||
}
|
||||
|
||||
export async function listTagsSimple(projectName: string, packageName: string, params: TagListParams = {}): Promise<TagDetail[]> {
|
||||
const data = await listTags(projectName, packageName, params);
|
||||
return data.items;
|
||||
}
|
||||
|
||||
export async function getTag(projectName: string, packageName: string, tagName: string): Promise<TagDetail> {
|
||||
const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/tags/${tagName}`);
|
||||
return handleResponse<TagDetail>(response);
|
||||
}
|
||||
|
||||
export async function createTag(projectName: string, packageName: string, data: { name: string; artifact_id: string }): Promise<Tag> {
|
||||
const response = await fetch(`${API_BASE}/project/${projectName}/${packageName}/tags`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return handleResponse<Tag>(response);
|
||||
}
|
||||
|
||||
// Artifact API
|
||||
export async function getArtifact(artifactId: string): Promise<ArtifactDetail> {
|
||||
const response = await fetch(`${API_BASE}/artifact/${artifactId}`);
|
||||
@@ -287,14 +258,10 @@ export async function uploadArtifact(
|
||||
projectName: string,
|
||||
packageName: string,
|
||||
file: File,
|
||||
tag?: string,
|
||||
version?: string
|
||||
): Promise<UploadResponse> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
if (tag) {
|
||||
formData.append('tag', tag);
|
||||
}
|
||||
if (version) {
|
||||
formData.append('version', version);
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ function DependencyGraph({ projectName, packageName, tagName, onClose }: Depende
|
||||
label: `${artifact.project}/${artifact.package}`,
|
||||
project: artifact.project,
|
||||
package: artifact.package,
|
||||
version: artifact.version || artifact.tag,
|
||||
version: artifact.version,
|
||||
size: artifact.size,
|
||||
isRoot,
|
||||
onNavigate,
|
||||
|
||||
@@ -524,7 +524,7 @@ describe('DragDropUpload', () => {
|
||||
}
|
||||
vi.stubGlobal('XMLHttpRequest', MockXHR);
|
||||
|
||||
render(<DragDropUpload {...defaultProps} tag="v1.0.0" />);
|
||||
render(<DragDropUpload {...defaultProps} />);
|
||||
|
||||
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
const file = createMockFile('test.txt', 100, 'text/plain');
|
||||
|
||||
@@ -13,7 +13,6 @@ interface StoredUploadState {
|
||||
completedParts: number[];
|
||||
project: string;
|
||||
package: string;
|
||||
tag?: string;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
@@ -87,7 +86,6 @@ export interface DragDropUploadProps {
|
||||
maxFileSize?: number; // in bytes
|
||||
maxConcurrentUploads?: number;
|
||||
maxRetries?: number;
|
||||
tag?: string;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
disabledReason?: string;
|
||||
@@ -230,7 +228,6 @@ export function DragDropUpload({
|
||||
maxFileSize,
|
||||
maxConcurrentUploads = 3,
|
||||
maxRetries = 3,
|
||||
tag,
|
||||
className = '',
|
||||
disabled = false,
|
||||
disabledReason,
|
||||
@@ -368,7 +365,6 @@ export function DragDropUpload({
|
||||
expected_hash: fileHash,
|
||||
filename: item.file.name,
|
||||
size: item.file.size,
|
||||
tag: tag || undefined,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -392,7 +388,6 @@ export function DragDropUpload({
|
||||
completedParts: [],
|
||||
project: projectName,
|
||||
package: packageName,
|
||||
tag: tag || undefined,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
|
||||
@@ -438,7 +433,6 @@ export function DragDropUpload({
|
||||
completedParts,
|
||||
project: projectName,
|
||||
package: packageName,
|
||||
tag: tag || undefined,
|
||||
createdAt: Date.now(),
|
||||
});
|
||||
|
||||
@@ -459,7 +453,7 @@ export function DragDropUpload({
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tag: tag || undefined }),
|
||||
body: JSON.stringify({}),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -475,18 +469,15 @@ export function DragDropUpload({
|
||||
size: completeData.size,
|
||||
deduplicated: false,
|
||||
};
|
||||
}, [projectName, packageName, tag, isOnline]);
|
||||
}, [projectName, packageName, isOnline]);
|
||||
|
||||
const uploadFileSimple = useCallback((item: UploadItem): Promise<UploadResult> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhrMapRef.current.set(item.id, xhr);
|
||||
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', item.file);
|
||||
if (tag) {
|
||||
formData.append('tag', tag);
|
||||
}
|
||||
|
||||
let lastLoaded = 0;
|
||||
let lastTime = Date.now();
|
||||
@@ -549,13 +540,13 @@ export function DragDropUpload({
|
||||
xhr.timeout = 300000;
|
||||
xhr.send(formData);
|
||||
|
||||
setUploadQueue(prev => prev.map(u =>
|
||||
u.id === item.id
|
||||
setUploadQueue(prev => prev.map(u =>
|
||||
u.id === item.id
|
||||
? { ...u, status: 'uploading' as UploadStatus, startTime: Date.now() }
|
||||
: u
|
||||
));
|
||||
});
|
||||
}, [projectName, packageName, tag]);
|
||||
}, [projectName, packageName]);
|
||||
|
||||
const uploadFile = useCallback((item: UploadItem): Promise<UploadResult> => {
|
||||
if (item.file.size >= CHUNKED_UPLOAD_THRESHOLD) {
|
||||
|
||||
@@ -233,7 +233,7 @@ export function GlobalSearch() {
|
||||
const flatIndex = results.projects.length + results.packages.length + index;
|
||||
return (
|
||||
<button
|
||||
key={artifact.tag_id}
|
||||
key={artifact.artifact_id}
|
||||
className={`global-search__result ${selectedIndex === flatIndex ? 'selected' : ''}`}
|
||||
onClick={() => navigateToResult({ type: 'artifact', item: artifact })}
|
||||
onMouseEnter={() => setSelectedIndex(flatIndex)}
|
||||
@@ -243,7 +243,7 @@ export function GlobalSearch() {
|
||||
<line x1="7" y1="7" x2="7.01" y2="7" />
|
||||
</svg>
|
||||
<div className="global-search__result-content">
|
||||
<span className="global-search__result-name">{artifact.tag_name}</span>
|
||||
<span className="global-search__result-name">{artifact.version}</span>
|
||||
<span className="global-search__result-path">
|
||||
{artifact.project_name} / {artifact.package_name}
|
||||
</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useSearchParams, useNavigate, useLocation, Link } from 'react-router-dom';
|
||||
import { PackageArtifact, Package, PaginatedResponse, AccessLevel, Dependency, DependentInfo } from '../types';
|
||||
import { listPackageArtifacts, getDownloadUrl, getPackage, getMyProjectAccess, createTag, getArtifactDependencies, getReverseDependencies, getEnsureFile, UnauthorizedError, ForbiddenError } from '../api';
|
||||
import { listPackageArtifacts, getDownloadUrl, getPackage, getMyProjectAccess, getArtifactDependencies, getReverseDependencies, getEnsureFile, UnauthorizedError, ForbiddenError } from '../api';
|
||||
import { Breadcrumb } from '../components/Breadcrumb';
|
||||
import { Badge } from '../components/Badge';
|
||||
import { SearchInput } from '../components/SearchInput';
|
||||
@@ -61,16 +61,11 @@ function PackagePage() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [accessDenied, setAccessDenied] = useState(false);
|
||||
const [uploadTag, setUploadTag] = useState('');
|
||||
const [uploadSuccess, setUploadSuccess] = useState<string | null>(null);
|
||||
const [accessLevel, setAccessLevel] = useState<AccessLevel | null>(null);
|
||||
const [createTagName, setCreateTagName] = useState('');
|
||||
const [createTagArtifactId, setCreateTagArtifactId] = useState('');
|
||||
const [createTagLoading, setCreateTagLoading] = useState(false);
|
||||
|
||||
// UI state
|
||||
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||
const [showCreateTagModal, setShowCreateTagModal] = useState(false);
|
||||
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
|
||||
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
|
||||
|
||||
@@ -226,15 +221,15 @@ function PackagePage() {
|
||||
}
|
||||
}, [projectName, packageName, loading, fetchReverseDeps]);
|
||||
|
||||
// Fetch ensure file for a specific tag
|
||||
const fetchEnsureFileForTag = useCallback(async (tagName: string) => {
|
||||
// Fetch ensure file for a specific version or artifact
|
||||
const fetchEnsureFileForRef = useCallback(async (ref: string) => {
|
||||
if (!projectName || !packageName) return;
|
||||
|
||||
setEnsureFileTagName(tagName);
|
||||
setEnsureFileTagName(ref);
|
||||
setEnsureFileLoading(true);
|
||||
setEnsureFileError(null);
|
||||
try {
|
||||
const content = await getEnsureFile(projectName, packageName, tagName);
|
||||
const content = await getEnsureFile(projectName, packageName, ref);
|
||||
setEnsureFileContent(content);
|
||||
setShowEnsureFile(true);
|
||||
} catch (err) {
|
||||
@@ -245,11 +240,13 @@ function PackagePage() {
|
||||
}
|
||||
}, [projectName, packageName]);
|
||||
|
||||
// Fetch ensure file for selected artifact (if it has tags)
|
||||
// Fetch ensure file for selected artifact
|
||||
const fetchEnsureFile = useCallback(async () => {
|
||||
if (!selectedArtifact || selectedArtifact.tags.length === 0) return;
|
||||
fetchEnsureFileForTag(selectedArtifact.tags[0]);
|
||||
}, [selectedArtifact, fetchEnsureFileForTag]);
|
||||
if (!selectedArtifact) return;
|
||||
const version = getArtifactVersion(selectedArtifact);
|
||||
const ref = version || `artifact:${selectedArtifact.id}`;
|
||||
fetchEnsureFileForRef(ref);
|
||||
}, [selectedArtifact, fetchEnsureFileForRef]);
|
||||
|
||||
// Keyboard navigation - go back with backspace
|
||||
useEffect(() => {
|
||||
@@ -265,11 +262,10 @@ function PackagePage() {
|
||||
|
||||
const handleUploadComplete = useCallback((results: UploadResult[]) => {
|
||||
const count = results.length;
|
||||
const message = count === 1
|
||||
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
|
||||
@@ -280,30 +276,6 @@ function PackagePage() {
|
||||
setError(errorMsg);
|
||||
}, []);
|
||||
|
||||
const handleCreateTag = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!createTagName.trim() || createTagArtifactId.length !== 64) return;
|
||||
|
||||
setCreateTagLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await createTag(projectName!, packageName!, {
|
||||
name: createTagName.trim(),
|
||||
artifact_id: createTagArtifactId,
|
||||
});
|
||||
setUploadSuccess(`Tag "${createTagName}" created successfully!`);
|
||||
setCreateTagName('');
|
||||
setCreateTagArtifactId('');
|
||||
loadData();
|
||||
setTimeout(() => setUploadSuccess(null), 5000);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create tag');
|
||||
} finally {
|
||||
setCreateTagLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
updateParams({ search: value, page: '1' });
|
||||
};
|
||||
@@ -346,9 +318,10 @@ function PackagePage() {
|
||||
return (a.format_metadata?.version as string) || null;
|
||||
};
|
||||
|
||||
// Helper to get download ref - prefer first tag, fallback to artifact ID
|
||||
// Helper to get download ref - prefer version, fallback to artifact ID
|
||||
const getDownloadRef = (a: PackageArtifact): string => {
|
||||
return a.tags.length > 0 ? a.tags[0] : `artifact:${a.id}`;
|
||||
const version = getArtifactVersion(a);
|
||||
return version || `artifact:${a.id}`;
|
||||
};
|
||||
|
||||
// System projects show Version first, regular projects show Tag first
|
||||
@@ -365,7 +338,7 @@ function PackagePage() {
|
||||
onClick={() => handleArtifactSelect(a)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
<span className="version-badge">{getArtifactVersion(a) || a.tags[0] || a.id.slice(0, 12)}</span>
|
||||
<span className="version-badge">{getArtifactVersion(a) || a.id.slice(0, 12)}</span>
|
||||
</strong>
|
||||
),
|
||||
},
|
||||
@@ -425,30 +398,22 @@ function PackagePage() {
|
||||
},
|
||||
]
|
||||
: [
|
||||
// Regular project columns: Tag, Version, Filename, Size, Created
|
||||
// Regular project columns: Version, Filename, Size, Created
|
||||
// Valid sort fields: created_at, size, original_name
|
||||
{
|
||||
key: 'tags',
|
||||
header: 'Tag',
|
||||
// tags is not a sortable DB field
|
||||
key: 'version',
|
||||
header: 'Version',
|
||||
// version is from format_metadata, not a sortable DB field
|
||||
render: (a: PackageArtifact) => (
|
||||
<strong
|
||||
className={`tag-name-link ${selectedArtifact?.id === a.id ? 'selected' : ''}`}
|
||||
onClick={() => handleArtifactSelect(a)}
|
||||
style={{ cursor: 'pointer' }}
|
||||
>
|
||||
{a.tags.length > 0 ? a.tags[0] : '—'}
|
||||
<span className="version-badge">{getArtifactVersion(a) || a.id.slice(0, 12)}</span>
|
||||
</strong>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'version',
|
||||
header: 'Version',
|
||||
// version is from format_metadata, not a sortable DB field
|
||||
render: (a: PackageArtifact) => (
|
||||
<span className="version-badge">{getArtifactVersion(a) || '—'}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'original_name',
|
||||
header: 'Filename',
|
||||
@@ -536,16 +501,9 @@ function PackagePage() {
|
||||
<button onClick={() => { navigator.clipboard.writeText(a.id); setOpenMenuId(null); setMenuPosition(null); }}>
|
||||
Copy Artifact ID
|
||||
</button>
|
||||
{a.tags.length > 0 && (
|
||||
<button onClick={() => { fetchEnsureFileForTag(a.tags[0]); setOpenMenuId(null); setMenuPosition(null); }}>
|
||||
View Ensure File
|
||||
</button>
|
||||
)}
|
||||
{canWrite && !isSystemProject && (
|
||||
<button onClick={() => { setCreateTagArtifactId(a.id); setShowCreateTagModal(true); setOpenMenuId(null); setMenuPosition(null); }}>
|
||||
Create/Update Tag
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => { const version = getArtifactVersion(a); const ref = version || `artifact:${a.id}`; fetchEnsureFileForRef(ref); setOpenMenuId(null); setMenuPosition(null); }}>
|
||||
View Ensure File
|
||||
</button>
|
||||
<button onClick={() => { handleArtifactSelect(a); setShowDepsModal(true); setOpenMenuId(null); setMenuPosition(null); }}>
|
||||
View Dependencies
|
||||
</button>
|
||||
@@ -623,13 +581,8 @@ function PackagePage() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{pkg && (pkg.tag_count !== undefined || pkg.artifact_count !== undefined) && (
|
||||
{pkg && pkg.artifact_count !== undefined && (
|
||||
<div className="package-header-stats">
|
||||
{!isSystemProject && pkg.tag_count !== undefined && (
|
||||
<span className="stat-item">
|
||||
<strong>{pkg.tag_count}</strong> tags
|
||||
</span>
|
||||
)}
|
||||
{pkg.artifact_count !== undefined && (
|
||||
<span className="stat-item">
|
||||
<strong>{pkg.artifact_count}</strong> {isSystemProject ? 'versions' : 'artifacts'}
|
||||
@@ -640,11 +593,6 @@ function PackagePage() {
|
||||
<strong>{formatBytes(pkg.total_size)}</strong> total
|
||||
</span>
|
||||
)}
|
||||
{!isSystemProject && pkg.latest_tag && (
|
||||
<span className="stat-item">
|
||||
Latest: <strong className="accent">{pkg.latest_tag}</strong>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -655,7 +603,7 @@ function PackagePage() {
|
||||
|
||||
|
||||
<div className="section-header">
|
||||
<h2>{isSystemProject ? 'Versions' : 'Tags / Versions'}</h2>
|
||||
<h2>{isSystemProject ? 'Versions' : 'Artifacts'}</h2>
|
||||
</div>
|
||||
|
||||
<div className="list-controls">
|
||||
@@ -754,9 +702,9 @@ function PackagePage() {
|
||||
<pre>
|
||||
<code>curl -O {window.location.origin}/api/v1/project/{projectName}/{packageName}/+/latest</code>
|
||||
</pre>
|
||||
<p>Or with a specific tag:</p>
|
||||
<p>Or with a specific version:</p>
|
||||
<pre>
|
||||
<code>curl -O {window.location.origin}/api/v1/project/{projectName}/{packageName}/+/v1.0.0</code>
|
||||
<code>curl -O {window.location.origin}/api/v1/project/{projectName}/{packageName}/+/1.0.0</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -765,7 +713,7 @@ function PackagePage() {
|
||||
<DependencyGraph
|
||||
projectName={projectName!}
|
||||
packageName={packageName!}
|
||||
tagName={selectedArtifact.tags.length > 0 ? selectedArtifact.tags[0] : `artifact:${selectedArtifact.id}`}
|
||||
tagName={getArtifactVersion(selectedArtifact) || `artifact:${selectedArtifact.id}`}
|
||||
onClose={() => setShowGraph(false)}
|
||||
/>
|
||||
)}
|
||||
@@ -788,24 +736,12 @@ function PackagePage() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<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={(result) => {
|
||||
handleUploadComplete(result);
|
||||
setShowUploadModal(false);
|
||||
setUploadTag('');
|
||||
}}
|
||||
onUploadError={handleUploadError}
|
||||
/>
|
||||
@@ -814,74 +750,6 @@ function PackagePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create/Update Tag Modal */}
|
||||
{showCreateTagModal && (
|
||||
<div className="modal-overlay" onClick={() => setShowCreateTagModal(false)}>
|
||||
<div className="create-tag-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h3>Create / Update Tag</h3>
|
||||
<button
|
||||
className="modal-close"
|
||||
onClick={() => { setShowCreateTagModal(false); setCreateTagName(''); setCreateTagArtifactId(''); }}
|
||||
title="Close"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p className="modal-description">Point a tag at an artifact by its ID</p>
|
||||
<form onSubmit={(e) => { handleCreateTag(e); setShowCreateTagModal(false); }}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="modal-tag-name">Tag Name</label>
|
||||
<input
|
||||
id="modal-tag-name"
|
||||
type="text"
|
||||
value={createTagName}
|
||||
onChange={(e) => setCreateTagName(e.target.value)}
|
||||
placeholder="latest, stable, v1.0.0..."
|
||||
disabled={createTagLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="modal-artifact-id">Artifact ID</label>
|
||||
<input
|
||||
id="modal-artifact-id"
|
||||
type="text"
|
||||
value={createTagArtifactId}
|
||||
onChange={(e) => setCreateTagArtifactId(e.target.value.toLowerCase().replace(/[^a-f0-9]/g, '').slice(0, 64))}
|
||||
placeholder="SHA256 hash (64 hex characters)"
|
||||
className="artifact-id-input"
|
||||
disabled={createTagLoading}
|
||||
/>
|
||||
{createTagArtifactId.length > 0 && createTagArtifactId.length !== 64 && (
|
||||
<p className="validation-hint">{createTagArtifactId.length}/64 characters</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => { setShowCreateTagModal(false); setCreateTagName(''); setCreateTagArtifactId(''); }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
disabled={createTagLoading || !createTagName.trim() || createTagArtifactId.length !== 64}
|
||||
>
|
||||
{createTagLoading ? 'Creating...' : 'Create Tag'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Ensure File Modal */}
|
||||
{showEnsureFile && (
|
||||
<div className="modal-overlay" onClick={() => setShowEnsureFile(false)}>
|
||||
@@ -943,15 +811,13 @@ function PackagePage() {
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<div className="deps-modal-controls">
|
||||
{selectedArtifact?.tags && selectedArtifact.tags.length > 0 && (
|
||||
<button
|
||||
className="btn btn-secondary btn-small"
|
||||
onClick={fetchEnsureFile}
|
||||
disabled={ensureFileLoading}
|
||||
>
|
||||
View Ensure File
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary btn-small"
|
||||
onClick={fetchEnsureFile}
|
||||
disabled={ensureFileLoading}
|
||||
>
|
||||
View Ensure File
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-small"
|
||||
onClick={() => { setShowDepsModal(false); setShowGraph(true); }}
|
||||
@@ -981,7 +847,7 @@ function PackagePage() {
|
||||
{dep.project}/{dep.package}
|
||||
</Link>
|
||||
<span className="dep-constraint">
|
||||
@ {dep.version || dep.tag}
|
||||
@ {dep.version}
|
||||
</span>
|
||||
<span className="dep-status dep-status--ok" title="Package exists">
|
||||
✓
|
||||
|
||||
@@ -349,9 +349,9 @@ function ProjectPage() {
|
||||
render: (pkg: Package) => <Badge variant="default">{pkg.format}</Badge>,
|
||||
}] : []),
|
||||
...(!project?.is_system ? [{
|
||||
key: 'tag_count',
|
||||
header: 'Tags',
|
||||
render: (pkg: Package) => pkg.tag_count ?? '—',
|
||||
key: 'version_count',
|
||||
header: 'Versions',
|
||||
render: (pkg: Package) => pkg.version_count ?? '—',
|
||||
}] : []),
|
||||
{
|
||||
key: 'artifact_count',
|
||||
@@ -365,10 +365,10 @@ function ProjectPage() {
|
||||
pkg.total_size !== undefined && pkg.total_size > 0 ? formatBytes(pkg.total_size) : '—',
|
||||
},
|
||||
...(!project?.is_system ? [{
|
||||
key: 'latest_tag',
|
||||
key: 'latest_version',
|
||||
header: 'Latest',
|
||||
render: (pkg: Package) =>
|
||||
pkg.latest_tag ? <strong style={{ color: 'var(--accent-primary)' }}>{pkg.latest_tag}</strong> : '—',
|
||||
pkg.latest_version ? <strong style={{ color: 'var(--accent-primary)' }}>{pkg.latest_version}</strong> : '—',
|
||||
}] : []),
|
||||
{
|
||||
key: 'created_at',
|
||||
|
||||
@@ -19,12 +19,6 @@ export interface Project {
|
||||
team_name?: string | null;
|
||||
}
|
||||
|
||||
export interface TagSummary {
|
||||
name: string;
|
||||
artifact_id: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Package {
|
||||
id: string;
|
||||
project_id: string;
|
||||
@@ -35,12 +29,11 @@ export interface Package {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
// Aggregated fields (from PackageDetailResponse)
|
||||
tag_count?: number;
|
||||
artifact_count?: number;
|
||||
version_count?: number;
|
||||
total_size?: number;
|
||||
latest_tag?: string | null;
|
||||
latest_upload_at?: string | null;
|
||||
recent_tags?: TagSummary[];
|
||||
latest_version?: string | null;
|
||||
}
|
||||
|
||||
export interface Artifact {
|
||||
@@ -65,25 +58,6 @@ export interface PackageArtifact {
|
||||
created_at: string;
|
||||
created_by: string;
|
||||
format_metadata?: Record<string, unknown> | null;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface Tag {
|
||||
id: string;
|
||||
package_id: string;
|
||||
name: string;
|
||||
artifact_id: string;
|
||||
created_at: string;
|
||||
created_by: string;
|
||||
}
|
||||
|
||||
export interface TagDetail extends Tag {
|
||||
artifact_size: number;
|
||||
artifact_content_type: string | null;
|
||||
artifact_original_name: string | null;
|
||||
artifact_created_at: string;
|
||||
artifact_format_metadata: Record<string, unknown> | null;
|
||||
version: string | null;
|
||||
}
|
||||
|
||||
export interface PackageVersion {
|
||||
@@ -98,20 +72,9 @@ export interface PackageVersion {
|
||||
size?: number;
|
||||
content_type?: string | null;
|
||||
original_name?: string | null;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface ArtifactTagInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
package_id: string;
|
||||
package_name: string;
|
||||
project_name: string;
|
||||
}
|
||||
|
||||
export interface ArtifactDetail extends Artifact {
|
||||
tags: ArtifactTagInfo[];
|
||||
}
|
||||
export interface ArtifactDetail extends Artifact {}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
@@ -131,8 +94,6 @@ export interface ListParams {
|
||||
order?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface TagListParams extends ListParams {}
|
||||
|
||||
export interface PackageListParams extends ListParams {
|
||||
format?: string;
|
||||
platform?: string;
|
||||
@@ -157,7 +118,6 @@ export interface UploadResponse {
|
||||
size: number;
|
||||
project: string;
|
||||
package: string;
|
||||
tag: string | null;
|
||||
version: string | null;
|
||||
version_source: string | null;
|
||||
}
|
||||
@@ -180,9 +140,8 @@ export interface SearchResultPackage {
|
||||
}
|
||||
|
||||
export interface SearchResultArtifact {
|
||||
tag_id: string;
|
||||
tag_name: string;
|
||||
artifact_id: string;
|
||||
version: string | null;
|
||||
package_id: string;
|
||||
package_name: string;
|
||||
project_name: string;
|
||||
@@ -405,8 +364,7 @@ export interface Dependency {
|
||||
artifact_id: string;
|
||||
project: string;
|
||||
package: string;
|
||||
version: string | null;
|
||||
tag: string | null;
|
||||
version: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -420,7 +378,6 @@ export interface DependentInfo {
|
||||
project: string;
|
||||
package: string;
|
||||
version: string | null;
|
||||
constraint_type: 'version' | 'tag';
|
||||
constraint_value: string;
|
||||
}
|
||||
|
||||
@@ -443,7 +400,6 @@ export interface ResolvedArtifact {
|
||||
project: string;
|
||||
package: string;
|
||||
version: string | null;
|
||||
tag: string | null;
|
||||
size: number;
|
||||
download_url: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user