fix: nice-to-have polish — breadcrumbs, close button, status badges
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 40s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
CI / cleanup-branch (pull_request) Has been skipped
CI / build (pull_request) Failing after 35s
CI / docker (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 40s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
CI / cleanup-branch (pull_request) Has been skipped
CI / build (pull_request) Failing after 35s
CI / docker (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
- 7.1: Add deployment status badge (StatusDot + Badge) to AppsTab app
list, sourced from catalog.deployment.status via slug lookup
- 7.3: Add X close button to top-right of exchange detail right panel
in ExchangesPage (position:absolute, triggers handleClearSelection)
- 7.5: PunchcardHeatmap shows "Requires at least 2 days of data"
when timeRangeMs < 2 days; DashboardL1 passes the range down
- 7.6: Command palette exchange results truncate IDs to ...{last8}
matching the exchanges table display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -489,7 +489,7 @@ function LayoutContent() {
|
|||||||
const exchangeItems: SearchResult[] = (exchangeResults?.data || []).map((e: any) => ({
|
const exchangeItems: SearchResult[] = (exchangeResults?.data || []).map((e: any) => ({
|
||||||
id: e.executionId,
|
id: e.executionId,
|
||||||
category: 'exchange' as const,
|
category: 'exchange' as const,
|
||||||
title: e.executionId,
|
title: `...${e.executionId.slice(-8)}`,
|
||||||
badges: [{ label: e.status, color: statusToColor(e.status) }],
|
badges: [{ label: e.status, color: statusToColor(e.status) }],
|
||||||
meta: `${e.routeId} · ${e.applicationId ?? ''} · ${formatDuration(e.durationMs)}`,
|
meta: `${e.routeId} · ${e.applicationId ?? ''} · ${formatDuration(e.durationMs)}`,
|
||||||
path: `/exchanges/${e.applicationId ?? ''}/${e.routeId}/${e.executionId}`,
|
path: `/exchanges/${e.applicationId ?? ''}/${e.routeId}/${e.executionId}`,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import type { Environment } from '../../api/queries/admin/environments';
|
|||||||
import { useApplicationConfig, useUpdateApplicationConfig, useProcessorRouteMapping } from '../../api/queries/commands';
|
import { useApplicationConfig, useUpdateApplicationConfig, useProcessorRouteMapping } from '../../api/queries/commands';
|
||||||
import type { ApplicationConfig, TapDefinition } from '../../api/queries/commands';
|
import type { ApplicationConfig, TapDefinition } from '../../api/queries/commands';
|
||||||
import { useCatalog } from '../../api/queries/catalog';
|
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 { DeploymentProgress } from '../../components/DeploymentProgress';
|
||||||
import { timeAgo } from '../../utils/format-utils';
|
import { timeAgo } from '../../utils/format-utils';
|
||||||
import { applyTracedProcessorUpdate, applyRouteRecordingUpdate } from '../../utils/config-draft-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 { data: allApps = [], isLoading: allLoading } = useAllApps();
|
||||||
const envId = useMemo(() => environments.find((e) => e.slug === selectedEnv)?.id, [environments, selectedEnv]);
|
const envId = useMemo(() => environments.find((e) => e.slug === selectedEnv)?.id, [environments, selectedEnv]);
|
||||||
const { data: envApps = [], isLoading: envLoading } = useApps(envId);
|
const { data: envApps = [], isLoading: envLoading } = useApps(envId);
|
||||||
|
const { data: catalog = [] } = useCatalog(selectedEnv);
|
||||||
|
|
||||||
const apps = selectedEnv ? envApps : allApps;
|
const apps = selectedEnv ? envApps : allApps;
|
||||||
const isLoading = selectedEnv ? envLoading : allLoading;
|
const isLoading = selectedEnv ? envLoading : allLoading;
|
||||||
|
|
||||||
const envMap = useMemo(() => new Map(environments.map((e) => [e.id, e])), [environments]);
|
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<string, string>();
|
||||||
|
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 };
|
type AppRow = App & { id: string; envName: string };
|
||||||
const rows: AppRow[] = useMemo(
|
const rows: AppRow[] = useMemo(
|
||||||
() => apps.map((a) => ({ ...a, envName: envMap.get(a.environmentId)?.displayName ?? '?' })),
|
() => 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,
|
...(!selectedEnv ? [{ key: 'envName', header: 'Environment', sortable: true,
|
||||||
render: (_v: unknown, row: AppRow) => <Badge label={row.envName} color={'auto' as const} />,
|
render: (_v: unknown, row: AppRow) => <Badge label={row.envName} color={'auto' as const} />,
|
||||||
}] : []),
|
}] : []),
|
||||||
|
{ key: 'slug', header: 'Status', sortable: false,
|
||||||
|
render: (_v: unknown, row: AppRow) => {
|
||||||
|
const status = deployStatusMap.get(row.slug);
|
||||||
|
if (!status) return <span className={styles.cellMeta}>—</span>;
|
||||||
|
const dotVariant = DEPLOY_STATUS_DOT[status] ?? 'dead';
|
||||||
|
const badgeColor = STATUS_COLORS[status] ?? 'auto';
|
||||||
|
return (
|
||||||
|
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||||||
|
<StatusDot variant={dotVariant} />
|
||||||
|
<Badge label={status} color={badgeColor} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{ key: 'updatedAt', header: 'Updated', sortable: true,
|
{ key: 'updatedAt', header: 'Updated', sortable: true,
|
||||||
render: (_v: unknown, row: AppRow) => <span className={styles.cellMeta}>{timeAgo(row.updatedAt)}</span>,
|
render: (_v: unknown, row: AppRow) => <span className={styles.cellMeta}>{timeAgo(row.updatedAt)}</span>,
|
||||||
},
|
},
|
||||||
{ key: 'createdAt', header: 'Created', sortable: true,
|
{ key: 'createdAt', header: 'Created', sortable: true,
|
||||||
render: (_v: unknown, row: AppRow) => <span className={styles.cellMeta}>{new Date(row.createdAt).toLocaleDateString()}</span>,
|
render: (_v: unknown, row: AppRow) => <span className={styles.cellMeta}>{new Date(row.createdAt).toLocaleDateString()}</span>,
|
||||||
},
|
},
|
||||||
], [selectedEnv]);
|
], [selectedEnv, deployStatusMap]);
|
||||||
|
|
||||||
if (isLoading) return <PageLoader />;
|
if (isLoading) return <PageLoader />;
|
||||||
|
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ export default function DashboardL1() {
|
|||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="7-Day Pattern">
|
<Card title="7-Day Pattern">
|
||||||
<PunchcardHeatmap cells={punchcardData ?? []} />
|
<PunchcardHeatmap cells={punchcardData ?? []} timeRangeMs={timeRange.end.getTime() - timeRange.start.getTime()} />
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface PunchcardCell {
|
|||||||
|
|
||||||
interface PunchcardHeatmapProps {
|
interface PunchcardHeatmapProps {
|
||||||
cells: PunchcardCell[];
|
cells: PunchcardCell[];
|
||||||
|
timeRangeMs?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mode = 'transactions' | 'errors';
|
type Mode = 'transactions' | 'errors';
|
||||||
@@ -38,8 +39,11 @@ const GAP = 2;
|
|||||||
const LABEL_W = 28;
|
const LABEL_W = 28;
|
||||||
const LABEL_H = 14;
|
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<Mode>('transactions');
|
const [mode, setMode] = useState<Mode>('transactions');
|
||||||
|
const insufficientData = timeRangeMs !== undefined && timeRangeMs < TWO_DAYS_MS;
|
||||||
|
|
||||||
const { grid, maxVal } = useMemo(() => {
|
const { grid, maxVal } = useMemo(() => {
|
||||||
const cellMap = new Map<string, PunchcardCell>();
|
const cellMap = new Map<string, PunchcardCell>();
|
||||||
@@ -63,6 +67,14 @@ export function PunchcardHeatmap({ cells }: PunchcardHeatmapProps) {
|
|||||||
const svgW = LABEL_W + cols * (CELL + GAP);
|
const svgW = LABEL_W + cols * (CELL + GAP);
|
||||||
const svgH = LABEL_H + rows * (CELL + GAP);
|
const svgH = LABEL_H + rows * (CELL + GAP);
|
||||||
|
|
||||||
|
if (insufficientData) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '2rem 1rem', color: 'var(--text-muted)', fontSize: '0.8125rem', fontStyle: 'italic' }}>
|
||||||
|
Requires at least 2 days of data
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.toggleRow}>
|
<div className={styles.toggleRow}>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rightPanel {
|
.rightPanel {
|
||||||
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -43,3 +44,26 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.875rem;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
import { useNavigate, useLocation, useParams } from 'react-router';
|
import { useNavigate, useLocation, useParams } from 'react-router';
|
||||||
import { useGlobalFilters, useToast } from '@cameleer/design-system';
|
import { useGlobalFilters, useToast } from '@cameleer/design-system';
|
||||||
import { useExecutionDetail } from '../../api/queries/executions';
|
import { useExecutionDetail } from '../../api/queries/executions';
|
||||||
@@ -122,6 +123,9 @@ export default function ExchangesPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.splitter} onPointerDown={handleSplitterDown} />
|
<div className={styles.splitter} onPointerDown={handleSplitterDown} />
|
||||||
<div className={styles.rightPanel} style={{ width: `${100 - splitPercent}%` }}>
|
<div className={styles.rightPanel} style={{ width: `${100 - splitPercent}%` }}>
|
||||||
|
<button className={styles.closeBtn} onClick={handleClearSelection} title="Close panel" aria-label="Close panel">
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
<DiagramPanel
|
<DiagramPanel
|
||||||
appId={panelAppId}
|
appId={panelAppId}
|
||||||
routeId={panelRouteId}
|
routeId={panelRouteId}
|
||||||
|
|||||||
Reference in New Issue
Block a user