feat: implement page components for ShadowRun and Models features
Add 4 Vue 3 pages with KBX adapter integration: Pages: - ShadowRunList.vue (252+ day validation search & grid) - ShadowRunDetail.vue (metrics breakdown, phase analysis) - ModelsList.vue (lifecycle management, quick filters) - ModelDetail.vue (activation requirements, configuration) Features: - Registry-driven screen definitions - KbxListPage + KbxDataGrid + KbxButton adapters - Mock data (replaced with TanStack Query in Task F) - Keyboard shortcuts (F3, Ctrl+N, Escape, Ctrl+E) - Responsive density-aware layout - Validation indicators (PBO, DSR, OOS thresholds) - Phase lifecycle visualization - Quick filter badges Implementation pattern: 1. useKbxRegistry() for screen access 2. Computed state for data state management 3. useRoute/useRouter for navigation 4. Slots for flexible layout composition Ready for Task F: TanStack Query API integration Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import KbxListPage from '@shared/ui/adapter/KbxListPage.vue'
|
||||
import KbxDataGrid from '@shared/ui/adapter/KbxDataGrid.vue'
|
||||
import KbxButton from '@shared/ui/adapter/KbxButton.vue'
|
||||
import KbxInput from '@shared/ui/adapter/KbxInput.vue'
|
||||
import { useKbxRegistry } from '@shared/composables/useKbxRegistry'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const registry = useKbxRegistry()
|
||||
|
||||
// Get screen definition from registry
|
||||
const screenDef = computed(() =>
|
||||
registry.getScreen('model-ops.shadow-run.list'),
|
||||
)
|
||||
|
||||
// Search and filter state
|
||||
const searchQuery = ref('')
|
||||
const statusFilter = ref('all')
|
||||
const dateRangeStart = ref('')
|
||||
const dateRangeEnd = ref('')
|
||||
|
||||
// Pagination
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(50)
|
||||
|
||||
// Mock data state (replace with actual API query)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const dataState = computed<'idle' | 'pending' | 'ready' | 'error' | 'empty'>(() => {
|
||||
if (isLoading.value) return 'pending'
|
||||
if (error.value) return 'error'
|
||||
if (shadowRuns.value.length === 0) return 'empty'
|
||||
return 'ready'
|
||||
})
|
||||
|
||||
// Mock shadow runs data (will be replaced with TanStack Query)
|
||||
const shadowRuns = ref([
|
||||
{
|
||||
runId: '00000000-0000-0000-0000-000000000001',
|
||||
modelName: 'Alpha Strategy',
|
||||
windowStart: '2024-01-02',
|
||||
windowEnd: '2024-09-10',
|
||||
tradingDays: 187,
|
||||
totalReturn: 12.5,
|
||||
sharpeRatio: 1.8,
|
||||
pbo: 15.2,
|
||||
dsr: 96.5,
|
||||
oos: 1.8,
|
||||
status: 'completed',
|
||||
createdAt: '2026-08-01',
|
||||
},
|
||||
{
|
||||
runId: '00000000-0000-0000-0000-000000000002',
|
||||
modelName: 'Beta Model',
|
||||
windowStart: '2024-01-02',
|
||||
windowEnd: '2024-09-10',
|
||||
tradingDays: 187,
|
||||
totalReturn: -2.3,
|
||||
sharpeRatio: -0.5,
|
||||
pbo: 42.1,
|
||||
dsr: 45.2,
|
||||
oos: 3.2,
|
||||
status: 'completed',
|
||||
createdAt: '2026-08-02',
|
||||
},
|
||||
])
|
||||
|
||||
// Quick filters
|
||||
const quickFilters = computed(() => [
|
||||
{ id: 'all', label: 'All', active: statusFilter.value === 'all', badge: shadowRuns.value.length },
|
||||
{ id: 'valid', label: 'Valid', active: statusFilter.value === 'valid', badge: 1 },
|
||||
{ id: 'review', label: 'Review', active: statusFilter.value === 'review', badge: 1 },
|
||||
])
|
||||
|
||||
// Summary items
|
||||
const summaryItems = computed(() => [
|
||||
{ label: 'Total Runs', value: shadowRuns.value.length },
|
||||
{ label: 'Valid', value: 1 },
|
||||
{ label: 'Avg Sharpe', value: '0.65' },
|
||||
])
|
||||
|
||||
// Actions
|
||||
const handleSearch = () => {
|
||||
isLoading.value = true
|
||||
setTimeout(() => {
|
||||
isLoading.value = false
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const handleNewRun = () => {
|
||||
router.push('/model-ops/shadow-runs/new')
|
||||
}
|
||||
|
||||
const handleRowClick = (runId: string) => {
|
||||
router.push(`/model-ops/shadow-runs/${runId}`)
|
||||
}
|
||||
|
||||
const handleQuickFilter = (filterId: string) => {
|
||||
statusFilter.value = filterId
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
const handleKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'F3') {
|
||||
e.preventDefault()
|
||||
handleSearch()
|
||||
} else if (e.ctrlKey && e.key === 'n') {
|
||||
e.preventDefault()
|
||||
handleNewRun()
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="screenDef" class="shadow-run-list">
|
||||
<KbxListPage
|
||||
:screen="screenDef"
|
||||
:data-state="dataState"
|
||||
:loading="isLoading"
|
||||
:summary-items="summaryItems"
|
||||
:quick-filters="quickFilters"
|
||||
@quick-filter="handleQuickFilter"
|
||||
@refresh="handleRefresh"
|
||||
>
|
||||
<!-- Header Actions -->
|
||||
<template #header-actions>
|
||||
<KbxButton
|
||||
label="New Shadow Run"
|
||||
variant="primary"
|
||||
@click="handleNewRun"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Search Panel -->
|
||||
<template #search>
|
||||
<div class="shadow-run-search">
|
||||
<div class="search-row">
|
||||
<KbxInput
|
||||
v-model="searchQuery"
|
||||
placeholder="Search by model name..."
|
||||
@keydown.enter="handleSearch"
|
||||
/>
|
||||
<KbxButton
|
||||
label="Search"
|
||||
variant="default"
|
||||
@click="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="search-row">
|
||||
<KbxInput
|
||||
v-model="dateRangeStart"
|
||||
type="date"
|
||||
placeholder="Start Date"
|
||||
/>
|
||||
<KbxInput
|
||||
v-model="dateRangeEnd"
|
||||
type="date"
|
||||
placeholder="End Date"
|
||||
/>
|
||||
<select v-model="statusFilter" class="status-filter">
|
||||
<option value="all">All Status</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="running">Running</option>
|
||||
<option value="failed">Failed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Content Area -->
|
||||
<template #content>
|
||||
<KbxDataGrid
|
||||
v-if="screenDef.grid"
|
||||
:columns="screenDef.grid.columnDefs"
|
||||
:rows="shadowRuns"
|
||||
:loading="isLoading"
|
||||
@row-click="(runId) => handleRowClick(runId)"
|
||||
/>
|
||||
</template>
|
||||
</KbxListPage>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shadow-run-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.shadow-run-search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--kbx-color-surface, #f5f5f5);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-row input,
|
||||
.search-row select {
|
||||
height: var(--kbx-input-height, 34px);
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 4px;
|
||||
font-size: var(--kbx-font-size, 14px);
|
||||
}
|
||||
|
||||
.status-filter {
|
||||
flex: 0 0 120px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: var(--kbx-color-primary, #3b82f6);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.state-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #d0d0d0;
|
||||
border-top-color: var(--kbx-color-primary, #3b82f6);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user