Add live/paused toggle, env badge, remove topnav search, rename labels
- 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:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}>⌘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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user