re-uploading work

This commit is contained in:
2026-02-04 17:46:30 -06:00
commit 3b14c65998
1388 changed files with 381262 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
# Pack Configuration Display
## Overview
The Pack detail page now displays pack configuration in a unified view that combines the configuration schema (`conf_schema`) with actual configuration values (`config`).
## Features
### Configuration Section
When viewing a pack that has configuration properties defined in its schema, a "Configuration" section is automatically displayed on the pack detail page.
### Display Components
The configuration display shows:
1. **Property Name**: The configuration key (displayed in monospace font)
2. **Type Badge**: The data type (string, boolean, integer, number, array, object)
3. **Default Badge**: A yellow badge indicating when the default value is being used
4. **Description**: Human-readable description from the schema
5. **Current Value**: The actual configuration value, formatted based on type
6. **Range Information**: For numeric types with min/max constraints
### Value Formatting
Values are formatted intelligently based on their type:
- **Boolean**: Green checkmark badge for `true`, gray badge for `false`
- **Numbers**: Displayed in monospace font
- **Strings**: Plain text (truncated if over 50 characters)
- **Arrays**: Shows item count (e.g., "[3 items]")
- **Objects**: Shows key count (e.g., "{5 keys}")
- **Not Set**: Displays as italic gray "not set" text
### Default Value Handling
When a configuration property has a default value defined in the schema but no actual value is set in `config`:
- The default value is displayed
- A yellow "default" badge indicates it's using the schema default
- No "default" badge appears when an explicit value is set
## Example
For a pack with the following schema and config:
```yaml
conf_schema:
type: object
properties:
max_action_timeout:
type: integer
description: "Maximum timeout for action execution in seconds"
default: 300
minimum: 1
maximum: 3600
enable_debug_logging:
type: boolean
description: "Enable debug logging for core pack actions"
default: false
required: []
config:
max_action_timeout: 300
enable_debug_logging: false
```
The UI will display:
```
Configuration
─────────────────────────────────────────────────
max_action_timeout [integer]
Maximum timeout for action execution in seconds
300
Range: 1 - 3600
enable_debug_logging [boolean]
Enable debug logging for core pack actions
✗ false
```
## No Configuration
If a pack has no `conf_schema` properties defined, the Configuration section is not displayed.
## Implementation
- **Component**: `PackConfiguration` in `web/src/pages/packs/PacksPage.tsx`
- **Value Renderer**: `ConfigValue` helper component for type-specific formatting
- **Location**: Displayed in the pack detail view, after "Pack Information" card
## API Data
The configuration display uses data from the pack detail endpoint:
```
GET /api/v1/packs/{ref}
```
Response includes:
- `conf_schema`: JSON Schema defining configuration structure
- `config`: JSON object with actual configuration values
Both fields are already included in the `PackResponse` DTO.
## Usage
1. Navigate to any pack detail page: `/packs/{ref}`
2. If the pack has configuration properties, scroll to the "Configuration" section
3. View current values, types, and descriptions
4. See which values are using defaults (yellow badge)
5. For numeric values, view valid range constraints
## Future Enhancements
Potential improvements:
- Inline editing of configuration values
- Validation against schema constraints
- Configuration history/audit trail
- Environment-specific configuration overrides
- Secret/sensitive value masking
- Configuration export/import

View File

@@ -0,0 +1,440 @@
# Web UI Pack Testing Integration
## Overview
The Attune web UI now includes comprehensive pack testing capabilities, allowing users to view test results, execute tests, and monitor pack quality through a visual interface.
## Features
### 1. Pack Detail Page - Test Results
The pack detail page displays the latest test results and provides quick access to test history.
**Location**: `/packs/{ref}`
**Features**:
- Latest test status badge (passed/failed/skipped)
- Test summary (total, passed, failed, skipped)
- Pass rate percentage
- Test execution timestamp
- "Run Tests" button for manual test execution
- Toggle between latest results and test history
### 2. Test Result Display Component
**Component**: `PackTestResult`
Displays detailed test execution results with:
- Overall status indicator (passed/failed)
- Test statistics (total, passed, failed, skipped)
- Pass rate and duration
- Expandable test suites
- Individual test case results with error messages
- stdout/stderr output for failed tests
**Usage**:
```tsx
import PackTestResult from '@/components/packs/PackTestResult';
<PackTestResult
result={testResultData}
showDetails={true}
/>
```
### 3. Test History Component
**Component**: `PackTestHistory`
Displays a paginated list of all test executions for a pack.
**Features**:
- Chronological list of test executions
- Status badges for each execution
- Trigger reason indicators (register, manual, ci, schedule)
- Expandable details for each execution
- Load more pagination
- Date/time formatting
- Duration display
**Usage**:
```tsx
import PackTestHistory from '@/components/packs/PackTestHistory';
<PackTestHistory
executions={testExecutions}
isLoading={false}
onLoadMore={() => loadNextPage()}
hasMore={hasMorePages}
/>
```
### 4. Test Status Badge
**Component**: `PackTestBadge`
Compact status indicator showing test results.
**Variants**:
- `passed` - Green with checkmark
- `failed` - Red with X
- `skipped` - Gray with clock
- Size options: `sm`, `md`, `lg`
**Usage**:
```tsx
import PackTestBadge from '@/components/packs/PackTestBadge';
<PackTestBadge
status="passed"
passed={76}
total={76}
size="md"
showCounts={true}
/>
```
### 5. Pack Registration Page
**Location**: `/packs/register`
Form for registering packs from local filesystem with test control options.
**Form Fields**:
- **Pack Directory Path** (required) - Absolute path to pack directory
- **Skip Tests** (checkbox) - Skip test execution during registration
- **Force Registration** (checkbox) - Proceed even if tests fail or pack exists
**Features**:
- Real-time validation
- Loading state during registration
- Success/error messages
- Automatic redirect to pack details on success
- Info panel explaining registration process
- Help section with guidance
## API Integration
### React Query Hooks
#### `usePackLatestTest(packRef: string)`
Fetches the latest test result for a pack.
```tsx
const { data: latestTest } = usePackLatestTest('core');
// Returns: { data: PackTestExecution | null }
```
#### `usePackTestHistory(packRef: string, params)`
Fetches paginated test history for a pack.
```tsx
const { data: testHistory } = usePackTestHistory('core', {
page: 1,
pageSize: 10
});
// Returns: { data: { items: PackTestExecution[], meta: PaginationMeta } }
```
#### `useExecutePackTests()`
Mutation hook for executing pack tests.
```tsx
const executeTests = useExecutePackTests();
await executeTests.mutateAsync('core');
// Executes tests and invalidates test queries
```
#### `useRegisterPack()`
Mutation hook for registering packs.
```tsx
const registerPack = useRegisterPack();
await registerPack.mutateAsync({
path: '/path/to/pack',
force: false,
skipTests: false
});
```
## User Workflows
### View Test Results
1. Navigate to `/packs`
2. Click on a pack name
3. View latest test results in the main section
4. Click "View History" to see all test executions
5. Click on any execution to expand details
### Run Tests Manually
1. Navigate to pack detail page
2. Click "Run Tests" button in sidebar (or main section)
3. Wait for test execution (button shows "Running Tests...")
4. Results update automatically on completion
5. View detailed results in the expanded test result component
### Register a New Pack
1. Navigate to `/packs`
2. Click "+ Register Pack" button
3. Fill in the pack directory path (e.g., `/home/user/packs/mypack`)
4. Optionally check "Skip Tests" to bypass test execution
5. Optionally check "Force Registration" to override conflicts
6. Click "Register Pack"
7. Wait for registration to complete
8. View results and test outcomes
9. Automatically redirected to pack details page
### Register Pack with Tests Disabled
Use this workflow during development:
1. Navigate to `/packs/register`
2. Enter pack path
3. **Check "Skip Tests"**
4. Click "Register Pack"
5. Pack is registered without validation
6. Later, manually run tests from pack detail page
### Force Re-registration
Use this to replace an existing pack:
1. Navigate to `/packs/register`
2. Enter pack path (same as existing pack)
3. **Check "Force Registration"**
4. Optionally check "Skip Tests" for faster iteration
5. Click "Register Pack"
6. Existing pack is deleted and replaced
7. Tests run (unless skipped)
## Visual Design
### Color Scheme
- **Passed Tests**: Green (`green-600`, `green-50`)
- **Failed Tests**: Red (`red-600`, `red-50`)
- **Skipped Tests**: Gray (`gray-600`, `gray-50`)
- **Trigger Types**:
- Register: Blue
- Manual: Purple
- CI: Green
- Schedule: Yellow
### Icons
- **CheckCircle** - Passed tests
- **XCircle** - Failed tests
- **Clock** - Skipped tests
- **AlertCircle** - Unknown/error state
- **Play** - Run tests button
- **Calendar** - Test execution date
- **Clock** - Test duration
- **ChevronDown/ChevronRight** - Expandable sections
## Error Handling
### Test Execution Failures
When tests fail:
- Error message displayed at top of page
- Red alert banner with error details
- Test results still stored (if execution completed)
- User can retry by clicking "Run Tests" again
### Registration Failures
When registration fails:
- Red alert box with error message
- Form remains filled for corrections
- Common errors:
- "Pack directory does not exist"
- "pack.yaml not found"
- "Pack already exists" (suggest using force)
- "Tests failed" (suggest using force or fixing tests)
### Network Errors
When API calls fail:
- Error toast/alert
- Retry button available
- State preserved for manual retry
## Accessibility
- All interactive elements keyboard accessible
- ARIA labels on icon buttons
- Color is not the only indicator (icons + text)
- Focus states clearly visible
- Proper heading hierarchy
- Screen reader friendly status messages
## Performance Considerations
### Data Fetching
- `staleTime: 30000` (30 seconds) on test queries
- Automatic refetch on window focus
- Pagination for test history (10 items per page)
- Query invalidation after test execution
### Optimizations
- Expandable sections to reduce initial render
- Lazy loading of test details
- Memoized components for large lists
- Efficient re-renders with React Query
## Responsive Design
- Mobile-friendly layouts
- Stacked columns on small screens
- Touch-friendly tap targets (min 44x44px)
- Horizontal scroll for wide tables
- Collapsible sections for mobile
## Future Enhancements
### Planned Features
1. **Real-time Test Execution**
- WebSocket updates during test runs
- Progress bar showing test completion
- Live stdout/stderr streaming
2. **Test Comparison**
- Compare test results across versions
- Show performance regressions
- Highlight newly failing tests
3. **Test Filtering**
- Filter by status (passed/failed/skipped)
- Filter by trigger type
- Date range filtering
- Search by test name
4. **Test Analytics**
- Trend charts (pass rate over time)
- Flaky test detection
- Duration trends
- Test coverage metrics
5. **Bulk Actions**
- Run tests for multiple packs
- Batch pack registration
- Export test results (CSV, JSON)
6. **Notifications**
- Browser notifications for test completion
- Email alerts for test failures
- Slack/webhook integrations
## Development Notes
### File Structure
```
web/src/
├── components/
│ └── packs/
│ ├── PackTestResult.tsx # Detailed test result display
│ ├── PackTestBadge.tsx # Status badge component
│ └── PackTestHistory.tsx # Test history list
├── hooks/
│ └── usePackTests.ts # React Query hooks
└── pages/
└── packs/
├── PackDetailPage.tsx # Shows latest test results
└── PackRegisterPage.tsx # Pack registration form
```
### Adding New Components
1. Create component in `components/packs/`
2. Export from component file
3. Add to relevant pages
4. Update types if needed
5. Test build: `npm run build`
### API Client Updates
When backend API changes:
1. Start API server: `cargo run --bin attune-api`
2. Regenerate client: `cd web && npm run generate:api`
3. Update hook imports if service names changed
4. Update TypeScript types in hooks
5. Test and update components
### Testing
Manual testing checklist:
- [ ] Pack list page loads
- [ ] Pack detail page shows test results
- [ ] Test history displays correctly
- [ ] Run tests button works
- [ ] Pack registration form submits
- [ ] Success/error messages display
- [ ] Redirects work correctly
- [ ] Test details expand/collapse
- [ ] Status badges show correct colors
- [ ] Mobile layout works
## Troubleshooting
### "Cannot read property 'result' of undefined"
Test data not loaded yet. Add loading check:
```tsx
{latestTest?.data && <PackTestResult result={latestTest.data.result} />}
```
### Test history not updating
Manually invalidate queries after test execution:
```tsx
queryClient.invalidateQueries({ queryKey: ['pack-tests', packRef] });
```
### Styling not applied
- Check Tailwind classes are valid
- Verify `lucide-react` is installed
- Run `npm install` to ensure dependencies
- Clear build cache: `rm -rf dist && npm run build`
### API calls failing (401/403)
- Check access token in localStorage
- Verify token not expired
- Log in again to refresh token
- Check CORS configuration
## Related Documentation
- [Pack Testing Framework](./pack-testing-framework.md) - Overall testing design
- [Pack Testing User Guide](./PACK_TESTING.md) - CLI and API usage
- [Pack Testing API](./api-pack-testing.md) - API endpoint reference
- [Pack Install Integration](./pack-install-testing.md) - Installation with testing
- [Web UI Architecture](./web-ui-architecture.md) - Frontend architecture
## Changelog
- **2026-01-22**: Initial web UI integration
- Pack test result display component
- Test history component
- Status badge component
- Pack registration page
- React Query hooks for test data
- Integration with pack detail page

View File

@@ -0,0 +1,318 @@
# WebSocket Usage in Web UI
## Overview
The Attune web UI uses a **single shared WebSocket connection** for real-time notifications across all pages. This connection is managed by `WebSocketProvider` and accessed via React hooks.
## Architecture
```
App.tsx
└── WebSocketProvider (manages single WS connection)
├── EventsPage (subscribes to "event" notifications)
├── ExecutionsPage (subscribes to "execution" notifications)
└── DashboardPage (subscribes to multiple entity types)
```
Only **one WebSocket connection** is created per browser tab, regardless of how many pages subscribe to notifications.
## Basic Usage
### 1. Ensure Provider is Configured
The `WebSocketProvider` should be set up in `App.tsx` (already configured):
```typescript
import { WebSocketProvider } from "@/contexts/WebSocketContext";
function App() {
return (
<WebSocketProvider>
{/* Your app routes */}
</WebSocketProvider>
);
}
```
### 2. Subscribe to Entity Notifications
Use `useEntityNotifications` to receive real-time updates for a specific entity type:
```typescript
import { useCallback } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useEntityNotifications } from "@/contexts/WebSocketContext";
function MyPage() {
const queryClient = useQueryClient();
// IMPORTANT: Wrap handler in useCallback for stable reference
const handleNotification = useCallback(() => {
// Invalidate queries to refetch data
queryClient.invalidateQueries({ queryKey: ["myEntity"] });
}, [queryClient]);
const { connected } = useEntityNotifications("myEntity", handleNotification);
return (
<div>
{connected && <span>Live updates enabled</span>}
{/* Rest of your component */}
</div>
);
}
```
## Available Entity Types
Subscribe to these entity types to receive notifications:
- `"event"` - Event creation/updates
- `"execution"` - Execution status changes
- `"enforcement"` - Rule enforcement events
- `"inquiry"` - Human-in-the-loop interactions
- `"action"` - Action changes (from pack updates)
- `"rule"` - Rule changes
- `"trigger"` - Trigger changes
- `"sensor"` - Sensor changes
## Advanced Usage
### Multiple Subscriptions in One Component
```typescript
function DashboardPage() {
const queryClient = useQueryClient();
const handleEventNotification = useCallback(() => {
queryClient.invalidateQueries({ queryKey: ["events"] });
}, [queryClient]);
const handleExecutionNotification = useCallback(() => {
queryClient.invalidateQueries({ queryKey: ["executions"] });
}, [queryClient]);
useEntityNotifications("event", handleEventNotification);
useEntityNotifications("execution", handleExecutionNotification);
// Both subscriptions share the same WebSocket connection
}
```
### Custom Notification Handling
```typescript
import { useCallback } from "react";
import { useEntityNotifications, Notification } from "@/contexts/WebSocketContext";
function MyPage() {
const handleNotification = useCallback((notification: Notification) => {
console.log("Received notification:", notification);
// Access notification details
console.log("Entity type:", notification.entity_type);
console.log("Entity ID:", notification.entity_id);
console.log("Payload:", notification.payload);
console.log("Timestamp:", notification.timestamp);
// Custom logic based on notification
if (notification.entity_id === specificId) {
// Do something specific
}
}, []);
useEntityNotifications("execution", handleNotification);
}
```
### Conditional Subscriptions
```typescript
function MyPage({ entityType }: { entityType: string }) {
const [enabled, setEnabled] = useState(true);
const handleNotification = useCallback(() => {
// Handle notification
}, []);
// Only subscribe when enabled is true
useEntityNotifications(entityType, handleNotification, enabled);
}
```
### Direct Context Access
For advanced use cases, access the WebSocket context directly:
```typescript
import { useWebSocketContext } from "@/contexts/WebSocketContext";
function AdvancedComponent() {
const { connected, subscribe, unsubscribe } = useWebSocketContext();
useEffect(() => {
const handler = (notification) => {
// Custom handling
};
// Subscribe to a custom filter
subscribe("entity_type:execution", handler);
return () => {
unsubscribe("entity_type:execution", handler);
};
}, [subscribe, unsubscribe]);
}
```
## Important Guidelines
### ✅ DO
- **Always wrap handlers in `useCallback`** to prevent re-subscriptions
- Include all dependencies in the `useCallback` dependency array
- Use `queryClient.invalidateQueries` for simple cache invalidation
- Show connection status in the UI when relevant
### ❌ DON'T
- Don't create inline functions as handlers (causes re-subscriptions)
- Don't create multiple WebSocket connections (use the context)
- Don't forget to handle the `connected` state
- Don't use the deprecated `useWebSocket` from `@/hooks/useWebSocket.ts`
## Connection Status
Display connection status to users:
```typescript
function MyPage() {
const { connected } = useEntityNotifications("event", handleNotification);
return (
<div>
{connected ? (
<div className="flex items-center gap-2 text-green-600">
<div className="w-2 h-2 bg-green-600 rounded-full animate-pulse" />
<span>Live updates</span>
</div>
) : (
<div className="text-gray-400">Connecting...</div>
)}
</div>
);
}
```
## Configuration
WebSocket connection is configured via environment variables:
```bash
# .env.local or .env.development
VITE_WS_URL=ws://localhost:8081
```
The provider automatically appends `/ws` to the URL if not present.
## Troubleshooting
### Multiple Connections Opening
**Problem**: Browser DevTools shows multiple WebSocket connections.
**Solution**:
- Ensure `WebSocketProvider` is only added once in `App.tsx`
- Check that handlers are wrapped in `useCallback`
- Verify React StrictMode isn't causing double-mounting in development
### Notifications Not Received
**Problem**: Component doesn't receive notifications.
**Checklist**:
1. Is `WebSocketProvider` wrapping your app?
2. Is the notifier service running? (`make run-notifier`)
3. Is `connected` returning `true`?
4. Is the entity type spelled correctly?
5. Are notifications actually being sent? (check server logs)
### Connection Keeps Reconnecting
**Problem**: WebSocket disconnects and reconnects repeatedly.
**Possible causes**:
- Notifier service restarting
- Network issues
- Authentication token expired (WebSocket doesn't currently use auth)
- Check console for error messages
## Example: Complete Page
```typescript
import { useState, useCallback } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useEntityNotifications } from "@/contexts/WebSocketContext";
import { useEvents } from "@/hooks/useEvents";
export default function EventsPage() {
const queryClient = useQueryClient();
const [page, setPage] = useState(1);
// Real-time updates
const handleEventNotification = useCallback(() => {
queryClient.invalidateQueries({ queryKey: ["events"] });
}, [queryClient]);
const { connected } = useEntityNotifications("event", handleEventNotification);
// Fetch data
const { data, isLoading } = useEvents({ page, pageSize: 20 });
return (
<div>
<div className="flex items-center justify-between">
<h1>Events</h1>
{connected && (
<div className="text-green-600">
<div className="w-2 h-2 bg-green-600 rounded-full animate-pulse" />
Live updates
</div>
)}
</div>
{/* Render events */}
</div>
);
}
```
## Performance Notes
- The single shared connection significantly reduces network overhead
- Subscriptions are reference-counted (only subscribe to each filter once)
- Handlers are called synchronously when notifications arrive
- No polling is needed - all updates are push-based via WebSocket
## Migration from Old Pattern
If you find code using the old pattern:
```typescript
// ❌ OLD - creates separate connection
import { useEntityNotifications } from "@/hooks/useWebSocket";
const { connected } = useEntityNotifications("event", () => {
queryClient.invalidateQueries({ queryKey: ["events"] });
});
```
Update to:
```typescript
// ✅ NEW - uses shared connection
import { useEntityNotifications } from "@/contexts/WebSocketContext";
const handleNotification = useCallback(() => {
queryClient.invalidateQueries({ queryKey: ["events"] });
}, [queryClient]);
const { connected } = useEntityNotifications("event", handleNotification);
```