feat: add React UI for rpm.lair.cafe
- Vite + React + SWC + TypeScript SPA with react-router and react-bootstrap - Dark/light/system theme with Bootstrap 5.3 data-bs-theme - Home page with repo setup instructions and copyable code blocks - Package list and detail pages driven by packages.json - Python script to generate packages.json from repodata XML - Nginx config updated for SPA fallback, asset caching, removed autoindex - New deploy-ui workflow triggered on ui/ or nginx config changes, requires runners with nvm label - packages.json generation added to publish job after createrepo_c - Runner setup docs for nvm and sequoia-sq added to readme Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
49
ui/src/pages/Home.tsx
Normal file
49
ui/src/pages/Home.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Card, Col, Row } from "react-bootstrap";
|
||||
import { CodeBlock } from "../components/CodeBlock.tsx";
|
||||
|
||||
const GPG_KEY_URL = "https://rpm.lair.cafe/8b2023ce.gpg";
|
||||
|
||||
const REPO_FILE = `[lair-cafe]
|
||||
name=lair.cafe RPM Repository
|
||||
baseurl=https://rpm.lair.cafe/fedora/$releasever/$basearch/
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgkey=${GPG_KEY_URL}`;
|
||||
|
||||
export function Home() {
|
||||
return (
|
||||
<>
|
||||
<h1 className="mb-3">rpm.lair.cafe</h1>
|
||||
<p className="lead mb-4">
|
||||
Self-hosted RPM repository for Fedora, currently hosting CUDA-accelerated
|
||||
builds of{" "}
|
||||
<a href="https://github.com/EricLBuehler/mistral.rs">mistral.rs</a>.
|
||||
</p>
|
||||
|
||||
<Row className="g-4">
|
||||
<Col lg={12}>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title>Quick start</Card.Title>
|
||||
|
||||
<h6 className="mt-4">1. Import the signing key</h6>
|
||||
<CodeBlock language="bash">
|
||||
{`sudo rpm --import ${GPG_KEY_URL}`}
|
||||
</CodeBlock>
|
||||
|
||||
<h6 className="mt-4">2. Add the repository</h6>
|
||||
<CodeBlock language="bash">
|
||||
{`sudo dnf config-manager addrepo --from-repofile=/dev/stdin <<'EOF'\n${REPO_FILE}\nEOF`}
|
||||
</CodeBlock>
|
||||
|
||||
<h6 className="mt-4">3. Install a package</h6>
|
||||
<CodeBlock language="bash">
|
||||
{`sudo dnf install mistralrs-server-cuda13`}
|
||||
</CodeBlock>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
100
ui/src/pages/PackageDetail.tsx
Normal file
100
ui/src/pages/PackageDetail.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { Accordion, Alert, Badge, Spinner, Table } from "react-bootstrap";
|
||||
import { useParams } from "react-router";
|
||||
import { usePackages } from "../hooks/usePackages.ts";
|
||||
import { CodeBlock } from "../components/CodeBlock.tsx";
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export function PackageDetail() {
|
||||
const { name } = useParams<{ name: string }>();
|
||||
const { manifest, loading, error } = usePackages();
|
||||
|
||||
if (loading) return <Spinner animation="border" />;
|
||||
if (error) return <Alert variant="danger">Failed to load packages: {error}</Alert>;
|
||||
if (!manifest) return <Alert variant="info">No package data available.</Alert>;
|
||||
|
||||
const versions = manifest.packages
|
||||
.filter((p) => p.name === name)
|
||||
.sort((a, b) => b.buildTime - a.buildTime);
|
||||
|
||||
if (versions.length === 0)
|
||||
return <Alert variant="warning">Package not found: {name}</Alert>;
|
||||
|
||||
const latest = versions[0];
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="mb-1">{name}</h1>
|
||||
<p className="text-body-secondary mb-4">{latest.summary}</p>
|
||||
|
||||
<CodeBlock language="bash">{`sudo dnf install ${name}`}</CodeBlock>
|
||||
|
||||
<h2 className="mt-4 mb-3">
|
||||
Versions <Badge bg="secondary">{versions.length}</Badge>
|
||||
</h2>
|
||||
|
||||
<Table striped hover responsive>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Size</th>
|
||||
<th>Built</th>
|
||||
<th>Download</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{versions.map((pkg) => (
|
||||
<tr key={`${pkg.version}-${pkg.release}`}>
|
||||
<td>
|
||||
{pkg.version}-{pkg.release}
|
||||
</td>
|
||||
<td>{formatBytes(pkg.size)}</td>
|
||||
<td>{new Date(pkg.buildTime * 1000).toLocaleDateString()}</td>
|
||||
<td>
|
||||
<a href={`${manifest.baseUrl}/${pkg.rpmFilename}`}>
|
||||
{pkg.rpmFilename}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
{versions.some((v) => v.changelog.length > 0) && (
|
||||
<>
|
||||
<h2 className="mt-4 mb-3">Changelog</h2>
|
||||
<Accordion>
|
||||
{versions
|
||||
.filter((v) => v.changelog.length > 0)
|
||||
.map((pkg) => (
|
||||
<Accordion.Item
|
||||
key={`${pkg.version}-${pkg.release}`}
|
||||
eventKey={`${pkg.version}-${pkg.release}`}
|
||||
>
|
||||
<Accordion.Header>
|
||||
{pkg.version}-{pkg.release} —{" "}
|
||||
{new Date(pkg.buildTime * 1000).toLocaleDateString()}
|
||||
</Accordion.Header>
|
||||
<Accordion.Body>
|
||||
{pkg.changelog.map((entry, i) => (
|
||||
<div key={i} className="mb-3">
|
||||
<small className="text-body-secondary">
|
||||
{new Date(entry.date * 1000).toLocaleDateString()}{" "}
|
||||
— {entry.author}
|
||||
</small>
|
||||
<pre className="mb-0 mt-1">{entry.text}</pre>
|
||||
</div>
|
||||
))}
|
||||
</Accordion.Body>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
</Accordion>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
53
ui/src/pages/PackageList.tsx
Normal file
53
ui/src/pages/PackageList.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Alert, Spinner, Table } from "react-bootstrap";
|
||||
import { Link } from "react-router";
|
||||
import { usePackages } from "../hooks/usePackages.ts";
|
||||
|
||||
export function PackageList() {
|
||||
const { manifest, loading, error } = usePackages();
|
||||
|
||||
if (loading) return <Spinner animation="border" />;
|
||||
if (error) return <Alert variant="danger">Failed to load packages: {error}</Alert>;
|
||||
if (!manifest || manifest.packages.length === 0)
|
||||
return <Alert variant="info">No packages published yet.</Alert>;
|
||||
|
||||
const byName = Map.groupBy(manifest.packages, (p) => p.name);
|
||||
const summaries = [...byName.entries()].map(([name, versions]) => {
|
||||
const latest = versions.reduce((a, b) =>
|
||||
a.buildTime >= b.buildTime ? a : b,
|
||||
);
|
||||
return { name, latest, versionCount: versions.length };
|
||||
});
|
||||
summaries.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="mb-3">Packages</h1>
|
||||
<Table striped hover responsive>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Package</th>
|
||||
<th>Latest version</th>
|
||||
<th>Versions</th>
|
||||
<th>Summary</th>
|
||||
<th>Built</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{summaries.map(({ name, latest, versionCount }) => (
|
||||
<tr key={name}>
|
||||
<td>
|
||||
<Link to={`/packages/${name}`}>{name}</Link>
|
||||
</td>
|
||||
<td>
|
||||
{latest.version}-{latest.release}
|
||||
</td>
|
||||
<td>{versionCount}</td>
|
||||
<td>{latest.summary}</td>
|
||||
<td>{new Date(latest.buildTime * 1000).toLocaleDateString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user