Add React UI with Execution Explorer, auth, and standalone deployment
- Scaffold Vite + React + TypeScript frontend in ui/ with full design system (dark/light themes) matching the HTML mockups - Implement Execution Explorer page: search filters, results table with expandable processor tree and exchange detail sidebar, pagination - Add UI authentication: UiAuthController (login/refresh endpoints), JWT filter handles ui: subject prefix, CORS configuration - Shared components: StatusPill, DurationBar, StatCard, AppBadge, FilterChip, Pagination — all using CSS Modules with design tokens - API client layer: openapi-fetch with auth middleware, TanStack Query hooks for search/detail/snapshot queries, Zustand for state - Standalone deployment: Nginx Dockerfile, K8s Deployment + ConfigMap + NodePort (30080), runtime config.js for API base URL - Embedded mode: maven-resources-plugin copies ui/dist into JAR static resources, SPA forward controller for client-side routing - CI/CD: UI build step, Docker build/push for server-ui image, K8s deploy step for UI, UI credential secrets Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
201
ui/src/components/shared/shared.module.css
Normal file
201
ui/src/components/shared/shared.module.css
Normal file
@@ -0,0 +1,201 @@
|
||||
/* ─── Status Pill ─── */
|
||||
.statusPill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 99px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.pillCompleted { background: var(--green-glow); color: var(--green); }
|
||||
.pillFailed { background: var(--rose-glow); color: var(--rose); }
|
||||
.pillRunning { background: rgba(59, 130, 246, 0.12); color: var(--blue); }
|
||||
.pillRunning .statusDot { animation: livePulse 1.5s ease-in-out infinite; }
|
||||
|
||||
/* ─── Duration Bar ─── */
|
||||
.durationBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
background: var(--bg-base);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.barFill {
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.barFast { background: var(--green); }
|
||||
.barMedium { background: var(--amber); }
|
||||
.barSlow { background: var(--rose); }
|
||||
|
||||
/* ─── Stat Card ─── */
|
||||
.statCard {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.statCard:hover { border-color: var(--border); }
|
||||
|
||||
.statCard::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.amber::before { background: linear-gradient(90deg, var(--amber), transparent); }
|
||||
.cyan::before { background: linear-gradient(90deg, var(--cyan), transparent); }
|
||||
.rose::before { background: linear-gradient(90deg, var(--rose), transparent); }
|
||||
.green::before { background: linear-gradient(90deg, var(--green), transparent); }
|
||||
.blue::before { background: linear-gradient(90deg, var(--blue), transparent); }
|
||||
|
||||
.statLabel {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.statValue {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.amber .statValue { color: var(--amber); }
|
||||
.cyan .statValue { color: var(--cyan); }
|
||||
.rose .statValue { color: var(--rose); }
|
||||
.green .statValue { color: var(--green); }
|
||||
.blue .statValue { color: var(--blue); }
|
||||
|
||||
.statChange {
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.up { color: var(--rose); }
|
||||
.down { color: var(--green); }
|
||||
.neutral { color: var(--text-muted); }
|
||||
|
||||
/* ─── App Badge ─── */
|
||||
.appBadge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 2px 8px;
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.appDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* ─── Filter Chip ─── */
|
||||
.chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 5px 12px;
|
||||
background: var(--bg-raised);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 99px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.chip:hover { border-color: var(--text-muted); color: var(--text-primary); }
|
||||
|
||||
.chipActive { background: var(--amber-glow); border-color: var(--amber-dim); color: var(--amber); }
|
||||
.chipActive.chipGreen { background: var(--green-glow); border-color: rgba(16, 185, 129, 0.3); color: var(--green); }
|
||||
.chipActive.chipRose { background: var(--rose-glow); border-color: rgba(244, 63, 94, 0.3); color: var(--rose); }
|
||||
.chipActive.chipBlue { background: rgba(59, 130, 246, 0.12); border-color: rgba(59, 130, 246, 0.3); color: var(--blue); }
|
||||
|
||||
.chipDot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.chipCount {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ─── Pagination ─── */
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pageBtn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.pageBtn:hover:not(:disabled) { border-color: var(--border); background: var(--bg-raised); }
|
||||
.pageBtnActive { background: var(--amber-glow); border-color: var(--amber-dim); color: var(--amber); }
|
||||
.pageBtnDisabled { opacity: 0.3; cursor: default; }
|
||||
|
||||
.pageEllipsis {
|
||||
color: var(--text-muted);
|
||||
padding: 0 4px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
Reference in New Issue
Block a user