Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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,157 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { useState } from "react";
import { ChevronDown, ChevronUp, Plus, Minus, RefreshCcw } from "lucide-react";
import type { InstrumentationDiff } from "../../utils/release-diff";
import { DiffResultsSection } from "../telemetry-comparison/diff-results-section";
import { GlowBadge } from "@/components/ui/glow-badge";

const STATUS_CONFIG: Record<
InstrumentationDiff["status"],
{ className: string; icon: React.ReactNode }
> = {
added: {
className: "border-green-400/30 bg-green-400/10 text-green-400",
icon: <Plus className="h-4 w-4" />,
},
removed: {
className: "border-red-400/30 bg-red-400/10 text-red-400",
icon: <Minus className="h-4 w-4" />,
},
changed: {
className: "border-blue-400/30 bg-blue-400/10 text-blue-400",
icon: <RefreshCcw className="h-4 w-4" />,
},
unchanged: {
className: "border-border/50 bg-muted/20 text-muted-foreground",
icon: <div className="h-2 w-2 rounded-full bg-current" />,
},
};

interface InstrumentationDiffCardProps {
diff: InstrumentationDiff;
}

export function InstrumentationDiffCard({ diff }: InstrumentationDiffCardProps) {
const [isExpanded, setIsExpanded] = useState(false);

const changedMetricsCount = diff.telemetryDiff.metrics.filter(
(m) => m.status !== "unchanged"
).length;
const changedSpansCount = diff.telemetryDiff.spans.filter((s) => s.status !== "unchanged").length;
const configChangesCount =
(diff.configDiff?.added.length || 0) +
(diff.configDiff?.removed.length || 0) +
(diff.configDiff?.changed.length || 0);

const statusInfo = STATUS_CONFIG[diff.status];

return (
<div className="border-border/30 bg-card/20 overflow-hidden rounded-xl border transition-all duration-200">
<button
onClick={() => setIsExpanded(!isExpanded)}
aria-expanded={isExpanded}
className="flex w-full items-center justify-between p-4 text-left transition-colors hover:bg-white/5"
>
<div className="flex items-center gap-4">
<div
className={`flex h-8 w-8 items-center justify-center rounded-lg border ${statusInfo.className}`}
>
{statusInfo.icon}
</div>
<div>
<h3 className="text-sm font-semibold">{diff.displayName}</h3>
<p className="text-muted-foreground text-xs">{diff.id}</p>
</div>
</div>

<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
{changedMetricsCount > 0 && (
<GlowBadge variant="success">
{changedMetricsCount} Metric{changedMetricsCount > 1 ? "s" : ""}
</GlowBadge>
)}
{changedSpansCount > 0 && (
<GlowBadge variant="info">
{changedSpansCount} Span{changedSpansCount > 1 ? "s" : ""}
</GlowBadge>
)}
{configChangesCount > 0 && (
<GlowBadge variant="warning">
{configChangesCount} Config{configChangesCount > 1 ? "s" : ""}
</GlowBadge>
)}
</div>
{isExpanded ? (
<ChevronUp className="text-muted-foreground h-5 w-5" />
) : (
<ChevronDown className="text-muted-foreground h-5 w-5" />
)}
</div>
</button>

{isExpanded && (
<div className="border-border/30 animate-in fade-in slide-in-from-top-2 space-y-8 border-t p-6 duration-200">
{configChangesCount > 0 && (
<div className="space-y-4">
<div className="flex items-center justify-center gap-4">
<div className="to-border h-px w-16 bg-gradient-to-r from-transparent" />
<span className="text-muted-foreground text-sm font-medium tracking-wider uppercase">
Configuration Changes
</span>
<div className="to-border h-px w-16 bg-gradient-to-l from-transparent" />
</div>

<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{diff.configDiff?.added.map((name) => (
<div
key={name}
className="flex items-center gap-2 rounded-lg border border-green-400/20 bg-green-400/5 p-3"
>
<Plus className="h-4 w-4 text-green-400" />
<code className="text-foreground/90 font-mono text-xs">{name}</code>
</div>
))}
{diff.configDiff?.changed.map((name) => (
<div
key={name}
className="flex items-center gap-2 rounded-lg border border-blue-400/20 bg-blue-400/5 p-3"
>
<RefreshCcw className="h-4 w-4 text-blue-400" />
<code className="text-foreground/90 font-mono text-xs">{name}</code>
</div>
))}
{diff.configDiff?.removed.map((name) => (
<div
key={name}
className="flex items-center gap-2 rounded-lg border border-red-400/20 bg-red-400/5 p-3"
>
<Minus className="h-4 w-4 text-red-400" />
<code className="text-foreground/90 font-mono text-xs">{name}</code>
</div>
))}
</div>
</div>
)}

<DiffResultsSection diffResult={diff.telemetryDiff} />
</div>
)}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Info } from "lucide-react";
import type { VersionInfo } from "@/types/javaagent";

interface ReleaseVersionSelectorProps {
versions: VersionInfo[];
fromVersion: string;
toVersion: string;
onFromVersionChange: (version: string) => void;
onToVersionChange: (version: string) => void;
}

export function ReleaseVersionSelector({
versions,
fromVersion,
toVersion,
onFromVersionChange,
onToVersionChange,
}: ReleaseVersionSelectorProps) {
return (
<div className="mx-auto max-w-4xl">
<div className="border-border/30 bg-card/40 flex flex-col gap-6 rounded-xl border p-6 shadow-sm backdrop-blur-sm">
<div className="bg-secondary/10 border-secondary/20 flex w-fit items-center gap-2 rounded-lg border px-3 py-2">
<Info className="text-secondary h-4 w-4" aria-hidden="true" />
<span className="text-foreground/90 text-xs font-medium">
Select versions to compare Java Agent changes
</span>
</div>

<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-3">
<label
htmlFor="from-version-select"
className="bg-muted/50 text-foreground/70 block w-fit rounded-md px-3 py-1.5 text-[10px] font-bold tracking-widest uppercase"
>
From Version
</label>
<select
id="from-version-select"
value={fromVersion}
onChange={(e) => onFromVersionChange(e.target.value)}
className="border-primary/20 bg-primary/5 text-foreground hover:border-primary/40 hover:bg-primary/10 focus:ring-primary/50 focus:border-primary/50 w-full cursor-pointer rounded-lg border-2 px-4 py-2.5 text-sm font-medium shadow-sm transition-all duration-200 hover:shadow-md focus:ring-2 focus:outline-none"
>
{versions.map((v) => (
<option key={v.version} value={v.version}>
{v.version} {v.is_latest ? "(latest)" : ""}
</option>
))}
</select>
</div>

<div className="space-y-3">
<label
htmlFor="to-version-select"
className="bg-muted/50 text-foreground/70 block w-fit rounded-md px-3 py-1.5 text-[10px] font-bold tracking-widest uppercase"
>
To Version
</label>
<select
id="to-version-select"
value={toVersion}
onChange={(e) => onToVersionChange(e.target.value)}
className="border-primary/20 bg-primary/5 text-foreground hover:border-primary/40 hover:bg-primary/10 focus:ring-primary/50 focus:border-primary/50 w-full cursor-pointer rounded-lg border-2 px-4 py-2.5 text-sm font-medium shadow-sm transition-all duration-200 hover:shadow-md focus:ring-2 focus:outline-none"
>
{versions.map((v) => (
<option key={v.version} value={v.version}>
{v.version} {v.is_latest ? "(latest)" : ""}
</option>
))}
</select>
</div>
</div>
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright The OpenTelemetry Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { useState, useEffect } from "react";
import * as javaagentData from "@/lib/api/javaagent-data";
import { compareReleases, type ReleaseDiff } from "../utils/release-diff";

/**
* Custom hook to fetch instrumentation data for two Java Agent versions and compute the difference.
*
* @param fromVersion The base version for comparison
* @param toVersion The target version for comparison
* @returns An object containing the diff results, loading state, and any error encountered
*/
export function useReleaseComparison(
fromVersion: string,
toVersion: string,
validVersions: string[] = []
) {
const [diff, setDiff] = useState<ReleaseDiff | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);

useEffect(() => {
let cancelled = false;

async function loadComparison() {
if (!fromVersion || !toVersion) {
setDiff(null);
setLoading(false);
setError(null);
return;
}
if (fromVersion === toVersion) {
setDiff(null);
setLoading(false);
setError(null);
return;
}

if (validVersions.length > 0) {
const fromIndex = validVersions.indexOf(fromVersion);
const toIndex = validVersions.indexOf(toVersion);
if (fromIndex === -1 || toIndex === -1 || fromIndex <= toIndex) {
setDiff(null);
setLoading(false);
setError(null);
return;
}
}

setLoading(true);
setError(null);
setDiff(null); // Clear previous diff to avoid showing stale data

try {
const [fromData, toData] = await Promise.all([
javaagentData.loadAllInstrumentations(fromVersion),
javaagentData.loadAllInstrumentations(toVersion),
]);

if (cancelled) return;

if (!fromData || !toData) {
throw new Error("Failed to load instrumentation data for one or both versions.");
}

const result = compareReleases(fromVersion, toVersion, fromData, toData);
setDiff(result);
setLoading(false);
} catch (err) {
if (!cancelled) {
setDiff(null);
setError(err instanceof Error ? err : new Error(String(err)));
setLoading(false);
Comment thread
Pittu-Sharma marked this conversation as resolved.
}
}
}

loadComparison();

return () => {
cancelled = true;
};
}, [fromVersion, toVersion, validVersions]);

return { diff, loading, error };
}
Loading