feat(frontend): My Reviews queue, duplicate-test badge, tactic order, legend + tooltip fixes
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

- TestsPage gains a "My Reviews" toggle for red_lead/blue_lead, separate
  from "My Tasks" — filters by reviewer_id at the red_review/blue_review
  gate instead of the unrelated in_review validation stage
- Test Catalog cards now warn when a technique already has existing
  tests, using the backend's new existing_test_count
- ExecutiveDashboardPage drops its duplicate client-side tactic-order
  sort now that the backend returns canonical order; time-range filter
  now notes which sections it actually scopes
- HeatmapLegend adds the missing review_required swatch (as an overlay
  indicator, matching how it's actually rendered on cells) and hover
  tooltips reusing StatusBadge's copy instead of being unexplained
  static color chips
- Fix a real React key bug in ControlsTable: rows were grouped in a
  keyless shorthand fragment, which can misreconcile when the table is
  filtered/sorted; switched to a keyed Fragment
- Minor: CompliancePage summary-card grid no longer strands a lone card
  on tablet widths
This commit is contained in:
kitos
2026-07-08 08:46:24 +02:00
parent 1c8fa436ab
commit 8cf8001de5
9 changed files with 125 additions and 43 deletions
+47 -7
View File
@@ -149,6 +149,8 @@ export default function TestsPage() {
const [platformFilter, setPlatformFilter] = useState("");
const [searchText, setSearchText] = useState("");
const [showMyTasks, setShowMyTasks] = useState(false);
const [showMyReviews, setShowMyReviews] = useState(false);
const isReviewLead = user?.role === "red_lead" || user?.role === "blue_lead";
// ── Sort state ────────────────────────────────────────────────────
const [sortKey, setSortKey] = useState<SortKey>("created_at");
@@ -167,7 +169,13 @@ export default function TestsPage() {
const filters = useMemo<TestListFilters>(() => {
const f: TestListFilters = { limit: 200 };
if (showMyTasks && user) {
if (showMyReviews && user && isReviewLead) {
// "My reviews" — tests assigned to ME at the red_review/blue_review
// gate. Distinct from "My Tasks" below, which covers the later
// in_review manager-validation stage regardless of assignment.
f.state = user.role === "red_lead" ? "red_review" : "blue_review";
f.reviewer_id = user.id;
} else if (showMyTasks && user) {
switch (user.role) {
case "red_tech":
f.created_by = user.id;
@@ -192,7 +200,7 @@ export default function TestsPage() {
if (platformFilter) f.platform = platformFilter;
return f;
}, [stateFilter, platformFilter, showMyTasks, user]);
}, [stateFilter, platformFilter, showMyTasks, showMyReviews, isReviewLead, user]);
const {
data: allTests,
@@ -497,7 +505,10 @@ export default function TestsPage() {
<button
onClick={() => {
setShowMyTasks(!showMyTasks);
if (!showMyTasks) setStateFilter("");
if (!showMyTasks) {
setShowMyReviews(false);
setStateFilter("");
}
}}
className={`flex items-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-medium transition-colors ${
showMyTasks
@@ -510,6 +521,29 @@ export default function TestsPage() {
</button>
)}
{/* My reviews toggle — red_lead/blue_lead only, the red_review/blue_review
lead-review gate assignment queue (distinct from "My Tasks" above,
which covers the later in_review manager-validation stage). */}
{isReviewLead && (
<button
onClick={() => {
setShowMyReviews(!showMyReviews);
if (!showMyReviews) {
setShowMyTasks(false);
setStateFilter("");
}
}}
className={`flex items-center gap-1.5 rounded-lg border px-3 py-2 text-sm font-medium transition-colors ${
showMyReviews
? "border-cyan-500/50 bg-cyan-500/20 text-cyan-400"
: "border-gray-700 bg-gray-800 text-gray-300 hover:border-gray-600"
}`}
>
<Eye className="h-4 w-4" />
My Reviews
</button>
)}
{/* State filter */}
<div className="flex items-center gap-1.5">
<Filter className="h-4 w-4 text-gray-500" />
@@ -556,13 +590,14 @@ export default function TestsPage() {
</div>
{/* Clear filters */}
{(stateFilter || platformFilter || searchText || showMyTasks) && (
{(stateFilter || platformFilter || searchText || showMyTasks || showMyReviews) && (
<button
onClick={() => {
setStateFilter("");
setPlatformFilter("");
setSearchText("");
setShowMyTasks(false);
setShowMyReviews(false);
}}
className="text-xs text-gray-400 hover:text-white transition-colors"
>
@@ -572,9 +607,14 @@ export default function TestsPage() {
</div>
{/* Active filter summary */}
{(stateFilter || showMyTasks) && (
{(stateFilter || showMyTasks || showMyReviews) && (
<div className="mt-3 flex items-center gap-2 text-xs text-gray-400">
<span>Showing:</span>
{showMyReviews && (
<span className="rounded-full border border-cyan-500/30 bg-cyan-500/10 px-2 py-0.5 text-cyan-400">
My Reviews
</span>
)}
{showMyTasks && (
<span className="rounded-full border border-cyan-500/30 bg-cyan-500/10 px-2 py-0.5 text-cyan-400">
{myTasksLabel}
@@ -629,11 +669,11 @@ export default function TestsPage() {
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-semibold text-white">
{showMyTasks ? myTasksLabel : "All Tests"}
{showMyReviews ? "My Reviews" : showMyTasks ? myTasksLabel : "All Tests"}
</h2>
<span className="text-sm text-gray-400">{tests.length} tests</span>
</div>
<TestTable tests={tests} columns={mainTableColumns} sortKey={sortKey} sortDir={sortDir} handleSort={handleSort} navigate={navigate} formatDate={formatDate} emptyMessage={showMyTasks ? "No pending tasks for your role." : "No tests found matching your filters."} />
<TestTable tests={tests} columns={mainTableColumns} sortKey={sortKey} sortDir={sortDir} handleSort={handleSort} navigate={navigate} formatDate={formatDate} emptyMessage={showMyReviews ? "No tests currently assigned to you for review." : showMyTasks ? "No pending tasks for your role." : "No tests found matching your filters."} />
</div>
)}
</div>