|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import { useState, useMemo, useCallback } from "react"; |
| 3 | +import { useState, useMemo, useCallback, useRef, useEffect } from "react"; |
4 | 4 | import Link from "next/link"; |
| 5 | +import { useRouter } from "next/navigation"; |
5 | 6 | import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; |
6 | 7 | import { useTRPC } from "@/trpc/client"; |
7 | 8 | import { useEnvironmentStore } from "@/stores/environment-store"; |
8 | 9 | import { useTeamStore } from "@/stores/team-store"; |
| 10 | +import { useMatrixFilters } from "@/hooks/use-matrix-filters"; |
| 11 | +import { DeploymentMatrixToolbar } from "@/components/fleet/DeploymentMatrixToolbar"; |
| 12 | +import { aggregateProcessStatus } from "@/lib/pipeline-status"; |
9 | 13 |
|
10 | 14 | import { Badge } from "@/components/ui/badge"; |
11 | 15 | import { Button } from "@/components/ui/button"; |
@@ -63,6 +67,76 @@ export default function FleetPage() { |
63 | 67 | // Pick the first environment if none is selected yet |
64 | 68 | const activeEnvId = selectedEnvironmentId || environments[0]?.id || ""; |
65 | 69 |
|
| 70 | + const router = useRouter(); |
| 71 | + |
| 72 | + // --- Matrix filter state (URL-synced) --- |
| 73 | + const { |
| 74 | + search: matrixSearch, |
| 75 | + statusFilter: matrixStatusFilter, |
| 76 | + tagFilter: matrixTagFilter, |
| 77 | + hasActiveFilters: matrixHasActiveFilters, |
| 78 | + setSearch: setMatrixSearch, |
| 79 | + setStatusFilter: setMatrixStatusFilter, |
| 80 | + setTagFilter: setMatrixTagFilter, |
| 81 | + } = useMatrixFilters(); |
| 82 | + |
| 83 | + // Same query as DeploymentMatrix — React Query deduplicates by key |
| 84 | + const matrixQuery = useQuery({ |
| 85 | + ...trpc.fleet.listWithPipelineStatus.queryOptions({ environmentId: activeEnvId }), |
| 86 | + enabled: !!activeEnvId, |
| 87 | + }); |
| 88 | + |
| 89 | + // Derive available tags from all deployed pipelines |
| 90 | + const availableTags = useMemo(() => { |
| 91 | + const tagSet = new Set<string>(); |
| 92 | + for (const p of matrixQuery.data?.deployedPipelines ?? []) { |
| 93 | + for (const t of (p.tags as string[]) ?? []) { |
| 94 | + tagSet.add(t); |
| 95 | + } |
| 96 | + } |
| 97 | + return [...tagSet].sort(); |
| 98 | + }, [matrixQuery.data?.deployedPipelines]); |
| 99 | + |
| 100 | + // Compute filtered pipelines with AND logic across search, status, and tag filters |
| 101 | + const filteredDeployedPipelines = useMemo(() => { |
| 102 | + let result = matrixQuery.data?.deployedPipelines ?? []; |
| 103 | + const nodes = matrixQuery.data?.nodes ?? []; |
| 104 | + |
| 105 | + if (matrixSearch) { |
| 106 | + const lc = matrixSearch.toLowerCase(); |
| 107 | + result = result.filter((p) => p.name.toLowerCase().includes(lc)); |
| 108 | + } |
| 109 | + |
| 110 | + if (matrixStatusFilter.length > 0) { |
| 111 | + result = result.filter((p) => { |
| 112 | + const nodeStatuses = nodes.flatMap((n) => |
| 113 | + n.pipelineStatuses.filter((s) => s.pipelineId === p.id), |
| 114 | + ); |
| 115 | + const agg = aggregateProcessStatus(nodeStatuses); |
| 116 | + // statusFilter values are PascalCase ("Running"), agg is SCREAMING_SNAKE ("RUNNING") |
| 117 | + return agg !== null && matrixStatusFilter.map((s) => s.toUpperCase()).includes(agg); |
| 118 | + }); |
| 119 | + } |
| 120 | + |
| 121 | + if (matrixTagFilter.length > 0) { |
| 122 | + result = result.filter((p) => { |
| 123 | + const pTags = (p.tags as string[]) ?? []; |
| 124 | + return matrixTagFilter.some((t) => pTags.includes(t)); |
| 125 | + }); |
| 126 | + } |
| 127 | + |
| 128 | + return result; |
| 129 | + }, [matrixQuery.data, matrixSearch, matrixStatusFilter, matrixTagFilter]); |
| 130 | + |
| 131 | + // Clear all matrix filters when environment changes (D-07) |
| 132 | + const prevEnvRef = useRef(activeEnvId); |
| 133 | + useEffect(() => { |
| 134 | + if (prevEnvRef.current !== activeEnvId) { |
| 135 | + prevEnvRef.current = activeEnvId; |
| 136 | + router.replace("/fleet", { scroll: false }); |
| 137 | + } |
| 138 | + }, [activeEnvId, router]); |
| 139 | + |
66 | 140 | // --- Filter state --- |
67 | 141 | const [search, setSearch] = useState(""); |
68 | 142 | const [statusFilter, setStatusFilter] = useState<string[]>([]); |
@@ -464,7 +538,22 @@ export default function FleetPage() { |
464 | 538 | {activeEnvId && ( |
465 | 539 | <div className="space-y-4"> |
466 | 540 | <h3 className="text-lg font-semibold">Pipeline Deployment Matrix</h3> |
467 | | - <DeploymentMatrix environmentId={activeEnvId} /> |
| 541 | + {matrixQuery.data && ( |
| 542 | + <DeploymentMatrixToolbar |
| 543 | + search={matrixSearch} |
| 544 | + onSearchChange={setMatrixSearch} |
| 545 | + statusFilter={matrixStatusFilter} |
| 546 | + onStatusFilterChange={setMatrixStatusFilter} |
| 547 | + tagFilter={matrixTagFilter} |
| 548 | + onTagFilterChange={setMatrixTagFilter} |
| 549 | + availableTags={availableTags} |
| 550 | + /> |
| 551 | + )} |
| 552 | + <DeploymentMatrix |
| 553 | + environmentId={activeEnvId} |
| 554 | + filteredPipelines={matrixQuery.data ? filteredDeployedPipelines : undefined} |
| 555 | + hasActiveFilters={matrixHasActiveFilters} |
| 556 | + /> |
468 | 557 | </div> |
469 | 558 | )} |
470 | 559 |
|
|
0 commit comments