Add live/paused toggle, env badge, remove topnav search, rename labels
All checks were successful
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 46s
CI / deploy (push) Successful in 28s

- Add LIVE/PAUSED toggle button that auto-refreshes search results every 5s
- Source environment badge from VITE_ENV_NAME env var (defaults to DEV locally, PRODUCTION in Docker)
- Remove search trigger button from topnav (command palette still available via keyboard)
- Rename "Transaction Explorer" to "Route Explorer" and "Active Now" to "In-Flight"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-13 18:54:24 +01:00
parent 868cf84c4e
commit cf804638d7
7 changed files with 47 additions and 60 deletions

View File

@@ -5,6 +5,9 @@ COPY package.json package-lock.json ./
RUN npm ci RUN npm ci
COPY . . COPY . .
ARG VITE_ENV_NAME=PRODUCTION
ENV VITE_ENV_NAME=$VITE_ENV_NAME
RUN npm run build RUN npm run build
FROM nginx:1.27-alpine FROM nginx:1.27-alpine

View File

@@ -14,7 +14,7 @@ export function useExecutionStats() {
}); });
} }
export function useSearchExecutions(filters: SearchRequest) { export function useSearchExecutions(filters: SearchRequest, live = false) {
return useQuery({ return useQuery({
queryKey: ['executions', 'search', filters], queryKey: ['executions', 'search', filters],
queryFn: async () => { queryFn: async () => {
@@ -25,6 +25,7 @@ export function useSearchExecutions(filters: SearchRequest) {
return data!; return data!;
}, },
placeholderData: (prev) => prev, placeholderData: (prev) => prev,
refetchInterval: live ? 5_000 : false,
}); });
} }

View File

@@ -61,43 +61,6 @@
gap: 16px; gap: 16px;
} }
.searchTrigger {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 12px 5px 10px;
background: var(--bg-raised);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-muted);
font-size: 13px;
font-family: var(--font-body);
cursor: pointer;
transition: border-color 0.15s, color 0.15s;
}
.searchTrigger:hover {
border-color: var(--text-muted);
color: var(--text-secondary);
}
.searchTrigger svg {
width: 14px;
height: 14px;
opacity: 0.5;
}
.kbdKey {
font-family: var(--font-mono);
font-size: 11px;
padding: 1px 5px;
background: var(--bg-hover);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-muted);
line-height: 1.4;
}
.envBadge { .envBadge {
font-family: var(--font-mono); font-family: var(--font-mono);
font-size: 11px; font-size: 11px;

View File

@@ -1,13 +1,11 @@
import { NavLink } from 'react-router'; import { NavLink } from 'react-router';
import { useThemeStore } from '../../theme/theme-store'; import { useThemeStore } from '../../theme/theme-store';
import { useAuthStore } from '../../auth/auth-store'; import { useAuthStore } from '../../auth/auth-store';
import { useCommandPalette } from '../command-palette/use-command-palette';
import styles from './TopNav.module.css'; import styles from './TopNav.module.css';
export function TopNav() { export function TopNav() {
const { theme, toggle } = useThemeStore(); const { theme, toggle } = useThemeStore();
const { username, logout } = useAuthStore(); const { username, logout } = useAuthStore();
const openPalette = useCommandPalette((s) => s.open);
return ( return (
<nav className={styles.topnav}> <nav className={styles.topnav}>
@@ -28,15 +26,7 @@ export function TopNav() {
</ul> </ul>
<div className={styles.navRight}> <div className={styles.navRight}>
<button className={styles.searchTrigger} onClick={openPalette}> <span className={styles.envBadge}>{import.meta.env.VITE_ENV_NAME || 'DEV'}</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
Search...
<kbd className={styles.kbdKey}>&#8984;K</kbd>
</button>
<span className={styles.envBadge}>PRODUCTION</span>
<button className={styles.themeToggle} onClick={toggle} title="Toggle theme"> <button className={styles.themeToggle} onClick={toggle} title="Toggle theme">
{theme === 'dark' ? '\u2600\uFE0F' : '\uD83C\uDF19'} {theme === 'dark' ? '\u2600\uFE0F' : '\uD83C\uDF19'}
</button> </button>

View File

@@ -19,22 +19,48 @@
margin-top: 2px; margin-top: 2px;
} }
.liveIndicator { .liveToggle {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 12px; font-size: 12px;
color: var(--green);
font-family: var(--font-mono); font-family: var(--font-mono);
font-weight: 500; font-weight: 500;
background: none;
border: 1px solid var(--border);
border-radius: 6px;
padding: 6px 14px;
cursor: pointer;
transition: all 0.15s ease;
}
.liveToggle:hover {
background: var(--surface-hover);
}
.liveOn {
color: var(--green);
border-color: var(--green);
}
.liveOff {
color: var(--text-muted);
}
.liveOn .liveDot {
background: var(--green);
animation: livePulse 2s ease-in-out infinite;
}
.liveOff .liveDot {
background: var(--text-muted);
animation: none;
} }
.liveDot { .liveDot {
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
background: var(--green);
animation: livePulse 2s ease-in-out infinite;
} }
.statsBar { .statsBar {

View File

@@ -7,9 +7,9 @@ import { ResultsTable } from './ResultsTable';
import styles from './ExecutionExplorer.module.css'; import styles from './ExecutionExplorer.module.css';
export function ExecutionExplorer() { export function ExecutionExplorer() {
const { toSearchRequest, offset, limit, setOffset } = useExecutionSearch(); const { toSearchRequest, offset, limit, setOffset, live, toggleLive } = useExecutionSearch();
const searchRequest = toSearchRequest(); const searchRequest = toSearchRequest();
const { data, isLoading, isFetching } = useSearchExecutions(searchRequest); const { data, isLoading, isFetching } = useSearchExecutions(searchRequest, live);
const { data: stats } = useExecutionStats(); const { data: stats } = useExecutionStats();
const { data: timeseries } = useStatsTimeseries( const { data: timeseries } = useStatsTimeseries(
searchRequest.timeFrom ?? undefined, searchRequest.timeFrom ?? undefined,
@@ -39,13 +39,13 @@ export function ExecutionExplorer() {
{/* Page Header */} {/* Page Header */}
<div className={`${styles.pageHeader} animate-in`}> <div className={`${styles.pageHeader} animate-in`}>
<div> <div>
<h1>Transaction Explorer</h1> <h1>Route Explorer</h1>
<div className={styles.subtitle}>Search and analyze route executions</div> <div className={styles.subtitle}>Search and analyze route executions</div>
</div> </div>
<div className={styles.liveIndicator}> <button className={`${styles.liveToggle} ${live ? styles.liveOn : styles.liveOff}`} onClick={toggleLive}>
<span className={styles.liveDot} /> <span className={styles.liveDot} />
LIVE {live ? 'LIVE' : 'PAUSED'}
</div> </button>
</div> </div>
{/* Stats Bar */} {/* Stats Bar */}
@@ -54,7 +54,7 @@ export function ExecutionExplorer() {
<StatCard label="Avg Duration" value={`${avgDuration}ms`} accent="cyan" sparkData={sparkAvgDuration} /> <StatCard label="Avg Duration" value={`${avgDuration}ms`} accent="cyan" sparkData={sparkAvgDuration} />
<StatCard label="Failed (page)" value={failedCount.toString()} accent="rose" sparkData={sparkFailed} /> <StatCard label="Failed (page)" value={failedCount.toString()} accent="rose" sparkData={sparkFailed} />
<StatCard label="P99 Latency" value={stats ? `${stats.p99LatencyMs}ms` : '--'} accent="green" change="last hour" sparkData={sparkP99} /> <StatCard label="P99 Latency" value={stats ? `${stats.p99LatencyMs}ms` : '--'} accent="green" change="last hour" sparkData={sparkP99} />
<StatCard label="Active Now" value={stats ? stats.activeCount.toString() : '--'} accent="blue" change="running executions" sparkData={sparkActive} /> <StatCard label="In-Flight" value={stats ? stats.activeCount.toString() : '--'} accent="blue" change="running executions" sparkData={sparkActive} />
</div> </div>
{/* Filters */} {/* Filters */}

View File

@@ -19,9 +19,11 @@ interface ExecutionSearchState {
routeId: string; routeId: string;
agentId: string; agentId: string;
processorType: string; processorType: string;
live: boolean;
offset: number; offset: number;
limit: number; limit: number;
toggleLive: () => void;
setStatus: (statuses: string[]) => void; setStatus: (statuses: string[]) => void;
toggleStatus: (s: string) => void; toggleStatus: (s: string) => void;
setTimeFrom: (v: string) => void; setTimeFrom: (v: string) => void;
@@ -47,9 +49,11 @@ export const useExecutionSearch = create<ExecutionSearchState>((set, get) => ({
routeId: '', routeId: '',
agentId: '', agentId: '',
processorType: '', processorType: '',
live: true,
offset: 0, offset: 0,
limit: 25, limit: 25,
toggleLive: () => set((state) => ({ live: !state.live })),
setStatus: (statuses) => set({ status: statuses, offset: 0 }), setStatus: (statuses) => set({ status: statuses, offset: 0 }),
toggleStatus: (s) => toggleStatus: (s) =>
set((state) => ({ set((state) => ({