refactor: full-stack restructure with multi-tenancy, workspace management, and K8s diagnostics
- Add Workspace domain (entity, repository, service, handler, DTO) - Add multi-tenant K8s client with tenant binding and quota management - Add K8s diagnostics client (instance diagnostics) - Add authorization middleware (authz package) - Restructure frontend to feature-based architecture (features/) - Add User Management page in configuration - Add AccessDenied page and route guards - Refactor shared components (form inputs, layout, UI) - Update Tailwind config for new design system - Add comprehensive documentation (docs/, tasks/, plans) - Improve cluster service with better kubeconfig handling - Add tests for crypto, config, helm client, tenant binding
This commit is contained in:
@ -48,10 +48,8 @@ interface RegistryNode {
|
||||
}
|
||||
|
||||
const FILTER_OPTIONS: Array<{ value: ListArtifactsFilter | undefined; label: string }> = [
|
||||
{ value: undefined, label: "All" },
|
||||
{ value: "chart", label: "Charts" },
|
||||
{ value: "image", label: "Images" },
|
||||
{ value: "other", label: "Other" },
|
||||
{ value: undefined, label: "All tags" },
|
||||
];
|
||||
|
||||
const ArtifactBrowserPage: React.FC = () => {
|
||||
@ -67,7 +65,7 @@ const ArtifactBrowserPage: React.FC = () => {
|
||||
const [artifacts, setArtifacts] = useState<ArtifactListItem[]>([]);
|
||||
const [loadingArtifacts, setLoadingArtifacts] = useState(false);
|
||||
const [artifactError, setArtifactError] = useState<string | null>(null);
|
||||
const [filter, setFilter] = useState<ListArtifactsFilter | undefined>(undefined);
|
||||
const [filter, setFilter] = useState<ListArtifactsFilter | undefined>("chart");
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
@ -143,7 +141,7 @@ const ArtifactBrowserPage: React.FC = () => {
|
||||
try {
|
||||
let repoNodes = globalCache.get<RepositoryNode[]>("repositories", registryId);
|
||||
if (!repoNodes) {
|
||||
const response = await listRepositories({ registryId });
|
||||
const response = await listRepositories({ registryId }, { artifactType: "chart" });
|
||||
repoNodes = normalizeRepositories(registry, response);
|
||||
globalCache.set("repositories", repoNodes, registryId);
|
||||
}
|
||||
@ -255,17 +253,21 @@ const ArtifactBrowserPage: React.FC = () => {
|
||||
? registryNodes.find((node) => node.registry.id === selectedRepository.registryId)?.registry
|
||||
.name
|
||||
: null;
|
||||
const selectedRegistryUrl = selectedRepository
|
||||
? registryNodes.find((node) => node.registry.id === selectedRepository.registryId)?.registry
|
||||
.url
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-8rem)] -m-6 flex flex-col">
|
||||
<div className="flex-shrink-0 border-b border-dark-border bg-dark-card px-6 py-4">
|
||||
<div className="flex-shrink-0 border-b border-slate-200 bg-white px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Package className="w-6 h-6 text-purple-400" />
|
||||
<Package className="w-6 h-6 text-blue-600" />
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-white">Artifact Browser</h1>
|
||||
<p className="text-sm text-gray-400">
|
||||
Browse registries, repositories, and artifacts
|
||||
<h1 className="text-xl font-semibold text-slate-900">Chart Browser</h1>
|
||||
<p className="text-sm text-slate-500">
|
||||
Select a Harbor chart and launch it into a Kubernetes cluster
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -280,23 +282,23 @@ const ArtifactBrowserPage: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex overflow-hidden bg-dark-bg">
|
||||
<aside className="w-80 border-r border-dark-border bg-gradient-to-b from-gray-900 via-gray-950 to-gray-900 flex flex-col">
|
||||
<div className="p-4 border-b border-dark-border space-y-2">
|
||||
<div className="flex-1 flex overflow-hidden bg-slate-50">
|
||||
<aside className="w-80 border-r border-slate-200 bg-white flex flex-col">
|
||||
<div className="p-4 border-b border-slate-200 space-y-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search registries / repositories..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-8 pr-3 py-2 rounded-lg bg-gray-900/70 border border-gray-700 text-sm text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
className="w-full pl-8 pr-3 py-2 rounded-lg bg-white border border-slate-200 text-sm text-slate-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
{repositoryError && (
|
||||
<p className="text-xs text-red-400">{repositoryError}</p>
|
||||
)}
|
||||
<div className="flex items-center justify-between text-xs text-gray-400">
|
||||
<div className="flex items-center justify-between text-xs text-slate-500">
|
||||
<span>Registries</span>
|
||||
<Badge variant="secondary">{registryNodes.length}</Badge>
|
||||
</div>
|
||||
@ -312,7 +314,7 @@ const ArtifactBrowserPage: React.FC = () => {
|
||||
<EmptyState
|
||||
icon={Database}
|
||||
title="No registries"
|
||||
description="Add a registry to get started."
|
||||
description="Add a Harbor registry to browse deployable charts."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@ -320,18 +322,18 @@ const ArtifactBrowserPage: React.FC = () => {
|
||||
<div key={node.registry.id || node.registry.name}>
|
||||
<button
|
||||
onClick={() => toggleRegistry(node.registry.id)}
|
||||
className="w-full flex items-center justify-between px-4 py-3 hover:bg-gray-800/60 transition"
|
||||
className="w-full flex items-center justify-between px-4 py-3 hover:bg-slate-50 transition"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{node.expanded ? (
|
||||
<ChevronDown className="w-4 h-4 text-gray-400" />
|
||||
<ChevronDown className="w-4 h-4 text-slate-500" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-gray-400" />
|
||||
<ChevronRight className="w-4 h-4 text-slate-500" />
|
||||
)}
|
||||
<Database className="w-4 h-4 text-purple-400" />
|
||||
<Database className="w-4 h-4 text-blue-600" />
|
||||
<div className="text-left">
|
||||
<p className="text-sm text-white">{node.registry.name || "Unnamed"}</p>
|
||||
<p className="text-[11px] text-gray-500 truncate">
|
||||
<p className="text-sm text-slate-900">{node.registry.name || "Unnamed"}</p>
|
||||
<p className="text-[11px] text-slate-500 truncate">
|
||||
{node.registry.url}
|
||||
</p>
|
||||
</div>
|
||||
@ -339,12 +341,12 @@ const ArtifactBrowserPage: React.FC = () => {
|
||||
<Badge variant="secondary">{node.repositories.length}</Badge>
|
||||
</button>
|
||||
{node.expanded && (
|
||||
<div className="bg-gray-900/60">
|
||||
<div className="bg-slate-50/60">
|
||||
{node.repositories.length === 0 ? (
|
||||
<p className="px-8 py-3 text-xs text-gray-500">
|
||||
<p className="px-8 py-3 text-xs text-slate-500">
|
||||
{loadingRepositories
|
||||
? "Loading repositories..."
|
||||
: "No repositories found."}
|
||||
: "No chart repositories found."}
|
||||
</p>
|
||||
) : (
|
||||
node.repositories.map((repo) => {
|
||||
@ -357,13 +359,13 @@ const ArtifactBrowserPage: React.FC = () => {
|
||||
onClick={() => handleRepositoryClick(repo)}
|
||||
className={`w-full text-left px-8 py-2 flex items-center justify-between text-sm transition ${
|
||||
isSelected
|
||||
? "bg-purple-600/20 text-white"
|
||||
: "hover:bg-gray-800/80 text-gray-300"
|
||||
? "bg-blue-50 text-blue-700"
|
||||
: "hover:bg-white/80 text-slate-700"
|
||||
}`}
|
||||
>
|
||||
<span className="truncate">{repo.name}</span>
|
||||
{repo.artifactCount !== undefined && (
|
||||
<span className="text-xs text-gray-500">
|
||||
<span className="text-xs text-slate-500">
|
||||
{repo.artifactCount}
|
||||
</span>
|
||||
)}
|
||||
@ -379,38 +381,38 @@ const ArtifactBrowserPage: React.FC = () => {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 flex flex-col bg-dark-card overflow-hidden">
|
||||
<main className="flex-1 flex flex-col bg-white overflow-hidden">
|
||||
{!selectedRepository ? (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<EmptyState
|
||||
icon={Package}
|
||||
title="Select a repository"
|
||||
description="Choose a repository from the left panel to view artifacts."
|
||||
description="Choose a chart repository from the left panel."
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex-shrink-0 border-b border-dark-border p-5 bg-gradient-to-r from-gray-900 to-gray-850">
|
||||
<div className="flex-shrink-0 border-b border-slate-200 p-5 bg-slate-50">
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-gray-500">Repository</p>
|
||||
<h2 className="text-2xl font-semibold text-white">
|
||||
<p className="text-xs uppercase text-slate-500">Chart repository</p>
|
||||
<h2 className="text-2xl font-semibold text-slate-900">
|
||||
{selectedRepository.name}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-400">
|
||||
<p className="text-sm text-slate-500">
|
||||
{selectedRegistryName || selectedRepository.registryId}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Filter className="w-4 h-4 text-gray-400" />
|
||||
<Filter className="w-4 h-4 text-slate-500" />
|
||||
{FILTER_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.label}
|
||||
onClick={() => setFilter(option.value)}
|
||||
className={`px-3 py-1.5 text-xs rounded-full border transition ${
|
||||
filter === option.value
|
||||
? "bg-purple-600 text-white border-purple-500"
|
||||
: "border-gray-700 text-gray-300 hover:border-gray-500"
|
||||
? "bg-blue-600 text-white border-blue-600"
|
||||
: "border-slate-200 text-slate-700 hover:border-slate-400"
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
@ -432,8 +434,8 @@ const ArtifactBrowserPage: React.FC = () => {
|
||||
title="No artifacts"
|
||||
description={
|
||||
filter
|
||||
? `No ${filter} artifacts found for this repository.`
|
||||
: "This repository doesn't contain any artifacts yet."
|
||||
? `No ${filter} tags found for this repository.`
|
||||
: "This repository doesn't contain any tagged artifacts yet."
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
@ -442,6 +444,7 @@ const ArtifactBrowserPage: React.FC = () => {
|
||||
<TagCard
|
||||
key={`${artifact.repositoryName || "repo"}-${artifact.tag || index}`}
|
||||
registryId={selectedRepository.registryId}
|
||||
registryUrl={selectedRegistryUrl}
|
||||
tag={artifact}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user