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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user