OPC # 0001: Gitea services

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
amadzarak
2026-04-25 19:35:46 -04:00
parent 76962a6af4
commit 65a6b4afaf
13 changed files with 457 additions and 93 deletions
+5 -2
View File
@@ -9,6 +9,7 @@ import ImageBuildPage from './pages/ImageBuildPage';
import BranchPage from './pages/BranchPage';
import OpcPage from './opc/OpcPage';
import InfraPage from './pages/InfraPage';
import ChangesetsPage from './pages/ChangesetsPage';
function App() {
const [activeNav, setActiveNav] = useState('opc');
@@ -30,8 +31,9 @@ function App() {
<MenuItem icon="build" text="Image Build" active={activeNav === 'image-build'} onClick={() => setActiveNav('image-build')} />
<MenuItem icon="pulse" text="Build Monitor" active={activeNav === 'build-monitor'} onClick={() => setActiveNav('build-monitor')} />
<MenuDivider />
<MenuItem icon="heat-grid" text="Infrastructure" active={activeNav === 'infra'} onClick={() => setActiveNav('infra')} />
<MenuItem icon="clipboard" text="OPC" active={activeNav === 'opc'} onClick={() => setActiveNav('opc')} />
<MenuItem icon="heat-grid" text="Infrastructure" active={activeNav === 'infra'} onClick={() => setActiveNav('infra')} />
<MenuItem icon="clipboard" text="OPC" active={activeNav === 'opc'} onClick={() => setActiveNav('opc')} />
<MenuItem icon="history" text="Changesets" active={activeNav === 'changesets'} onClick={() => setActiveNav('changesets')} />
<MenuItem icon="people" text="Clients" active={activeNav === 'clients'} onClick={() => setActiveNav('clients')} />
<MenuItem icon="cog" text="Settings" active={activeNav === 'settings'} onClick={() => setActiveNav('settings')} />
</Menu>
@@ -57,6 +59,7 @@ function App() {
{activeNav === 'build-monitor' && <BuildMonitorPage />}
{activeNav === 'infra' && <InfraPage />}
{activeNav === 'opc' && <OpcPage />}
{activeNav === 'changesets' && <ChangesetsPage />}
{activeNav === 'clients' && <PlaceholderPage title="Clients" />}
{activeNav === 'settings' && <PlaceholderPage title="Settings" />}
</main>
+20 -2
View File
@@ -204,6 +204,22 @@ export async function getBranchCoverageForRepo(repoKey: string, hashes: string[]
return res.json();
}
// ── Changesets (paginated cross-repo commit log) ───────────────────────────────
// Reuses LinkedCommit shape — repoKey identifies which repo each commit came from.
export async function getChangesets(
page = 1,
limit = 25,
repo = 'all',
grep?: string,
): Promise<LinkedCommit[]> {
const params = new URLSearchParams({ page: String(page), limit: String(limit), repo });
if (grep) params.set('grep', grep);
const res = await fetch(`${BASE_URL}/api/git/log?${params}`);
if (!res.ok) throw new Error(`Failed to load changesets: ${res.statusText}`);
return res.json();
}
// ── Commit detail (full diff) ─────────────────────────────────────────────────
export interface CommitFile {
@@ -296,13 +312,15 @@ function mapArtifact(r: any): OpcArtifact {
// ── Gitea branch integration ───────────────────────────────────────────────────
export interface GiteaBranch {
repoKey?: string; // present when querying ?repo=all
name: string;
commitSha: string;
protected: boolean;
}
export async function listGiteaBranches(): Promise<GiteaBranch[]> {
const res = await fetch(`${BASE_URL}/api/gitea/branches`);
export async function listGiteaBranches(repoKey?: string): Promise<GiteaBranch[]> {
const params = repoKey ? `?repo=${encodeURIComponent(repoKey)}` : '';
const res = await fetch(`${BASE_URL}/api/gitea/branches${params}`);
if (!res.ok) throw new Error(`Failed to load Gitea branches: ${res.statusText}`);
return res.json();
}
@@ -14,23 +14,29 @@ const ENVIRONMENTS: { value: TenantEnvironment; label: string; description: stri
];
const TIERS: { value: TenantTier; label: string; description: string; badge: string }[] = [
{
value: 'Trial',
label: 'Trial',
badge: 'Sandbox',
description: 'Ephemeral all-in-one sandbox. Bundled Postgres, shared Keycloak and Vault. No persistent data guarantee.',
},
{
value: 'Shared',
label: 'Shared',
badge: 'Standard',
description: 'Shared Keycloak, Vault, Postgres and MinIO. Isolated by realm, namespace and bucket.',
},
{
value: 'Isolated',
label: 'Isolated',
badge: 'Professional',
description: 'Shared Keycloak and Vault, but a dedicated Postgres container and MinIO bucket per tenant.',
},
{
value: 'Dedicated',
label: 'Dedicated',
badge: 'Professional',
description: 'Own sidecar containers per component (Postgres, Keycloak, Vault, MinIO) on the shared host.',
},
{
value: 'Enterprise',
label: 'Enterprise',
badge: 'Enterprise',
description: 'Fully dedicated Keycloak, Vault, Postgres and MinIO containers for complete hard isolation.',
description: 'Full VM isolation per component. VpsDocker or VpsBareMetal, provisioned via Pulumi.',
},
];
@@ -24,7 +24,7 @@ export default function ReviewStep({ data }: Props) {
<tr><td>Container Name</td><td><code>{containerName}</code></td></tr>
<tr><td>Client URL</td><td><code style={{ fontSize: '0.9em' }}>{clientUrl}</code></td></tr>
<tr><td>Admin Email</td><td>{data.adminEmail}</td></tr>
<tr><td>Tier</td><td><Tag intent={data.tier === 'Dedicated' ? Intent.DANGER : data.tier === 'Isolated' ? Intent.WARNING : Intent.NONE} round>{data.tier}</Tag></td></tr>
<tr><td>Tier</td><td><Tag intent={data.tier === 'Enterprise' ? Intent.DANGER : data.tier === 'Dedicated' ? Intent.WARNING : data.tier === 'Trial' ? Intent.PRIMARY : Intent.NONE} round>{data.tier}</Tag></td></tr>
</tbody>
</HTMLTable>
+96 -1
View File
@@ -385,8 +385,10 @@ body {
}
.tier-badge-shared { background: #e5e8eb; color: #5f6b7c; }
.tier-badge-trial { background: #dce9ff; color: #184a90; }
.tier-badge-isolated { background: #fef3c7; color: #92400e; }
.tier-badge-dedicated { background: #fee2e2; color: #991b1b; }
.tier-badge-dedicated { background: #fde8d8; color: #9e3a06; }
.tier-badge-enterprise { background: #fee2e2; color: #991b1b; }
.tier-card-description {
font-size: 0.8rem;
@@ -963,3 +965,96 @@ body {
color: #82071e;
}
/* ── Changesets Page ───────────────────────────────────────────────────── */
.cs-timeline {
display: flex;
flex-direction: column;
border: 1px solid #dce0e6;
border-radius: 8px;
overflow: hidden;
margin-top: 0.5rem;
}
.cs-row {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.55rem 1rem;
border-bottom: 1px solid #e5e8eb;
cursor: pointer;
transition: background 0.1s;
}
.cs-row:last-child {
border-bottom: none;
}
.cs-row:hover {
background: #f6f7f9;
}
.cs-row-left {
flex-shrink: 0;
width: 68px;
display: flex;
justify-content: flex-end;
padding-top: 2px;
}
.cs-repo-badge {
font-size: 0.67rem;
min-width: 52px;
text-align: center;
letter-spacing: 0.01em;
}
.cs-row-body {
flex: 1;
min-width: 0;
}
.cs-row-top {
display: flex;
align-items: baseline;
gap: 0.5rem;
flex-wrap: nowrap;
min-width: 0;
}
.cs-subject {
font-size: 0.84rem;
font-weight: 500;
color: #1c2127;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cs-row-meta {
font-size: 0.72rem;
color: #738091;
margin-top: 2px;
}
.cs-file-count {
color: #8f99a8;
}
.cs-pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 1rem 0;
border-top: 1px solid #e5e8eb;
margin-top: 0.25rem;
}
.cs-page-label {
font-size: 0.85rem;
color: #5c7080;
font-weight: 500;
min-width: 60px;
text-align: center;
}
+2 -2
View File
@@ -370,7 +370,7 @@ function CommitsTab({ opc, isActive }: { opc: Opc; isActive: boolean }) {
// Load Gitea branch independently — don't block commit rendering
const opcTag = opc.number.replace('OPC # ', 'OPC-');
listGiteaBranches()
listGiteaBranches('all')
.then(branches => {
const found = branches.find(b => b.name.includes(opcTag));
setLinkedBranch(found ?? null);
@@ -458,7 +458,7 @@ function CommitsTab({ opc, isActive }: { opc: Opc; isActive: boolean }) {
{linkedBranch.name}
</Tag>
<a
href={`https://opc.clarity.test/ClarityStack/Clarity/src/branch/${encodeURIComponent(linkedBranch.name)}`}
href={`https://opc.clarity.test/ClarityStack/${linkedBranch.repoKey ?? 'Clarity'}/src/branch/${encodeURIComponent(linkedBranch.name)}`}
target="_blank" rel="noreferrer"
style={{ fontSize: '0.8rem', color: 'var(--bp4-intent-primary)' }}>
Open in Gitea
@@ -0,0 +1,174 @@
import { useState, useEffect, useCallback } from 'react';
import { Button, HTMLSelect, InputGroup, Intent, NonIdealState, Spinner, Tag, Tooltip } from '@blueprintjs/core';
import { GitCommitDrawer } from '../components/GitCommitDrawer';
import { getChangesets } from '../api/opcApi';
import type { LinkedCommit } from '../api/opcApi';
const PAGE_SIZE = 25;
const REPO_OPTIONS = [
{ value: 'all', label: 'All Repos' },
{ value: 'Clarity', label: 'Clarity' },
{ value: 'OPC', label: 'OPC' },
{ value: 'Gateway', label: 'Gateway' },
];
const REPO_INTENT: Record<string, Intent> = {
Clarity: Intent.PRIMARY,
OPC: Intent.WARNING,
Gateway: Intent.SUCCESS,
};
export default function ChangesetsPage() {
const [commits, setCommits] = useState<LinkedCommit[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(false);
const [repoFilter, setRepoFilter] = useState('all');
const [grepFilter, setGrepFilter] = useState('');
const [grepInput, setGrepInput] = useState('');
const [viewingHash, setViewingHash] = useState<string | null>(null);
const load = useCallback(async (p: number, repo: string, grep: string) => {
setLoading(true);
setError(null);
try {
// Fetch one extra to detect whether there's a next page
const rows = await getChangesets(p, PAGE_SIZE + 1, repo, grep || undefined);
setHasMore(rows.length > PAGE_SIZE);
setCommits(rows.slice(0, PAGE_SIZE));
} catch (e) {
setError(String(e));
setCommits([]);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
load(page, repoFilter, grepFilter);
}, [load, page, repoFilter, grepFilter]);
const applySearch = () => {
setPage(1);
setGrepFilter(grepInput);
};
const clearSearch = () => {
setGrepInput('');
setGrepFilter('');
setPage(1);
};
const handleRepoChange = (repo: string) => {
setPage(1);
setRepoFilter(repo);
};
return (
<div>
<div className="page-header">
<div>
<h1>Changesets</h1>
<p>Chronological commit timeline across all three repos.</p>
</div>
<Button icon="refresh" minimal onClick={() => load(page, repoFilter, grepFilter)} />
</div>
{/* Filter bar */}
<div className="opc-filter-bar">
<HTMLSelect
value={repoFilter}
onChange={e => handleRepoChange(e.target.value)}
options={REPO_OPTIONS}
/>
<InputGroup
leftIcon="search"
placeholder="Filter commits by text or OPC number…"
value={grepInput}
onChange={e => setGrepInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') applySearch(); }}
style={{ width: 320 }}
rightElement={
<Button minimal icon="arrow-right" intent={Intent.PRIMARY}
disabled={!grepInput.trim()} onClick={applySearch} />
}
/>
{grepFilter && (
<Button minimal small icon="cross" text="Clear" onClick={clearSearch} />
)}
<span className="opc-count-badge">{loading ? '…' : `${commits.length} shown`}</span>
</div>
{/* Content */}
{loading ? (
<NonIdealState icon={<Spinner />} title="Loading changesets…" />
) : error ? (
<NonIdealState icon="warning-sign" title="Could not load commits"
description={error} intent={Intent.DANGER} />
) : commits.length === 0 ? (
<NonIdealState icon="git-commit" title="No commits found"
description="Try changing the repo filter or clearing the search." />
) : (
<div className="cs-timeline">
{commits.map(c => (
<div
key={`${c.repoKey}-${c.hash}`}
className="cs-row"
onClick={() => setViewingHash(c.hash)}
>
<div className="cs-row-left">
<Tag
intent={REPO_INTENT[c.repoKey] ?? Intent.NONE}
minimal round
className="cs-repo-badge"
>
{c.repoKey || '?'}
</Tag>
</div>
<div className="cs-row-body">
<div className="cs-row-top">
<Tooltip content="View diff" placement="top">
<code className="opc-commit-hash">{c.shortHash}</code>
</Tooltip>
<span className="cs-subject">{c.subject}</span>
</div>
<div className="cs-row-meta">
{c.author} · {c.date}
{c.files.length > 0 && (
<span className="cs-file-count"> · {c.files.length} file{c.files.length !== 1 ? 's' : ''}</span>
)}
</div>
</div>
</div>
))}
</div>
)}
{/* Pagination */}
{!loading && !error && (commits.length > 0 || page > 1) && (
<div className="cs-pagination">
<Button
icon="arrow-left"
minimal
text="Previous"
disabled={page <= 1}
onClick={() => setPage(p => p - 1)}
/>
<span className="cs-page-label">Page {page}</span>
<Button
rightIcon="arrow-right"
minimal
text="Next"
disabled={!hasMore}
onClick={() => setPage(p => p + 1)}
/>
</div>
)}
<GitCommitDrawer hash={viewingHash} onClose={() => setViewingHash(null)} />
</div>
);
}
@@ -1,4 +1,4 @@
export type TenantTier = 'Shared' | 'Isolated' | 'Dedicated';
export type TenantTier = 'Trial' | 'Shared' | 'Dedicated' | 'Enterprise';
export type TenantEnvironment = 'fdev' | 'uat' | 'prod';
export interface ProvisioningRequest {