fix(jira): expose non-admin jira-config, drop security-test label; catalog paging

GET /system/jira-config was admin-only, so non-admin users hit a 403
and the frontend silently fell back to a hardcoded jira.atlassian.com
base URL when building /browse/{key} links. Opened it to any
authenticated user (still no secrets returned).

Also drops the redundant 'security-test' Jira label, and adds a
page-size selector (10/25/50/100) and a 'no existing test yet' filter
to the Test Catalog.
This commit is contained in:
kitos
2026-07-10 10:24:13 +02:00
parent 2afb886cd9
commit f32f636f05
5 changed files with 77 additions and 17 deletions
+48 -11
View File
@@ -19,7 +19,8 @@ import { useAuth } from "../context/AuthContext";
// ── Constants ──────────────────────────────────────────────────────
const PAGE_SIZE = 12;
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
const DEFAULT_PAGE_SIZE = 25;
const SOURCE_OPTIONS = [
{ value: "", label: "All Sources" },
@@ -89,7 +90,9 @@ export default function TestCatalogPage() {
const [source, setSource] = useState(searchParams.get("source") || "");
const [platform, setPlatform] = useState(searchParams.get("platform") || "");
const [severity, setSeverity] = useState(searchParams.get("severity") || "");
const [hideCovered, setHideCovered] = useState(searchParams.get("hide_covered") === "1");
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
// Build filters
const filters = {
@@ -97,15 +100,21 @@ export default function TestCatalogPage() {
source: source || undefined,
platform: platform || undefined,
severity: severity || undefined,
offset: page * PAGE_SIZE,
limit: PAGE_SIZE,
offset: page * pageSize,
limit: pageSize,
};
const { data: templates = [], isLoading } = useQuery({
const { data: allTemplates = [], isLoading } = useQuery({
queryKey: ["test-templates", filters],
queryFn: () => getTemplates(filters),
});
// "No tests yet" is a client-side filter — existing_test_count is already
// returned per-template, no need for a separate backend round-trip.
const templates = hideCovered
? allTemplates.filter((t) => t.existing_test_count === 0)
: allTemplates;
// ── Filter handlers ──────────────────────────────────────────────
const applyFilters = () => {
@@ -115,6 +124,7 @@ export default function TestCatalogPage() {
if (source) params.set("source", source);
if (platform) params.set("platform", platform);
if (severity) params.set("severity", severity);
if (hideCovered) params.set("hide_covered", "1");
setSearchParams(params);
};
@@ -123,11 +133,12 @@ export default function TestCatalogPage() {
setSource("");
setPlatform("");
setSeverity("");
setHideCovered(false);
setPage(0);
setSearchParams({});
};
const hasActiveFilters = search || source || platform || severity;
const hasActiveFilters = search || source || platform || severity || hideCovered;
// ── Render ───────────────────────────────────────────────────────
@@ -220,6 +231,17 @@ export default function TestCatalogPage() {
</button>
)}
</div>
{/* No-coverage-yet toggle */}
<label className="mt-3 flex w-fit items-center gap-2 text-sm text-gray-400">
<input
type="checkbox"
checked={hideCovered}
onChange={(e) => { setHideCovered(e.target.checked); setPage(0); }}
className="h-4 w-4 rounded border-gray-600 bg-gray-800 text-cyan-500 focus:ring-cyan-500"
/>
Only show techniques with no existing test
</label>
</div>
{/* Results */}
@@ -253,11 +275,26 @@ export default function TestCatalogPage() {
{/* Pagination */}
<div className="flex items-center justify-between">
<p className="text-sm text-gray-500">
Showing {page * PAGE_SIZE + 1}
{" - "}
{page * PAGE_SIZE + templates.length}
</p>
<div className="flex items-center gap-3">
<p className="text-sm text-gray-500">
Showing {page * pageSize + 1}
{" - "}
{page * pageSize + allTemplates.length}
{hideCovered && templates.length !== allTemplates.length && ` (${templates.length} without existing tests)`}
</p>
<label className="flex items-center gap-1.5 text-sm text-gray-500">
Show
<select
value={pageSize}
onChange={(e) => { setPageSize(Number(e.target.value)); setPage(0); }}
className="rounded-lg border border-gray-700 bg-gray-800 px-2 py-1 text-sm text-gray-200 focus:border-cyan-500 focus:outline-none"
>
{PAGE_SIZE_OPTIONS.map((n) => (
<option key={n} value={n}>{n}</option>
))}
</select>
</label>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setPage((p) => Math.max(0, p - 1))}
@@ -270,7 +307,7 @@ export default function TestCatalogPage() {
<span className="text-sm text-gray-400">Page {page + 1}</span>
<button
onClick={() => setPage((p) => p + 1)}
disabled={templates.length < PAGE_SIZE}
disabled={allTemplates.length < pageSize}
className="flex items-center gap-1 rounded-lg border border-gray-700 px-3 py-1.5 text-sm text-gray-400 hover:bg-gray-800 disabled:opacity-40 transition-colors"
>
Next