Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a19cc17
feat: UI improvements across fleet, dashboard, pipelines, and profile
TerrifiedBug Mar 7, 2026
20065ff
fix: invalidate listViews cache on layout save and remove deprecated …
TerrifiedBug Mar 7, 2026
53665c9
fix: resolve stale closure and missing cleanup in layout debounce
TerrifiedBug Mar 7, 2026
7d1919a
fix: move ref update into useEffect and memoize panels cast
TerrifiedBug Mar 7, 2026
d3082df
feat: move service accounts to dedicated settings tab
TerrifiedBug Mar 7, 2026
b62985d
Merge remote-tracking branch 'origin/main' into feat/ui-improvements
TerrifiedBug Mar 7, 2026
3fa3341
chore: add diagnostic logging for OIDC group reconciliation
TerrifiedBug Mar 8, 2026
b7375f1
feat: add debug logger gated behind VF_LOG_LEVEL env var
TerrifiedBug Mar 8, 2026
d134798
ui: adjust padding on dashboard
TerrifiedBug Mar 8, 2026
b5e14a1
ui: move view create button to the far right
TerrifiedBug Mar 8, 2026
6194585
feat: replace audit log enable/disable buttons with toggle switch
TerrifiedBug Mar 8, 2026
490c24c
feat: abbreviate large error and discarded counts with K/M suffixes
TerrifiedBug Mar 8, 2026
ae4fdd0
fix: replace jargon 'pp' with clear '% vs last period' with tooltip
TerrifiedBug Mar 8, 2026
ccf8afe
fix: prevent version history actions column from clipping on small sc…
TerrifiedBug Mar 8, 2026
96909d2
feat: replace SLI health dot with informative badge showing breach count
TerrifiedBug Mar 8, 2026
c03754e
feat: guard manual team assignment for IdP-managed users across all r…
TerrifiedBug Mar 8, 2026
df11cd7
feat: split analytics reduction into Events Reduced and Bytes Saved
TerrifiedBug Mar 8, 2026
5cb64e7
feat: add nodesSnapshot/edgesSnapshot to PipelineVersion schema
TerrifiedBug Mar 8, 2026
079a2ed
feat: add discard changes mutation and UI
TerrifiedBug Mar 8, 2026
a626433
fix: pass node/edge snapshots in deployAgent and wrap rollback restor…
TerrifiedBug Mar 8, 2026
772dd35
feat: show SSO hint in Add Member dialog when SCIM or group sync enabled
TerrifiedBug Mar 8, 2026
e93d1cd
Merge branch 'fix/scim-reconciliation-fixes' into feat/ui-improvements
TerrifiedBug Mar 8, 2026
179d26d
fix: protect against destructive SCIM background sync and add debug l…
TerrifiedBug Mar 8, 2026
1e82080
fix: add ISO timestamps to debug log output
TerrifiedBug Mar 8, 2026
b49a673
merge: resolve conflicts with main (debugLog over console.log)
TerrifiedBug Mar 8, 2026
a34afb3
fix: call markClean() before cache invalidation in discard mutation
TerrifiedBug Mar 8, 2026
7d4f9e6
fix: use format specifiers in debug logger to prevent log injection
TerrifiedBug Mar 8, 2026
eb586a7
fix: escape apostrophe in JSX and remove unused componentKey destructure
TerrifiedBug Mar 8, 2026
d0d323f
Potential fix for code scanning alert no. 8: Log injection
TerrifiedBug Mar 8, 2026
e5d84ee
Update src/app/(dashboard)/analytics/page.tsx
TerrifiedBug Mar 8, 2026
7327729
chore: analytics page wording
TerrifiedBug Mar 8, 2026
3753a7e
fix: add missing closing </p> tag in analytics KPI card and sanitize …
TerrifiedBug Mar 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "PipelineVersion" ADD COLUMN "edgesSnapshot" JSONB,
ADD COLUMN "nodesSnapshot" JSONB;
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,8 @@ model PipelineVersion {
configToml String?
logLevel String?
globalConfig Json?
nodesSnapshot Json?
edgesSnapshot Json?
createdById String
changelog String?
createdAt DateTime @default(now())
Expand Down
107 changes: 90 additions & 17 deletions src/app/(dashboard)/analytics/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import { ArrowUp, ArrowDown, Minus, BarChart3 } from "lucide-react";
import { ArrowUp, ArrowDown, Minus, BarChart3, Info } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Table,
Expand All @@ -23,6 +23,7 @@ import { AreaChart, Area, XAxis, YAxis, CartesianGrid } from "recharts";
import { useEnvironmentStore } from "@/stores/environment-store";
import { formatBytes, formatTimeAxis } from "@/lib/format";
import { cn } from "@/lib/utils";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";

type VolumeRange = "1h" | "6h" | "1d" | "7d" | "30d";

Expand All @@ -41,9 +42,10 @@ interface PipelineRow {
eventsIn: number;
eventsOut: number;
reduction: number;
eventsReduced: number;
}

type SortKey = "pipelineName" | "bytesIn" | "bytesOut" | "reduction";
type SortKey = "pipelineName" | "bytesIn" | "bytesOut" | "reduction" | "eventsReduced";
type SortDir = "asc" | "desc";

export default function AnalyticsPage() {
Expand Down Expand Up @@ -76,6 +78,23 @@ export default function AnalyticsPage() {
? reductionPercent - prevReductionPercent
: null;

// Event-based reduction (matches pipelines table formula, clamped at 0%)
const totalEventsIn = Number(data?.current._sum.eventsIn ?? 0);
const totalEventsOut = Number(data?.current._sum.eventsOut ?? 0);
const eventsReducedPercent = totalEventsIn > 0 ? Math.max(0, (1 - totalEventsOut / totalEventsIn) * 100) : null;

const prevEventsIn = Number(data?.previous._sum.eventsIn ?? 0);
const prevEventsOut = Number(data?.previous._sum.eventsOut ?? 0);
const prevEventsReducedPercent = prevEventsIn > 0 ? Math.max(0, (1 - prevEventsOut / prevEventsIn) * 100) : null;
const eventsReducedDelta =
eventsReducedPercent != null && prevEventsReducedPercent != null
? eventsReducedPercent - prevEventsReducedPercent
: null;

// Rename bytes vars for clarity
const bytesSavedPercent = reductionPercent;
const bytesSavedDelta = reductionDelta;

const bytesInTrend = trendPercent(totalBytesIn, prevBytesIn);
const bytesOutTrend = trendPercent(totalBytesOut, prevBytesOut);

Expand All @@ -96,9 +115,10 @@ export default function AnalyticsPage() {
// Per-pipeline table with sorting
const sortedPipelines = (() => {
if (!data?.perPipeline) return [];
const rows: PipelineRow[] = data.perPipeline.map((p: Omit<PipelineRow, "reduction">) => ({
const rows: PipelineRow[] = data.perPipeline.map((p: Omit<PipelineRow, "reduction" | "eventsReduced">) => ({
...p,
reduction: p.bytesIn > 0 ? (1 - p.bytesOut / p.bytesIn) * 100 : 0,
eventsReduced: p.eventsIn > 0 ? Math.max(0, (1 - p.eventsOut / p.eventsIn) * 100) : 0,
}));
return rows.sort((a: PipelineRow, b: PipelineRow) => {
const aVal = a[sortKey];
Expand Down Expand Up @@ -166,7 +186,7 @@ export default function AnalyticsPage() {
</div>

{/* KPI Cards */}
<div className="grid gap-4 md:grid-cols-3">
<div className="grid gap-4 md:grid-cols-4">
{/* Total In */}
<Card>
<CardContent className="p-4">
Expand Down Expand Up @@ -205,31 +225,58 @@ export default function AnalyticsPage() {
</CardContent>
</Card>

{/* Reduction % */}
{/* Events Reduced */}
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-muted-foreground">Reduction</p>
<TrendArrow value={reductionDelta} invertColor />
<p className="text-sm font-medium text-muted-foreground">Events Reduced</p>
<TrendArrow value={eventsReducedDelta} invertColor />
</div>
<p
className={cn(
"mt-1 text-2xl font-bold",
reductionPercent != null && reductionPercent >= 50
eventsReducedPercent != null && eventsReducedPercent > 50
? "text-green-600 dark:text-green-400"
: reductionPercent != null && reductionPercent >= 20
: eventsReducedPercent != null && eventsReducedPercent > 10
? "text-amber-600 dark:text-amber-400"
: reductionPercent != null
? "text-red-600 dark:text-red-400"
: "text-muted-foreground",
: "text-muted-foreground",
)}
>
{reductionPercent != null ? `${reductionPercent.toFixed(1)}%` : "--"}
{eventsReducedPercent != null ? `${eventsReducedPercent.toFixed(1)}%` : "--"}
</p>
{reductionDelta != null && (
{eventsReducedDelta != null && (
<p className="text-xs text-muted-foreground">
{reductionDelta >= 0 ? "+" : ""}
{reductionDelta.toFixed(1)} pp vs previous period
{eventsReducedDelta >= 0 ? "+" : ""}
{eventsReducedDelta.toFixed(1)} pp vs last period
</p>
)}
</CardContent>
</Card>

{/* Bytes Saved */}
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-sm font-medium text-muted-foreground">Bytes Saved</p>
<Tooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
Total bytes saved including sink compression and encoding
</TooltipContent>
</Tooltip>
</div>
<TrendArrow value={bytesSavedDelta} invertColor />
</div>
<p className="mt-1 text-2xl font-bold text-muted-foreground">
{bytesSavedPercent != null ? `${bytesSavedPercent.toFixed(1)}%` : "--"}
</p>
{bytesSavedDelta != null && (
<p className="text-xs text-muted-foreground">
{bytesSavedDelta >= 0 ? "+" : ""}
{bytesSavedDelta.toFixed(1)} pp vs last period
</p>
)}
</CardContent>
Expand Down Expand Up @@ -349,11 +396,17 @@ export default function AnalyticsPage() {
>
Bytes Out{sortIndicator("bytesOut")}
</TableHead>
<TableHead
className="cursor-pointer select-none text-right"
onClick={() => toggleSort("eventsReduced")}
>
Events Reduced{sortIndicator("eventsReduced")}
</TableHead>
<TableHead
className="cursor-pointer select-none text-right"
onClick={() => toggleSort("reduction")}
>
Reduction %{sortIndicator("reduction")}
Bytes Saved{sortIndicator("reduction")}
</TableHead>
</TableRow>
</TableHeader>
Expand All @@ -367,6 +420,26 @@ export default function AnalyticsPage() {
<TableCell className="text-right font-mono">
{formatBytes(p.bytesOut)}
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<div className="h-2 w-16 rounded-full bg-muted overflow-hidden">
<div
className={cn(
"h-full rounded-full transition-all",
p.eventsReduced > 50
? "bg-green-500"
: p.eventsReduced > 10
? "bg-amber-500"
: "bg-muted-foreground/30",
)}
style={{ width: `${Math.max(0, Math.min(100, p.eventsReduced))}%` }}
/>
</div>
<span className="font-mono text-sm w-14 text-right">
{p.eventsReduced.toFixed(1)}%
</span>
</div>
</TableCell>
<TableCell className="text-right">
<div className="flex items-center justify-end gap-2">
<div className="h-2 w-16 rounded-full bg-muted overflow-hidden">
Expand Down
2 changes: 1 addition & 1 deletion src/app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default function DashboardLayout({
</header>
<ChangePasswordDialog open={passwordDialogOpen} onOpenChange={setPasswordDialogOpen} forced={me?.mustChangePassword} />
<UpdateBanner />
<div className="flex-1 p-6">
<div className="flex-1 py-2 px-6">
<ErrorBoundary>
{children}
</ErrorBoundary>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export default function DashboardPage() {
<button
type="button"
onClick={() => setCreateDialogOpen(true)}
className="shrink-0 flex items-center gap-1 px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
className="ml-auto shrink-0 flex items-center gap-1 px-3 py-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
<Plus className="h-3.5 w-3.5" />
New View
Expand Down
39 changes: 39 additions & 0 deletions src/app/(dashboard)/pipelines/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) {
const [templateOpen, setTemplateOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [undeployOpen, setUndeployOpen] = useState(false);
const [discardOpen, setDiscardOpen] = useState(false);
const [metricsOpen, setMetricsOpen] = useState(false);
const [logsOpen, setLogsOpen] = useState(false);

Expand Down Expand Up @@ -239,6 +240,21 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) {
})
);

// Discard changes mutation
const discardMutation = useMutation(
trpc.pipeline.discardChanges.mutationOptions({
onSuccess: () => {
markClean();
queryClient.invalidateQueries({ queryKey: trpc.pipeline.get.queryKey() });
toast.success("Changes discarded — restored to last deployed state");
setDiscardOpen(false);
},
onError: (err) => {
toast.error("Failed to discard changes", { description: err.message });
},
})
);

// Rename state
const [isRenaming, setIsRenaming] = useState(false);
const [renameValue, setRenameValue] = useState("");
Expand Down Expand Up @@ -390,6 +406,7 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) {
: null
}
gitOpsMode={pipelineQuery.data?.gitOpsMode}
onDiscardChanges={() => setDiscardOpen(true)}
/>
</div>
<div className="flex items-center px-3">
Expand Down Expand Up @@ -459,6 +476,28 @@ function PipelineBuilderInner({ pipelineId }: { pipelineId: string }) {
setUndeployOpen(false);
}}
/>
<Dialog open={discardOpen} onOpenChange={setDiscardOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Discard unsaved changes?</DialogTitle>
<DialogDescription>
This will revert the pipeline to its last deployed state. Any saved changes that haven&apos;t been deployed will be lost.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setDiscardOpen(false)}>
Cancel
</Button>
<Button
variant="destructive"
disabled={discardMutation.isPending}
onClick={() => discardMutation.mutate({ pipelineId })}
>
{discardMutation.isPending ? "Discarding..." : "Discard changes"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}
Expand Down
59 changes: 23 additions & 36 deletions src/app/(dashboard)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
AlertTriangle,
Clock,
Link2,
Info,
} from "lucide-react";

import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -343,42 +344,22 @@ function AuditLogShippingSection() {
Configure sinks
</Link>
</Button>
{isDeployed ? (
<Button
variant="outline"
size="sm"
onClick={() =>
undeployMutation.mutate({ pipelineId: systemPipeline.id })
}
disabled={isToggling}
>
{undeployMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Disabling...
</>
) : (
"Disable"
)}
</Button>
) : (
<Button
size="sm"
onClick={() =>
deployMutation.mutate({ pipelineId: systemPipeline.id, changelog: "Enabled system pipeline from settings" })
}
<div className="flex items-center gap-2">
<Switch
checked={!!isDeployed}
onCheckedChange={(checked) => {
if (checked) {
deployMutation.mutate({ pipelineId: systemPipeline.id, changelog: "Enabled system pipeline from settings" });
} else {
undeployMutation.mutate({ pipelineId: systemPipeline.id });
}
}}
disabled={isToggling}
>
{deployMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Enabling...
</>
) : (
"Enable"
)}
</Button>
)}
/>
<span className="text-sm text-muted-foreground">
{isToggling ? (isDeployed ? "Disabling..." : "Enabling...") : (isDeployed ? "Active" : "Disabled")}
</span>
</div>
</div>
) : (
<div className="flex items-center gap-3">
Expand Down Expand Up @@ -1664,7 +1645,13 @@ function TeamSettings() {
Add an existing user to the team by their email address.
</CardDescription>
</CardHeader>
<CardContent>
<CardContent className="space-y-4">
{(settingsQuery.data?.scimEnabled || settingsQuery.data?.oidcGroupSyncEnabled) && (
<div className="flex items-start gap-2 rounded-md border border-blue-200 bg-blue-50 p-3 text-sm text-blue-800 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-300">
<Info className="h-4 w-4 mt-0.5 shrink-0" />
<span>SSO users are managed by your identity provider. Only local users can be added manually.</span>
</div>
)}
<form onSubmit={handleInvite} className="grid grid-cols-[1fr_120px_auto] items-end gap-3">
<div className="flex flex-col gap-2">
<Label htmlFor="invite-email">Email</Label>
Expand Down
Loading
Loading