mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-24 12:42:38 +00:00
feat(web): consistent truncate-with-tooltip + tap-to-expand across dashboard cards
Add reusable TruncatedLabel component: a single-line label that truncates
with an ellipsis, shows the full text as a native hover tooltip, and on
click/Enter/Space expands to two wrapped lines (touch-friendly). An
interactive={false} mode provides tooltip-only behaviour for use inside
already-clickable rows/links where an inner onClick would hijack navigation.
Apply it consistently to the long, dynamic label columns across:
- fleet-health: failure reasons, model + app ranked lists
- sla: top failing apps, violators failure reason
- geographic-performance: fastest/slowest location cards
- vulnerability exposure panel: CVE sample software
- agent metrics: top exception types
- progress + download/install: app-name labels
- admin software tabs: software names
- session reports (comment/adminNote) + ops events (message): tooltip-only
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { authenticatedFetch } from "@/lib/authenticatedFetch";
|
||||
import TruncatedLabel from "@/components/TruncatedLabel";
|
||||
import { trackEvent } from "@/lib/appInsights";
|
||||
import type { AutoResolveResult, IgnoredSoftwareEntry } from "./SoftwareMappingTypes";
|
||||
|
||||
@@ -211,8 +212,8 @@ export function IgnoredSoftwareTab({
|
||||
|
||||
return (
|
||||
<tr key={key} className="hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors">
|
||||
<td className="px-3 py-3 text-sm text-gray-900 dark:text-gray-100 truncate">
|
||||
{entry.softwareName}
|
||||
<td className="px-3 py-3">
|
||||
<TruncatedLabel text={entry.softwareName} className="block text-sm text-gray-900 dark:text-gray-100" />
|
||||
</td>
|
||||
<td className="px-3 py-3 text-sm text-gray-600 dark:text-gray-400 truncate">
|
||||
{entry.publisher || <span className="text-gray-300 dark:text-gray-600 italic">unknown</span>}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { api } from "@/lib/api";
|
||||
import TruncatedLabel from "@/components/TruncatedLabel";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
import { extractContinuation } from "@/lib/paginationLink";
|
||||
import { extractSessionId, buildAutoReason } from "./opsEventSessionHelpers";
|
||||
@@ -445,8 +446,8 @@ export function OpsEventsSection({
|
||||
<td className="px-3 py-2.5 text-sm font-mono text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
||||
{evt.eventType}
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-sm text-gray-700 dark:text-gray-300 max-w-[300px] truncate">
|
||||
{evt.message}
|
||||
<td className="px-3 py-2.5 max-w-[300px]">
|
||||
<TruncatedLabel interactive={false} text={evt.message ?? ""} className="block text-sm text-gray-700 dark:text-gray-300" />
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-sm font-mono text-gray-700 dark:text-gray-300">
|
||||
{evt.tenantId ? (evt.tenantId.length > 8 ? `${evt.tenantId.slice(0, 8)}...` : evt.tenantId) : <span className="text-gray-300 dark:text-gray-600">-</span>}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
import TruncatedLabel from "@/components/TruncatedLabel";
|
||||
import { extractContinuation } from "@/lib/paginationLink";
|
||||
import { isGuid } from "@/utils/inputValidation";
|
||||
import { trackEvent } from "@/lib/appInsights";
|
||||
@@ -355,8 +356,10 @@ export function SessionReportsSection({
|
||||
<td className="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
|
||||
{r.submittedBy}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700 dark:text-gray-300 truncate max-w-xs">
|
||||
{r.comment || <span className="text-gray-400 italic">no comment</span>}
|
||||
<td className="px-4 py-3 max-w-xs">
|
||||
{r.comment
|
||||
? <TruncatedLabel interactive={false} text={r.comment} className="block text-sm text-gray-700 dark:text-gray-300" />
|
||||
: <span className="text-sm text-gray-400 italic">no comment</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
|
||||
{r.adminNote ? (
|
||||
@@ -364,7 +367,7 @@ export function SessionReportsSection({
|
||||
<svg className="w-4 h-4 text-indigo-500 dark:text-indigo-400 shrink-0" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
|
||||
</svg>
|
||||
<span className="truncate max-w-[140px]">{r.adminNote}</span>
|
||||
<TruncatedLabel interactive={false} text={r.adminNote} className="max-w-[140px]" />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-300 dark:text-gray-600">—</span>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
import TruncatedLabel from "@/components/TruncatedLabel";
|
||||
import { trackEvent } from "@/lib/appInsights";
|
||||
import type { UnmatchedSoftwareEntry, AutoResolveResult } from "./SoftwareMappingTypes";
|
||||
|
||||
@@ -564,9 +565,7 @@ export function UnmappedSoftwareTab({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-3 py-3 text-sm text-gray-900 dark:text-gray-100 flex-1 min-w-0 truncate">
|
||||
{entry.softwareName}
|
||||
</div>
|
||||
<TruncatedLabel text={entry.softwareName} className="px-3 py-3 text-sm text-gray-900 dark:text-gray-100 flex-1" />
|
||||
<div className="px-3 py-3 text-sm text-gray-600 dark:text-gray-400 truncate" style={{ width: "18%", flexShrink: 0 }}>
|
||||
{entry.publisher || <span className="text-gray-300 dark:text-gray-600 italic">unknown</span>}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState, useMemo, useCallback } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { api } from '@/lib/api';
|
||||
import TruncatedLabel from '@/components/TruncatedLabel';
|
||||
import { useAuth } from '../../../../contexts/AuthContext';
|
||||
import { useNotifications } from '../../../../contexts/NotificationContext';
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
@@ -705,7 +706,7 @@ export function SectionAgentMetrics() {
|
||||
<p className="text-xs text-gray-500 mb-1">Top exceptions:</p>
|
||||
{crashRate.topExceptions.map((e) => (
|
||||
<div key={e.exceptionType} className="flex justify-between text-xs">
|
||||
<span className="text-red-600 font-mono truncate mr-2">{e.exceptionType}</span>
|
||||
<TruncatedLabel text={e.exceptionType} className="text-red-600 font-mono mr-2" />
|
||||
<span className="text-gray-500">{e.count}x</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useNotifications } from "../../contexts/NotificationContext";
|
||||
import { api } from "@/lib/api";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
import FleetStatCard from "./components/FleetStatCard";
|
||||
import TruncatedLabel from "../../components/TruncatedLabel";
|
||||
import { useFleetHealth } from "./hooks/useFleetHealth";
|
||||
import { useAggregatedAdminScope } from "@/hooks";
|
||||
import { GlobalAdminBanner, globalAdminSubtitle } from "@/components/GlobalAdminBanner";
|
||||
@@ -404,9 +405,10 @@ export default function FleetHealthPage() {
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm text-gray-700 truncate pr-2">
|
||||
{fr.reason}
|
||||
</span>
|
||||
<TruncatedLabel
|
||||
text={fr.reason}
|
||||
className="text-sm text-gray-700 pr-2"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-900 flex-shrink-0">
|
||||
({fr.count})
|
||||
</span>
|
||||
@@ -444,9 +446,10 @@ export default function FleetHealthPage() {
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm text-gray-700 truncate pr-2">
|
||||
{m.model}
|
||||
</span>
|
||||
<TruncatedLabel
|
||||
text={m.model}
|
||||
className="text-sm text-gray-700 pr-2"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-900 flex-shrink-0">
|
||||
{m.avgMinutes} min
|
||||
</span>
|
||||
@@ -479,9 +482,10 @@ export default function FleetHealthPage() {
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm text-gray-700 truncate pr-2">
|
||||
{m.model}
|
||||
</span>
|
||||
<TruncatedLabel
|
||||
text={m.model}
|
||||
className="text-sm text-gray-700 pr-2"
|
||||
/>
|
||||
<span className="text-sm font-medium text-red-600 flex-shrink-0">
|
||||
{m.failed} failed
|
||||
</span>
|
||||
@@ -545,9 +549,10 @@ export default function FleetHealthPage() {
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm text-gray-700 truncate pr-2">
|
||||
{app.appName}
|
||||
</span>
|
||||
<TruncatedLabel
|
||||
text={app.appName}
|
||||
className="text-sm text-gray-700 pr-2"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-900 flex-shrink-0">
|
||||
{avgLabel} avg
|
||||
</span>
|
||||
@@ -595,9 +600,10 @@ export default function FleetHealthPage() {
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm text-gray-700 truncate pr-2">
|
||||
{app.appName}
|
||||
</span>
|
||||
<TruncatedLabel
|
||||
text={app.appName}
|
||||
className="text-sm text-gray-700 pr-2"
|
||||
/>
|
||||
<span className="text-sm font-medium text-red-600 flex-shrink-0">
|
||||
{app.failed} failed
|
||||
</span>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState, useRef, useMemo, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import dynamic from "next/dynamic";
|
||||
import { ProtectedRoute } from "../../components/ProtectedRoute";
|
||||
import TruncatedLabel from "@/components/TruncatedLabel";
|
||||
import { useTenant } from "../../contexts/TenantContext";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { api } from "@/lib/api";
|
||||
@@ -323,18 +324,14 @@ export default function GeographicPerformancePage() {
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-sm font-medium text-gray-500">Fastest Location</div>
|
||||
<div className="text-lg font-bold text-green-600 truncate" title={stats.fastest?.locationKey}>
|
||||
{stats.fastest?.locationKey || "—"}
|
||||
</div>
|
||||
<TruncatedLabel text={stats.fastest?.locationKey || "—"} className="block text-lg font-bold text-green-600" />
|
||||
<div className="text-xs text-gray-400">
|
||||
{stats.fastest ? `${Math.round(stats.fastest.avgDurationMinutes)} min avg` : "No data"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-sm font-medium text-gray-500">Slowest Location</div>
|
||||
<div className="text-lg font-bold text-red-600 truncate" title={stats.slowest?.locationKey}>
|
||||
{stats.slowest?.locationKey || "—"}
|
||||
</div>
|
||||
<TruncatedLabel text={stats.slowest?.locationKey || "—"} className="block text-lg font-bold text-red-600" />
|
||||
<div className="text-xs text-gray-400">
|
||||
{stats.slowest ? `${Math.round(stats.slowest.avgDurationMinutes)} min avg` : "No data"}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useTenant } from "../../contexts/TenantContext";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { useNotifications } from "../../contexts/NotificationContext";
|
||||
import TruncatedLabel from "@/components/TruncatedLabel";
|
||||
import { useSignalR } from "../../contexts/SignalRContext";
|
||||
import { ProtectedRoute } from "../../components/ProtectedRoute";
|
||||
import { useProgressSearch } from "./hooks/useProgressSearch";
|
||||
@@ -374,13 +375,16 @@ export default function ProgressPortalPage() {
|
||||
{isCurrent && (step.id === 3 || step.id === 5) && (
|
||||
<div className="flex items-center space-x-1.5 mt-0.5">
|
||||
<div className="w-1.5 h-1.5 bg-blue-400 rounded-full animate-pulse flex-shrink-0" />
|
||||
<span className="text-xs text-blue-500 truncate">
|
||||
{currentDownload?.active && currentDownload.appName
|
||||
? `Downloading ${currentDownload.appName}...`
|
||||
: currentInstall?.active && currentInstall.appName
|
||||
? `Installing ${currentInstall.appName}...`
|
||||
: "Processing..."}
|
||||
</span>
|
||||
<TruncatedLabel
|
||||
className="text-xs text-blue-500"
|
||||
text={
|
||||
currentDownload?.active && currentDownload.appName
|
||||
? `Downloading ${currentDownload.appName}...`
|
||||
: currentInstall?.active && currentInstall.appName
|
||||
? `Installing ${currentInstall.appName}...`
|
||||
: "Processing..."
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -398,9 +402,7 @@ export default function ProgressPortalPage() {
|
||||
<div>
|
||||
<p className="text-xs text-blue-500 mb-1 font-medium">Downloading</p>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm text-blue-700 font-medium truncate pr-2">
|
||||
{currentDownload.appName}
|
||||
</span>
|
||||
<TruncatedLabel text={currentDownload.appName} className="text-sm text-blue-700 font-medium pr-2" />
|
||||
{currentDownload.downloadRateBps > 0 && (
|
||||
<span className="text-xs text-blue-500 flex-shrink-0">
|
||||
{currentDownload.downloadRateBps >= 1024 * 1024
|
||||
@@ -443,9 +445,7 @@ export default function ProgressPortalPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-1.5 min-w-0">
|
||||
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse flex-shrink-0" />
|
||||
<span className="text-sm text-blue-700 font-medium truncate">
|
||||
{currentInstall.appName}
|
||||
</span>
|
||||
<TruncatedLabel text={currentInstall.appName} className="text-sm text-blue-700 font-medium" />
|
||||
</div>
|
||||
{installElapsedMs != null && installElapsedMs > 0 && (
|
||||
<span className="text-xs text-blue-600 font-medium tabular-nums flex-shrink-0 ml-2">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from 'react';
|
||||
import { useTenant } from '../../contexts/TenantContext';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useNotifications } from '../../contexts/NotificationContext';
|
||||
import TruncatedLabel from '@/components/TruncatedLabel';
|
||||
import { ProtectedRoute } from '../../components/ProtectedRoute';
|
||||
import { api } from "@/lib/api";
|
||||
import { authenticatedFetch, TokenExpiredError } from "@/lib/authenticatedFetch";
|
||||
@@ -333,7 +334,7 @@ export default function SlaPage() {
|
||||
<span className="text-xs text-gray-400 w-4">{i + 1}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300 truncate pr-2">{app.appName}</span>
|
||||
<TruncatedLabel text={app.appName} className="text-sm text-gray-700 dark:text-gray-300 pr-2" />
|
||||
<span className="text-sm font-medium text-red-600 dark:text-red-400 flex-shrink-0">{app.failCount} failed</span>
|
||||
</div>
|
||||
<div className="w-full h-2 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
|
||||
@@ -429,7 +430,9 @@ export default function SlaPage() {
|
||||
{v.violationType}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-gray-500 dark:text-gray-400 max-w-xs truncate">{v.failureReason || "-"}</td>
|
||||
<td className="px-6 py-4 max-w-xs">
|
||||
<TruncatedLabel text={v.failureReason || "-"} className="block text-sm text-gray-500 dark:text-gray-400" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { partitionHistoricReplayEvents } from "@/lib/historicReplay";
|
||||
import TruncatedLabel from "@/components/TruncatedLabel";
|
||||
import { shouldSkipLowBytesTotal, shouldSkipNoActivity } from "@/lib/downloadProgressFilters";
|
||||
import { formatBytes, formatThroughput, formatDuration } from "@/lib/formatting";
|
||||
import DoBreakdownBar from "./DoBreakdownBar";
|
||||
@@ -353,7 +354,7 @@ function DownloadItem({ download: dl, progressPercent }: { download: DownloadIte
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
)}
|
||||
<span className={`text-sm font-medium truncate ${dl.isSkipped ? "text-gray-500" : "text-gray-900"}`}>{dl.appName}</span>
|
||||
<TruncatedLabel text={dl.appName} className={`text-sm font-medium ${dl.isSkipped ? "text-gray-500" : "text-gray-900"}`} />
|
||||
{dl.isSkipped && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-200 text-gray-600 font-medium">Skipped</span>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { getErrorCodeEntry, formatErrorCode } from "@/utils/errorCodeMap";
|
||||
import { partitionHistoricReplayEvents } from "@/lib/historicReplay";
|
||||
import TruncatedLabel from "@/components/TruncatedLabel";
|
||||
|
||||
interface InstallEvent {
|
||||
timestamp: string;
|
||||
@@ -465,9 +466,7 @@ function InstallItemRow({ item }: { item: InstallItem }) {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
||||
</svg>
|
||||
)}
|
||||
<span className={`text-sm font-medium truncate ${item.state === "Skipped" ? "text-gray-500" : "text-gray-900"}`}>
|
||||
{item.appName}
|
||||
</span>
|
||||
<TruncatedLabel text={item.appName} className={`text-sm font-medium ${item.state === "Skipped" ? "text-gray-500" : "text-gray-900"}`} />
|
||||
{item.state === "Skipped" && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-200 text-gray-600 font-medium">Skipped</span>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface TruncatedLabelProps {
|
||||
/** The full text to display. Also used verbatim as the hover tooltip. */
|
||||
text: string;
|
||||
/** Extra classes for the text styling (font size, colour, padding). */
|
||||
className?: string;
|
||||
/**
|
||||
* When false, only the hover tooltip is provided — no click-to-expand and no
|
||||
* button semantics. Use this inside an already-clickable row/link/button,
|
||||
* where an inner onClick would hijack the outer navigation.
|
||||
*/
|
||||
interactive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single-line label that truncates with an ellipsis by default.
|
||||
*
|
||||
* - Hovering always shows the full text in a native browser tooltip (standard
|
||||
* tooltip, no special cursor).
|
||||
* - When `interactive` (the default), clicking — or pressing Enter/Space when
|
||||
* focused — toggles a two-line expanded view. This keeps long values readable
|
||||
* on touch devices where hover is unavailable.
|
||||
* - When `interactive={false}`, it is a plain truncated span with only the
|
||||
* tooltip, safe to nest inside a clickable row or link.
|
||||
*/
|
||||
export default function TruncatedLabel({
|
||||
text,
|
||||
className = "",
|
||||
interactive = true,
|
||||
}: TruncatedLabelProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
if (!interactive) {
|
||||
return (
|
||||
<span title={text} className={`${className} min-w-0 truncate`}>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const toggle = () => setExpanded((v) => !v);
|
||||
|
||||
return (
|
||||
<span
|
||||
title={text}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-expanded={expanded}
|
||||
onClick={toggle}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
toggle();
|
||||
}
|
||||
}}
|
||||
className={`${className} min-w-0 cursor-pointer ${
|
||||
expanded ? "line-clamp-2 break-words" : "truncate"
|
||||
}`}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { severityChipClass, KEV_BADGE_CLASS } from "@/lib/vulnerabilitySeverity";
|
||||
import TruncatedLabel from "@/components/TruncatedLabel";
|
||||
|
||||
export interface VulnerabilityTopCve {
|
||||
cveId: string;
|
||||
@@ -136,7 +137,7 @@ export default function VulnerabilityExposurePanel({
|
||||
)}
|
||||
</div>
|
||||
{c.sampleSoftware.length > 0 && (
|
||||
<div className="text-[11px] text-gray-500 dark:text-gray-400 truncate mt-0.5">{c.sampleSoftware.join(", ")}</div>
|
||||
<TruncatedLabel text={c.sampleSoftware.join(", ")} className="block text-[11px] text-gray-500 dark:text-gray-400 mt-0.5" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0">
|
||||
|
||||
Reference in New Issue
Block a user