diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index d5110a0d..f8305daa 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -489,7 +489,7 @@ function LayoutContent() { const exchangeItems: SearchResult[] = (exchangeResults?.data || []).map((e: any) => ({ id: e.executionId, category: 'exchange' as const, - title: e.executionId, + title: `...${e.executionId.slice(-8)}`, badges: [{ label: e.status, color: statusToColor(e.status) }], meta: `${e.routeId} · ${e.applicationId ?? ''} · ${formatDuration(e.durationMs)}`, path: `/exchanges/${e.applicationId ?? ''}/${e.routeId}/${e.executionId}`, diff --git a/ui/src/pages/AppsTab/AppsTab.tsx b/ui/src/pages/AppsTab/AppsTab.tsx index c85c2712..e7b40f3a 100644 --- a/ui/src/pages/AppsTab/AppsTab.tsx +++ b/ui/src/pages/AppsTab/AppsTab.tsx @@ -36,7 +36,7 @@ import type { Environment } from '../../api/queries/admin/environments'; import { useApplicationConfig, useUpdateApplicationConfig, useProcessorRouteMapping } from '../../api/queries/commands'; import type { ApplicationConfig, TapDefinition } from '../../api/queries/commands'; import { useCatalog } from '../../api/queries/catalog'; -import type { CatalogApp, CatalogRoute } from '../../api/queries/catalog'; +import type { CatalogApp } from '../../api/queries/catalog'; import { DeploymentProgress } from '../../components/DeploymentProgress'; import { timeAgo } from '../../utils/format-utils'; import { applyTracedProcessorUpdate, applyRouteRecordingUpdate } from '../../utils/config-draft-utils'; @@ -89,12 +89,22 @@ function AppListView({ selectedEnv, environments }: { selectedEnv: string | unde const { data: allApps = [], isLoading: allLoading } = useAllApps(); const envId = useMemo(() => environments.find((e) => e.slug === selectedEnv)?.id, [environments, selectedEnv]); const { data: envApps = [], isLoading: envLoading } = useApps(envId); + const { data: catalog = [] } = useCatalog(selectedEnv); const apps = selectedEnv ? envApps : allApps; const isLoading = selectedEnv ? envLoading : allLoading; const envMap = useMemo(() => new Map(environments.map((e) => [e.id, e])), [environments]); + // Build slug → deployment status map from catalog + const deployStatusMap = useMemo(() => { + const map = new Map(); + for (const app of catalog as CatalogApp[]) { + if (app.deployment) map.set(app.slug, app.deployment.status); + } + return map; + }, [catalog]); + type AppRow = App & { id: string; envName: string }; const rows: AppRow[] = useMemo( () => apps.map((a) => ({ ...a, envName: envMap.get(a.environmentId)?.displayName ?? '?' })), @@ -110,13 +120,27 @@ function AppListView({ selectedEnv, environments }: { selectedEnv: string | unde ...(!selectedEnv ? [{ key: 'envName', header: 'Environment', sortable: true, render: (_v: unknown, row: AppRow) => , }] : []), + { key: 'slug', header: 'Status', sortable: false, + render: (_v: unknown, row: AppRow) => { + const status = deployStatusMap.get(row.slug); + if (!status) return ; + const dotVariant = DEPLOY_STATUS_DOT[status] ?? 'dead'; + const badgeColor = STATUS_COLORS[status] ?? 'auto'; + return ( + + + + + ); + }, + }, { key: 'updatedAt', header: 'Updated', sortable: true, render: (_v: unknown, row: AppRow) => {timeAgo(row.updatedAt)}, }, { key: 'createdAt', header: 'Created', sortable: true, render: (_v: unknown, row: AppRow) => {new Date(row.createdAt).toLocaleDateString()}, }, - ], [selectedEnv]); + ], [selectedEnv, deployStatusMap]); if (isLoading) return ; diff --git a/ui/src/pages/DashboardTab/DashboardL1.tsx b/ui/src/pages/DashboardTab/DashboardL1.tsx index 5311d38e..d0468f18 100644 --- a/ui/src/pages/DashboardTab/DashboardL1.tsx +++ b/ui/src/pages/DashboardTab/DashboardL1.tsx @@ -462,7 +462,7 @@ export default function DashboardL1() { /> - + )} diff --git a/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx b/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx index 0bfaebb6..3381e6f3 100644 --- a/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx +++ b/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx @@ -10,6 +10,7 @@ export interface PunchcardCell { interface PunchcardHeatmapProps { cells: PunchcardCell[]; + timeRangeMs?: number; } type Mode = 'transactions' | 'errors'; @@ -38,8 +39,11 @@ const GAP = 2; const LABEL_W = 28; const LABEL_H = 14; -export function PunchcardHeatmap({ cells }: PunchcardHeatmapProps) { +const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000; + +export function PunchcardHeatmap({ cells, timeRangeMs }: PunchcardHeatmapProps) { const [mode, setMode] = useState('transactions'); + const insufficientData = timeRangeMs !== undefined && timeRangeMs < TWO_DAYS_MS; const { grid, maxVal } = useMemo(() => { const cellMap = new Map(); @@ -63,6 +67,14 @@ export function PunchcardHeatmap({ cells }: PunchcardHeatmapProps) { const svgW = LABEL_W + cols * (CELL + GAP); const svgH = LABEL_H + rows * (CELL + GAP); + if (insufficientData) { + return ( +
+ Requires at least 2 days of data +
+ ); + } + return (
diff --git a/ui/src/pages/Exchanges/ExchangesPage.module.css b/ui/src/pages/Exchanges/ExchangesPage.module.css index dac174db..937d6b61 100644 --- a/ui/src/pages/Exchanges/ExchangesPage.module.css +++ b/ui/src/pages/Exchanges/ExchangesPage.module.css @@ -27,6 +27,7 @@ } .rightPanel { + position: relative; flex: 1; display: flex; flex-direction: column; @@ -43,3 +44,26 @@ color: var(--text-muted); font-size: 0.875rem; } + +.closeBtn { + position: absolute; + top: 6px; + right: 8px; + z-index: 10; + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border: none; + border-radius: var(--radius-sm); + background: transparent; + color: var(--text-muted); + cursor: pointer; + transition: background 0.1s, color 0.1s; +} + +.closeBtn:hover { + background: var(--bg-hover); + color: var(--text-primary); +} diff --git a/ui/src/pages/Exchanges/ExchangesPage.tsx b/ui/src/pages/Exchanges/ExchangesPage.tsx index aa968bf2..03b5f68d 100644 --- a/ui/src/pages/Exchanges/ExchangesPage.tsx +++ b/ui/src/pages/Exchanges/ExchangesPage.tsx @@ -1,4 +1,5 @@ import { useState, useMemo, useCallback, useRef, useEffect } from 'react'; +import { X } from 'lucide-react'; import { useNavigate, useLocation, useParams } from 'react-router'; import { useGlobalFilters, useToast } from '@cameleer/design-system'; import { useExecutionDetail } from '../../api/queries/executions'; @@ -122,6 +123,9 @@ export default function ExchangesPage() {
+