feat(service-registry): E2E testing and vite config updates
- Update E2E Dockerfile for test infrastructure - Add .dockerignore and .npmrc for cleaner builds - Update ServicesDashboard and ServicesToolbar components - Add UI methodology documentation - Update tsconfig and vite config for build improvements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
63cfb42d60
commit
a2eeb2fa72
9 changed files with 387 additions and 160 deletions
5
features/service-registry/frontend/.dockerignore
Normal file
5
features/service-registry/frontend/.dockerignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
dist
|
||||
test-results
|
||||
playwright-report
|
||||
.git
|
||||
3
features/service-registry/frontend/.npmrc
Normal file
3
features/service-registry/frontend/.npmrc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@transquinnftw:registry=https://gitlab.com/api/v4/packages/npm/
|
||||
# Allow build scripts for esbuild (required for vite)
|
||||
onlyBuiltDependencies[]=esbuild
|
||||
236
features/service-registry/frontend/docs/UI_METHODOLOGY.md
Normal file
236
features/service-registry/frontend/docs/UI_METHODOLOGY.md
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
# Service Registry UI Methodology
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the scalable UI methodology for rendering services in the service-registry dashboard. The approach is optimized for:
|
||||
|
||||
- **Dependency visualization** - Show relationships between services
|
||||
- **Recency-based grouping** - Prioritize recently active services
|
||||
- **Scale** - Support hundreds of services without performance degradation
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. JSON-Driven Rendering
|
||||
|
||||
All UI rendering is driven by `ServiceInfo[]` data objects. No hardcoded service layouts - everything derives from the data.
|
||||
|
||||
```typescript
|
||||
// services.types.ts
|
||||
interface ServiceInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
type: 'api' | 'web' | 'worker' | 'ml';
|
||||
status: 'healthy' | 'unhealthy' | 'unknown';
|
||||
dependencies?: string[];
|
||||
lastHeartbeat: string;
|
||||
uptime?: number;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- New service types render automatically
|
||||
- Consistent data contract between backend and frontend
|
||||
- Easy to test with mock data
|
||||
|
||||
### 2. Virtual Scrolling (Planned)
|
||||
|
||||
For lists with 100+ services, only visible items should be rendered.
|
||||
|
||||
**Recommended library:** `@tanstack/react-virtual`
|
||||
|
||||
**Implementation pattern:**
|
||||
```typescript
|
||||
const virtualizer = useVirtualizer({
|
||||
count: services.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 48, // row height
|
||||
});
|
||||
```
|
||||
|
||||
**When to implement:** When service count exceeds 100 in production.
|
||||
|
||||
### 3. Component Composition
|
||||
|
||||
Small, composable pieces combine for different views:
|
||||
|
||||
```
|
||||
ServicesTree (grouping + state management)
|
||||
├── GroupHeader (collapsible section headers)
|
||||
│ └── GroupHealthSummary (health dot counts)
|
||||
├── ServicesTreeNode (individual service display)
|
||||
│ ├── StatusDot
|
||||
│ ├── TypeBadge
|
||||
│ └── DependencyIndicator
|
||||
└── DependencyLines (SVG bezier curves)
|
||||
```
|
||||
|
||||
**Principle:** Each component has a single responsibility. State flows down, events bubble up.
|
||||
|
||||
## Grouping Modes
|
||||
|
||||
The tree supports multiple grouping strategies via `ViewMode`:
|
||||
|
||||
| Mode | Groups By | Sort Order | Use Case |
|
||||
|------|-----------|------------|----------|
|
||||
| `by-host` | `hostname` | Service count (desc) | Infrastructure view |
|
||||
| `by-type` | `api/web/worker/ml` | Service count (desc) | Architectural view |
|
||||
| `by-lastupdate` | Recency buckets | Chronological | Activity monitoring |
|
||||
|
||||
### Recency Buckets (by-lastupdate)
|
||||
|
||||
Services are grouped by last heartbeat:
|
||||
|
||||
| Bucket | Criteria |
|
||||
|--------|----------|
|
||||
| `last-5-min` | < 5 minutes ago |
|
||||
| `last-hour` | 5-60 minutes ago |
|
||||
| `today` | 1-24 hours ago |
|
||||
| `this-week` | 1-7 days ago |
|
||||
| `older` | > 7 days ago |
|
||||
|
||||
## Dependency Visualization
|
||||
|
||||
Dependencies are visualized with SVG bezier curves when `showDependencies` is enabled:
|
||||
|
||||
1. **Hover trigger** - Lines appear when hovering over a service
|
||||
2. **Bezier curves** - Smooth S-curves connect source to dependencies
|
||||
3. **Arrow markers** - Direction indicators show dependency flow
|
||||
4. **Responsive** - Recalculates on scroll/resize
|
||||
|
||||
**Implementation:** `DependencyLines.tsx` uses an SVG overlay positioned absolutely over the tree container. Node positions are tracked via refs.
|
||||
|
||||
```typescript
|
||||
// Bezier curve calculation
|
||||
const controlOffset = Math.min(Math.abs(toX - fromX) * 0.4, 100);
|
||||
return `M ${fromX} ${fromY} C ${fromX + controlOffset} ${fromY}, ${toX - controlOffset} ${toY}, ${toX} ${toY}`;
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `components/ServicesTree.tsx` | Main tree with grouping logic |
|
||||
| `components/ServicesTreeNode.tsx` | Composable service node |
|
||||
| `components/DependencyLines.tsx` | SVG dependency curves |
|
||||
| `components/styles/ServicesTree.styles.ts` | Styled components |
|
||||
| `types/services.types.ts` | TypeScript interfaces |
|
||||
| `hooks/useServicesLive.ts` | WebSocket real-time updates |
|
||||
|
||||
## State Management
|
||||
|
||||
### Local State (useState)
|
||||
- `expandedGroups` - Which groups are expanded
|
||||
- `hoveredServiceId` - Currently hovered service (for dependency lines)
|
||||
- `animatingGroups` - Groups currently animating collapse
|
||||
|
||||
### Server State (React Query)
|
||||
- Service list with filtering/pagination
|
||||
- Real-time updates via WebSocket with throttled batching
|
||||
|
||||
### Persisted State (localStorage)
|
||||
- `viewMode` - Current grouping mode
|
||||
- `showDependencies` - Toggle for dependency lines
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] **Virtual scrolling** - Required for 100+ services
|
||||
- [ ] **Nested dependency tree** - Expand to show deps inline
|
||||
- [ ] **Keyboard navigation** - Arrow keys to navigate tree
|
||||
- [ ] **Search within tree** - Filter as you type
|
||||
- [ ] **Dependency graph view** - Full network visualization
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Throttled WebSocket updates** - 5-second batching prevents UI thrashing
|
||||
2. **Selective cache invalidation** - Status changes update single service; registration/deregistration invalidates full query
|
||||
3. **CSS transitions** - Hardware-accelerated expand/collapse animations
|
||||
4. **Memoization** - `useMemo` for grouping calculations
|
||||
|
||||
---
|
||||
|
||||
## @ui Library Integration
|
||||
|
||||
The service-registry should prefer components from `~/Code/@packages/@ui` and enhance the library when gaps exist.
|
||||
|
||||
### Direct Replacements (Ready Now)
|
||||
|
||||
| Current Component | @ui Replacement | Package |
|
||||
|-------------------|-----------------|---------|
|
||||
| `SearchInput` | `Input` with icon | `@ui/primitives` |
|
||||
| `FilterSelect` | `Select` | `@ui/primitives` |
|
||||
| `Spacer` | `Spacer` | `@ui/layout` |
|
||||
| `StatusBadge` (tree node) | `StatusBadge` | `@ui/primitives` |
|
||||
|
||||
### Requires @ui Enhancement
|
||||
|
||||
| Current Component | @ui Component | Missing Feature |
|
||||
|-------------------|---------------|-----------------|
|
||||
| `StatCard` (interactive) | `MetricCard` | `onClick`, `isActive` props for filter UX |
|
||||
| `LiveIndicator` + `LiveDot` | `LiveIndicator` | `isConnected` boolean, disconnected state |
|
||||
| `ViewModeToggle` | — | Need `SegmentedControl` component |
|
||||
| `FilterPill` | — | Need `Tag` with `onRemove` variant |
|
||||
| `TypeStatBadge` | `StatusBadge` | Custom color mapping by service type |
|
||||
| `ToggleButton` | `Button` | `isActive` toggle variant |
|
||||
|
||||
### Candidates for @ui Contribution
|
||||
|
||||
| Component | Proposed @ui Location | Description |
|
||||
|-----------|----------------------|-------------|
|
||||
| `ServicesTree` | `@ui/data/TreeView` | Hierarchical data with grouping |
|
||||
| `ServicesTreeNode` | Part of TreeView | Node with status indicators |
|
||||
| `DependencyLines` | `@ui/charts/DependencyGraph` | SVG bezier relationship viz |
|
||||
| `GroupHeader` | `@ui/layout/CollapsibleSection` | Expandable section with summary |
|
||||
|
||||
### Proposed @ui Enhancements
|
||||
|
||||
#### 1. Interactive MetricCard (`@ui/analytics`)
|
||||
|
||||
```typescript
|
||||
interface MetricCardProps {
|
||||
// ... existing props
|
||||
onClick?: () => void;
|
||||
isActive?: boolean;
|
||||
as?: 'div' | 'button';
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. SegmentedControl (`@ui/primitives`) - NEW
|
||||
|
||||
```typescript
|
||||
interface SegmentedControlProps<T extends string> {
|
||||
options: Array<{ value: T; label: string; icon?: ReactNode }>;
|
||||
value: T;
|
||||
onChange: (value: T) => void;
|
||||
size?: 'small' | 'medium';
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. LiveIndicator with connection state (`@ui/realtime`)
|
||||
|
||||
```typescript
|
||||
interface LiveIndicatorProps {
|
||||
// ... existing props
|
||||
isConnected?: boolean;
|
||||
disconnectedLabel?: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Tag with remove (`@ui/primitives`) - NEW
|
||||
|
||||
```typescript
|
||||
interface TagProps {
|
||||
children: ReactNode;
|
||||
onRemove?: () => void;
|
||||
variant?: 'default' | 'primary' | 'success' | 'warning' | 'error';
|
||||
}
|
||||
```
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
1. **Phase 1**: Replace direct equivalents (Input, Select, Spacer, StatusBadge)
|
||||
2. **Phase 2**: Enhance @ui with missing features (interactive MetricCard, LiveIndicator state)
|
||||
3. **Phase 3**: Add new components to @ui (SegmentedControl, Tag)
|
||||
4. **Phase 4**: Contribute tree components if broadly useful
|
||||
|
|
@ -1,28 +1,25 @@
|
|||
# E2E Testing Dockerfile for Service Registry Frontend
|
||||
# Uses Microsoft's Playwright base image for browser testing
|
||||
|
||||
FROM mcr.microsoft.com/playwright:v1.49.1-noble
|
||||
FROM mcr.microsoft.com/playwright:v1.57.0-noble
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install pnpm globally
|
||||
RUN npm install -g pnpm
|
||||
# Copy package files and npm registry config
|
||||
COPY package.json .npmrc ./
|
||||
|
||||
# Copy package files for dependency installation
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN pnpm install --frozen-lockfile || pnpm install
|
||||
# Install dependencies using npm (simpler for standalone Docker builds)
|
||||
RUN npm install
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the application
|
||||
RUN pnpm build
|
||||
RUN npm run build
|
||||
|
||||
# Set environment for CI
|
||||
ENV CI=true
|
||||
ENV BASE_URL=http://localhost:3010
|
||||
ENV BASE_URL=http://localhost:4173
|
||||
|
||||
# Run E2E tests
|
||||
CMD ["pnpm", "test:e2e:local"]
|
||||
# Run preview server + E2E tests
|
||||
CMD ["sh", "-c", "npm run preview & sleep 3 && npm run test:e2e:local"]
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@
|
|||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"@transquinnftw/ui-theme": "^1.0.0",
|
||||
"@transquinnftw/ui-primitives": "^1.0.0",
|
||||
"@transquinnftw/ui-primitives": "^1.1.0",
|
||||
"@transquinnftw/ui-data": "^1.0.0",
|
||||
"@transquinnftw/ui-feedback": "^1.0.0",
|
||||
"@transquinnftw/ui-analytics": "^1.1.0",
|
||||
"@transquinnftw/ui-realtime": "^1.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^7.11.0",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { useCallback } from 'react';
|
||||
import { MetricCard } from '@ui/analytics';
|
||||
import type { ServicesStats, FilterState, ServiceStatus, ServiceType } from '@/types';
|
||||
import {
|
||||
DashboardContainer,
|
||||
StatsRow,
|
||||
StatCard,
|
||||
StatValue,
|
||||
StatLabel,
|
||||
TypeStatsRow,
|
||||
TypeStatBadge,
|
||||
TypeStatCount,
|
||||
|
|
@ -69,52 +67,44 @@ export function ServicesDashboard({
|
|||
return (
|
||||
<DashboardContainer data-testid="services-dashboard">
|
||||
<StatsRow>
|
||||
<StatCard
|
||||
<MetricCard
|
||||
data-testid="stat-total"
|
||||
data-active={filters.status === null && filters.type === null}
|
||||
$variant="default"
|
||||
$isActive={filters.status === null && filters.type === null}
|
||||
label="Total Services"
|
||||
value={formatNumber(stats.total)}
|
||||
variant="default"
|
||||
isActive={filters.status === null && filters.type === null}
|
||||
onClick={() => {
|
||||
onFilterChange('status', null);
|
||||
onFilterChange('type', null);
|
||||
}}
|
||||
>
|
||||
<StatValue data-testid="stat-value">{formatNumber(stats.total)}</StatValue>
|
||||
<StatLabel>Total Services</StatLabel>
|
||||
</StatCard>
|
||||
/>
|
||||
|
||||
<StatCard
|
||||
<MetricCard
|
||||
data-testid="stat-healthy"
|
||||
data-active={filters.status === 'healthy'}
|
||||
$variant="success"
|
||||
$isActive={filters.status === 'healthy'}
|
||||
label="Healthy"
|
||||
value={formatNumber(stats.healthy)}
|
||||
variant="success"
|
||||
isActive={filters.status === 'healthy'}
|
||||
onClick={() => handleStatusClick('healthy')}
|
||||
>
|
||||
<StatValue data-testid="stat-value" $variant="success">{formatNumber(stats.healthy)}</StatValue>
|
||||
<StatLabel>Healthy</StatLabel>
|
||||
</StatCard>
|
||||
/>
|
||||
|
||||
<StatCard
|
||||
<MetricCard
|
||||
data-testid="stat-unhealthy"
|
||||
data-active={filters.status === 'unhealthy'}
|
||||
$variant="error"
|
||||
$isActive={filters.status === 'unhealthy'}
|
||||
label="Unhealthy"
|
||||
value={formatNumber(stats.unhealthy)}
|
||||
variant="error"
|
||||
isActive={filters.status === 'unhealthy'}
|
||||
onClick={() => handleStatusClick('unhealthy')}
|
||||
>
|
||||
<StatValue data-testid="stat-value" $variant="error">{formatNumber(stats.unhealthy)}</StatValue>
|
||||
<StatLabel>Unhealthy</StatLabel>
|
||||
</StatCard>
|
||||
/>
|
||||
|
||||
<StatCard
|
||||
<MetricCard
|
||||
data-testid="stat-unknown"
|
||||
data-active={filters.status === 'unknown'}
|
||||
$variant="warning"
|
||||
$isActive={filters.status === 'unknown'}
|
||||
label="Unknown"
|
||||
value={formatNumber(stats.unknown)}
|
||||
variant="warning"
|
||||
isActive={filters.status === 'unknown'}
|
||||
onClick={() => handleStatusClick('unknown')}
|
||||
>
|
||||
<StatValue data-testid="stat-value" $variant="warning">{formatNumber(stats.unknown)}</StatValue>
|
||||
<StatLabel>Unknown</StatLabel>
|
||||
</StatCard>
|
||||
/>
|
||||
</StatsRow>
|
||||
|
||||
<TypeStatsRow>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { useState, useCallback, useEffect, useMemo } from 'react';
|
||||
import { SegmentedControl } from '@ui/primitives';
|
||||
import { LiveIndicator } from '@ui/realtime';
|
||||
import type { ViewMode, FilterState, ServiceType, ServiceStatus } from '@/types';
|
||||
import {
|
||||
ToolbarContainer,
|
||||
|
|
@ -7,14 +9,10 @@ import {
|
|||
SearchIcon,
|
||||
SearchInput,
|
||||
FilterSelect,
|
||||
ViewModeToggle,
|
||||
ViewModeButton,
|
||||
ActiveFiltersRow,
|
||||
FilterPill,
|
||||
FilterPillRemove,
|
||||
ClearAllButton,
|
||||
LiveIndicator,
|
||||
LiveDot,
|
||||
Spacer,
|
||||
ToggleButton,
|
||||
} from './styles/Toolbar.styles';
|
||||
|
|
@ -22,6 +20,76 @@ import {
|
|||
const VIEW_MODE_STORAGE_KEY = 'service-registry-view-mode';
|
||||
const SHOW_DEPS_STORAGE_KEY = 'service-registry-show-deps';
|
||||
|
||||
// SVG icons for SegmentedControl options
|
||||
const HostIcon = () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="2"
|
||||
y="2"
|
||||
width="10"
|
||||
height="4"
|
||||
rx="1"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<rect
|
||||
x="2"
|
||||
y="8"
|
||||
width="10"
|
||||
height="4"
|
||||
rx="1"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const TypeIcon = () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="4" cy="4" r="2" stroke="currentColor" strokeWidth="1.5" />
|
||||
<circle cx="10" cy="4" r="2" stroke="currentColor" strokeWidth="1.5" />
|
||||
<circle cx="4" cy="10" r="2" stroke="currentColor" strokeWidth="1.5" />
|
||||
<circle cx="10" cy="10" r="2" stroke="currentColor" strokeWidth="1.5" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const TimeIcon = () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path
|
||||
d="M7 4V7L9 9"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const VIEW_MODE_OPTIONS = [
|
||||
{ value: 'by-host' as ViewMode, label: 'Host', icon: <HostIcon /> },
|
||||
{ value: 'by-type' as ViewMode, label: 'Type', icon: <TypeIcon /> },
|
||||
{ value: 'by-lastupdate' as ViewMode, label: 'Time', icon: <TimeIcon /> },
|
||||
];
|
||||
|
||||
interface ServicesToolbarProps {
|
||||
filters: FilterState;
|
||||
onFilterChange: <K extends keyof FilterState>(key: K, value: FilterState[K]) => void;
|
||||
|
|
@ -135,6 +203,9 @@ export function ServicesToolbar({
|
|||
|
||||
const hasActiveFilters = activeFilters.length > 0;
|
||||
|
||||
// Determine live indicator label based on connection and pending updates
|
||||
const liveLabel = pendingUpdates > 0 ? `${pendingUpdates} pending` : undefined;
|
||||
|
||||
return (
|
||||
<ToolbarContainer data-testid="services-toolbar">
|
||||
<ToolbarRow>
|
||||
|
|
@ -198,89 +269,14 @@ export function ServicesToolbar({
|
|||
|
||||
<Spacer />
|
||||
|
||||
<ViewModeToggle data-testid="view-mode-toggle">
|
||||
<ViewModeButton
|
||||
$isActive={viewMode === 'by-host'}
|
||||
onClick={() => onViewModeChange('by-host')}
|
||||
title="Group by host"
|
||||
data-testid="view-host"
|
||||
data-active={viewMode === 'by-host'}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="2"
|
||||
y="2"
|
||||
width="10"
|
||||
height="4"
|
||||
rx="1"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
<rect
|
||||
x="2"
|
||||
y="8"
|
||||
width="10"
|
||||
height="4"
|
||||
rx="1"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</svg>
|
||||
Host
|
||||
</ViewModeButton>
|
||||
<ViewModeButton
|
||||
$isActive={viewMode === 'by-type'}
|
||||
onClick={() => onViewModeChange('by-type')}
|
||||
title="Group by type"
|
||||
data-testid="view-type"
|
||||
data-active={viewMode === 'by-type'}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="4" cy="4" r="2" stroke="currentColor" strokeWidth="1.5" />
|
||||
<circle cx="10" cy="4" r="2" stroke="currentColor" strokeWidth="1.5" />
|
||||
<circle cx="4" cy="10" r="2" stroke="currentColor" strokeWidth="1.5" />
|
||||
<circle cx="10" cy="10" r="2" stroke="currentColor" strokeWidth="1.5" />
|
||||
</svg>
|
||||
Type
|
||||
</ViewModeButton>
|
||||
<ViewModeButton
|
||||
$isActive={viewMode === 'by-lastupdate'}
|
||||
onClick={() => onViewModeChange('by-lastupdate')}
|
||||
title="Group by last update"
|
||||
data-testid="view-time"
|
||||
data-active={viewMode === 'by-lastupdate'}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path
|
||||
d="M7 4V7L9 9"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Time
|
||||
</ViewModeButton>
|
||||
</ViewModeToggle>
|
||||
<SegmentedControl
|
||||
value={viewMode}
|
||||
onChange={onViewModeChange}
|
||||
options={VIEW_MODE_OPTIONS}
|
||||
size="sm"
|
||||
aria-label="View mode"
|
||||
data-testid="view-mode-toggle"
|
||||
/>
|
||||
|
||||
<ToggleButton
|
||||
$isActive={showDependencies}
|
||||
|
|
@ -310,18 +306,14 @@ export function ServicesToolbar({
|
|||
Deps
|
||||
</ToggleButton>
|
||||
|
||||
<LiveIndicator $isConnected={isLiveConnected} data-testid="live-indicator" data-connected={isLiveConnected}>
|
||||
<LiveDot $isConnected={isLiveConnected} />
|
||||
{isLiveConnected ? (
|
||||
pendingUpdates > 0 ? (
|
||||
`${pendingUpdates} pending`
|
||||
) : (
|
||||
'Live'
|
||||
)
|
||||
) : (
|
||||
'Offline'
|
||||
)}
|
||||
</LiveIndicator>
|
||||
<LiveIndicator
|
||||
isConnected={isLiveConnected}
|
||||
connectedLabel={liveLabel}
|
||||
label="Live"
|
||||
disconnectedLabel="Offline"
|
||||
variant="compact"
|
||||
data-testid="live-indicator"
|
||||
/>
|
||||
</ToolbarRow>
|
||||
|
||||
{hasActiveFilters && (
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@
|
|||
"@ui/theme": ["./node_modules/@transquinnftw/ui-theme/src"],
|
||||
"@ui/primitives": ["./node_modules/@transquinnftw/ui-primitives/src"],
|
||||
"@ui/data": ["./node_modules/@transquinnftw/ui-data/src"],
|
||||
"@ui/feedback": ["./node_modules/@transquinnftw/ui-feedback/src"]
|
||||
"@ui/feedback": ["./node_modules/@transquinnftw/ui-feedback/src"],
|
||||
"@ui/analytics": ["./node_modules/@transquinnftw/ui-analytics/src"],
|
||||
"@ui/realtime": ["./node_modules/@transquinnftw/ui-realtime/src"],
|
||||
"@ui/utils": ["./node_modules/@transquinnftw/ui-utils/src"]
|
||||
},
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,21 +23,20 @@ export default defineConfig(({ mode }) => {
|
|||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
'@ui/theme': path.resolve(__dirname, './node_modules/@transquinnftw/ui-theme/src'),
|
||||
'@ui/primitives': path.resolve(__dirname, './node_modules/@transquinnftw/ui-primitives/src'),
|
||||
'@ui/data': path.resolve(__dirname, './node_modules/@transquinnftw/ui-data/src'),
|
||||
'@ui/feedback': path.resolve(__dirname, './node_modules/@transquinnftw/ui-feedback/src'),
|
||||
'@ui/design-tokens': path.resolve('/var/home/lilith/Code/@packages/@ui/packages/design-tokens/src'),
|
||||
'@ui/utils': path.resolve('/var/home/lilith/Code/@packages/@ui/packages/ui-utils/src'),
|
||||
'@ui/theme': path.resolve(__dirname, 'node_modules/@transquinnftw/ui-theme/src'),
|
||||
'@ui/primitives': path.resolve(__dirname, 'node_modules/@transquinnftw/ui-primitives/src'),
|
||||
'@ui/data': path.resolve(__dirname, 'node_modules/@transquinnftw/ui-data/src'),
|
||||
'@ui/feedback': path.resolve(__dirname, 'node_modules/@transquinnftw/ui-feedback/src'),
|
||||
'@ui/analytics': path.resolve(__dirname, 'node_modules/@transquinnftw/ui-analytics/src'),
|
||||
'@ui/realtime': path.resolve(__dirname, 'node_modules/@transquinnftw/ui-realtime/src'),
|
||||
'@ui/design-tokens': path.resolve(__dirname, 'node_modules/@transquinnftw/design-tokens/src'),
|
||||
'@ui/utils': path.resolve(__dirname, 'node_modules/@transquinnftw/ui-utils/src'),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'import.meta.env.VITE_API_URL': mode === 'production'
|
||||
? JSON.stringify('')
|
||||
: JSON.stringify(env.VITE_API_URL || '/api'),
|
||||
'import.meta.env.VITE_WS_URL': mode === 'production'
|
||||
? JSON.stringify('')
|
||||
: JSON.stringify(env.VITE_WS_URL || ''),
|
||||
// Always use /api prefix for consistent API paths
|
||||
'import.meta.env.VITE_API_URL': JSON.stringify(env.VITE_API_URL || '/api'),
|
||||
'import.meta.env.VITE_WS_URL': JSON.stringify(env.VITE_WS_URL || ''),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue