v0.2.6.7 — GATE-006 D, version row OSV check
- evaluateSupplyChainHold checks version row osv_status for target version, closing hole where we OSV-check one version but install another. - confirm_dependencies uses TransitionByPackageFrom with source-state guard (StatusInstalling → Installed/Failed). - Clears pinned version on successful install. - Version bump to v0.2.6.7.
This commit is contained in:
parent
14cd0884b4
commit
7d089ab5d2
9 changed files with 339 additions and 51 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Search,
|
||||
RefreshCw,
|
||||
|
|
@ -47,6 +47,10 @@ const TAB_GROUPS: { key: string; label: string; statuses: string }[] = [
|
|||
{ key: 'failed-ignored', label: 'Failed / Ignored', statuses: 'failed,ignored' },
|
||||
];
|
||||
|
||||
const parseAgentUpdatesTab = (tab: string | null) => {
|
||||
return TAB_GROUPS.some(group => group.key === tab) ? tab || TAB_GROUPS[0].key : TAB_GROUPS[0].key;
|
||||
};
|
||||
|
||||
const STATUS_META: Record<string, { label: string; icon: React.ReactNode; class: string }> = {
|
||||
pending: { label: 'Pending', icon: <Clock className="h-3 w-3" />, class: 'text-gray-600 bg-gray-100' },
|
||||
approved: { label: 'Approved', icon: <Check className="h-3 w-3" />, class: 'text-blue-600 bg-blue-100' },
|
||||
|
|
@ -59,7 +63,8 @@ const STATUS_META: Record<string, { label: string; icon: React.ReactNode; class:
|
|||
};
|
||||
|
||||
export function AgentUpdatesEnhanced({ agentId, onNavigateToHistory }: AgentUpdatesEnhancedProps) {
|
||||
const [activeTab, setActiveTab] = useState(TAB_GROUPS[0].key);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const activeTab = parseAgentUpdatesTab(searchParams.get('updates_tab'));
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize] = useState(50);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
|
@ -75,6 +80,21 @@ export function AgentUpdatesEnhanced({ agentId, onNavigateToHistory }: AgentUpda
|
|||
const queryClient = useQueryClient();
|
||||
const activeGroup = TAB_GROUPS.find(g => g.key === activeTab) || TAB_GROUPS[0];
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
setSelectedUpdates([]);
|
||||
}, [activeTab]);
|
||||
|
||||
const selectActiveTab = (tab: string) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
if (tab === TAB_GROUPS[0].key) {
|
||||
params.delete('updates_tab');
|
||||
} else {
|
||||
params.set('updates_tab', tab);
|
||||
}
|
||||
setSearchParams(params, { replace: true });
|
||||
};
|
||||
|
||||
// Fetch updates with status filter
|
||||
const { data: updateData, isLoading, error, refetch } = useQuery<AgentUpdateResponse>({
|
||||
queryKey: ['agent-updates', agentId, activeGroup.statuses, currentPage, pageSize, searchTerm, selectedSeverity],
|
||||
|
|
@ -314,7 +334,7 @@ export function AgentUpdatesEnhanced({ agentId, onNavigateToHistory }: AgentUpda
|
|||
{TAB_GROUPS.map((tab) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
onClick={() => { setActiveTab(tab.key); setCurrentPage(1); setSelectedUpdates([]); }}
|
||||
onClick={() => selectActiveTab(tab.key)}
|
||||
className={cn(
|
||||
'px-4 py-2 border-b-2 transition-colors',
|
||||
activeTab === tab.key
|
||||
|
|
|
|||
|
|
@ -54,6 +54,32 @@ interface ChatTimelineProps {
|
|||
externalSearch?: string; // external search query from parent
|
||||
}
|
||||
|
||||
const entryPackageName = (entry: HistoryEntry): string | undefined => {
|
||||
if (entry.package_name) return entry.package_name;
|
||||
return typeof entry.params?.package_name === 'string' ? entry.params.package_name : undefined;
|
||||
};
|
||||
|
||||
const entryPackageType = (entry: HistoryEntry): string | undefined => {
|
||||
if (entry.package_type) return entry.package_type;
|
||||
return typeof entry.params?.package_type === 'string' ? entry.params.package_type : undefined;
|
||||
};
|
||||
|
||||
const entryUpdateLink = (entry: HistoryEntry): string | undefined => {
|
||||
if (typeof entry.params?.update_id === 'string' && entry.params.update_id) {
|
||||
return `/updates/${entry.params.update_id}`;
|
||||
}
|
||||
|
||||
const packageName = entryPackageName(entry);
|
||||
if (!packageName) return undefined;
|
||||
|
||||
const packageType = entryPackageType(entry);
|
||||
if (packageType) {
|
||||
return `/updates/package/${packageType}/${packageName}`;
|
||||
}
|
||||
|
||||
return `/updates?search=${encodeURIComponent(packageName)}`;
|
||||
};
|
||||
|
||||
// Helper function to create smart summaries for package operations
|
||||
const createPackageOperationSummary = (entry: HistoryEntry): string => {
|
||||
const action = entry.action.replace(/_/g, ' ');
|
||||
|
|
@ -635,6 +661,8 @@ const ChatTimeline: React.FC<ChatTimelineProps> = ({ agentId, className, isScope
|
|||
onToggle: () => void;
|
||||
}> = ({ entry, isExpanded, isScopedView: _isScopedView, onToggle }) => {
|
||||
const narrative = getNarrativeSummary(entry);
|
||||
const packageName = entryPackageName(entry);
|
||||
const updateLink = entryUpdateLink(entry);
|
||||
|
||||
return (
|
||||
<div className="group rounded-lg transition-all duration-200">
|
||||
|
|
@ -736,6 +764,17 @@ const ChatTimeline: React.FC<ChatTimelineProps> = ({ agentId, className, isScope
|
|||
{/* Critical vitals - always visible in collapsed view */}
|
||||
<div className="mt-2 ml-8 text-xs text-gray-600 space-y-1">
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
||||
{!_isScopedView && entry.agent_id && (
|
||||
<span>
|
||||
<span className="font-medium">Agent:</span>{' '}
|
||||
<Link
|
||||
to={`/agents/${entry.agent_id}`}
|
||||
className="text-blue-700 hover:text-blue-900 hover:underline"
|
||||
>
|
||||
{entry.hostname || `${entry.agent_id.slice(0, 8)}...`}
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
<span className="font-medium">Action:</span> {entry.action.replace(/_/g, ' ')}
|
||||
</span>
|
||||
|
|
@ -745,9 +784,15 @@ const ChatTimeline: React.FC<ChatTimelineProps> = ({ agentId, className, isScope
|
|||
<span className="text-gray-500"> (Exit Code: {entry.exit_code})</span>
|
||||
)}
|
||||
</span>
|
||||
{entry.package_name && (
|
||||
{packageName && updateLink && (
|
||||
<span>
|
||||
<span className="font-medium">Package:</span> {entry.package_name}
|
||||
<span className="font-medium">Package:</span>{' '}
|
||||
<Link
|
||||
to={updateLink}
|
||||
className="text-green-700 hover:text-green-900 hover:underline"
|
||||
>
|
||||
{packageName}
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
{narrative.subject && narrative.subject !== 'system operation' && narrative.subject !== entry.package_name && (
|
||||
|
|
@ -793,11 +838,11 @@ const ChatTimeline: React.FC<ChatTimelineProps> = ({ agentId, className, isScope
|
|||
<span className="text-gray-500 font-medium">Command ID</span>
|
||||
<span className="font-mono text-gray-800 break-all">{entry.id}</span>
|
||||
</div>
|
||||
{entry.package_name && (
|
||||
{packageName && (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-500 font-medium">Package</span>
|
||||
<span className="text-gray-800 truncate" title={entry.package_name}>
|
||||
{entry.package_name}
|
||||
<span className="text-gray-800 truncate" title={packageName}>
|
||||
{packageName}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1051,23 +1096,13 @@ const ChatTimeline: React.FC<ChatTimelineProps> = ({ agentId, className, isScope
|
|||
{entry.hostname || 'View Agent'}
|
||||
</Link>
|
||||
|
||||
{entry.package_name && entry.package_type && (
|
||||
{packageName && updateLink && (
|
||||
<Link
|
||||
to={`/updates/package/${entry.package_type}/${entry.package_name}`}
|
||||
to={updateLink}
|
||||
className="inline-flex items-center px-2.5 py-1.5 bg-green-50 text-green-700 rounded-md hover:bg-green-100 transition-colors font-medium"
|
||||
>
|
||||
<Package className="h-3 w-3 mr-1" />
|
||||
{entry.package_name}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{entry.package_name && !entry.package_type && (
|
||||
<Link
|
||||
to={`/updates?search=${encodeURIComponent(entry.package_name)}`}
|
||||
className="inline-flex items-center px-2.5 py-1.5 bg-green-50 text-green-700 rounded-md hover:bg-green-100 transition-colors font-medium"
|
||||
>
|
||||
<Package className="h-3 w-3 mr-1" />
|
||||
{entry.package_name}
|
||||
{packageName}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
|
|
@ -1233,4 +1268,4 @@ const ChatTimeline: React.FC<ChatTimelineProps> = ({ agentId, className, isScope
|
|||
);
|
||||
};
|
||||
|
||||
export default ChatTimeline;
|
||||
export default ChatTimeline;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ import toast from 'react-hot-toast';
|
|||
import { updateApi } from '@/lib/api';
|
||||
import DependencyClosureTree from '@/components/DependencyClosureTree';
|
||||
|
||||
type UpdatesTab = 'updates' | 'commands';
|
||||
|
||||
const parseUpdatesTab = (tab: string | null): UpdatesTab => {
|
||||
return tab === 'commands' ? 'commands' : 'updates';
|
||||
};
|
||||
|
||||
const Updates: React.FC = () => {
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
|
|
@ -61,11 +66,12 @@ const Updates: React.FC = () => {
|
|||
const [pendingDependencies, setPendingDependencies] = useState<string[]>([]);
|
||||
const [dependencyUpdateId, setDependencyUpdateId] = useState<string | null>(null);
|
||||
const [dependencyLoading, setDependencyLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'updates' | 'commands'>('updates');
|
||||
const activeTab = parseUpdatesTab(searchParams.get('tab'));
|
||||
|
||||
// Store filters in URL
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams();
|
||||
if (activeTab !== 'updates') params.set('tab', activeTab);
|
||||
if (debouncedSearchQuery) params.set('search', debouncedSearchQuery);
|
||||
if (statusFilter) params.set('status', statusFilter);
|
||||
if (severityFilter) params.set('severity', severityFilter);
|
||||
|
|
@ -78,7 +84,17 @@ const Updates: React.FC = () => {
|
|||
if (pageSize !== 100) params.set('page_size', pageSize.toString());
|
||||
|
||||
setSearchParams(params, { replace: true });
|
||||
}, [debouncedSearchQuery, statusFilter, severityFilter, typeFilter, agentFilter, sortBy, sortOrder, currentPage, pageSize]);
|
||||
}, [activeTab, debouncedSearchQuery, statusFilter, severityFilter, typeFilter, agentFilter, vulnFilter, sortBy, sortOrder, currentPage, pageSize, setSearchParams]);
|
||||
|
||||
const selectActiveTab = (tab: UpdatesTab) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
if (tab === 'updates') {
|
||||
params.delete('tab');
|
||||
} else {
|
||||
params.set('tab', tab);
|
||||
}
|
||||
setSearchParams(params, { replace: true });
|
||||
};
|
||||
|
||||
// Fetch updates list (still used for the summary stat cards + detail fallback)
|
||||
const { data: updatesData } = useUpdates({
|
||||
|
|
@ -1384,7 +1400,7 @@ const Updates: React.FC = () => {
|
|||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setActiveTab('updates')}
|
||||
onClick={() => selectActiveTab('updates')}
|
||||
className="btn btn-ghost"
|
||||
>
|
||||
← Back to Updates
|
||||
|
|
@ -1736,7 +1752,7 @@ const Updates: React.FC = () => {
|
|||
|
||||
{/* Command History button */}
|
||||
<button
|
||||
onClick={() => setActiveTab('commands')}
|
||||
onClick={() => selectActiveTab('commands')}
|
||||
className="btn btn-ghost"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
|
|
@ -2012,4 +2028,4 @@ const Updates: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default Updates;
|
||||
export default Updates;
|
||||
|
|
|
|||
Loading…
Reference in a new issue