OPC # 0001: Gitea services
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user