Files
KArtSell.Aegis/frontend/src/features/models/pages/ModelsList.vue
T
kjh2064 b7ee740f71 feat: integrate TanStack Query for data fetching (Task F)
Add composables for API integration with TanStack Query:

1. useShadowRuns.ts (shadow-run feature)
   - useShadowRunsList() with pagination & filtering
   - useShadowRunDetail() for detail view
   - useCreateShadowRun() mutation
   - useRefreshShadowRuns() for manual refresh
   - shadowRunQueryKeys factory for cache management

2. useModels.ts (models feature)
   - useModelsList() with filtering by phase/active
   - useModelDetail() for detail view
   - useActivateModel() / useDeactivateModel() mutations
   - useTransitionPhase() for lifecycle transitions
   - modelQueryKeys factory

3. Updated pages (ShadowRunList, ShadowRunDetail, ModelsList, ModelDetail)
   - Replace mock data with useQuery hooks
   - Auto-refetch on filter changes
   - Optimistic updates (activate/deactivate/transition)
   - Computed state management (idle/pending/ready/error/empty)
   - Cache invalidation on mutations

Features:
- Stale time: 5 minutes, garbage collection: 10 minutes
- Query key factories for cache management
- Mock API client (replace with real HTTP endpoints)
- Support for pagination, filtering, sorting
- Keyboard shortcuts still functional

Mock API client provides realistic data for testing.
Replace apiClient.* functions with actual HTTP calls in next phase.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-12 01:46:25 +09:00

232 lines
6.2 KiB
Vue

<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } 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'
import { useModelsList } from '../composables/useModels'
import type { ModelListParams } from '../composables/useModels'
const route = useRoute()
const router = useRouter()
const registry = useKbxRegistry()
// Get screen definition from registry
const screenDef = computed(() =>
registry.getScreen('model-ops.models.list'),
)
// Search and filter state
const searchQuery = ref('')
const phaseFilter = ref('all')
const activeFilter = ref('all')
// Pagination
const currentPage = ref(1)
const pageSize = ref(50)
// Query parameters
const queryParams = computed<ModelListParams>(() => ({
page: currentPage.value,
pageSize: pageSize.value,
search: searchQuery.value || undefined,
phase: phaseFilter.value === 'all' ? undefined : phaseFilter.value,
active: activeFilter.value === 'active' ? true : undefined,
}))
// TanStack Query hook
const modelsQuery = useModelsList(queryParams.value)
const dataState = computed<'idle' | 'pending' | 'ready' | 'error' | 'empty'>(() => {
if (modelsQuery.isPending.value) return 'pending'
if (modelsQuery.isError.value) return 'error'
if (modelsQuery.data.value?.items.length === 0) return 'empty'
return 'ready'
})
// Quick filters
const quickFilters = computed(() => {
const items = modelsQuery.data.value?.items || []
return [
{ id: 'all', label: 'All', active: phaseFilter.value === 'all', badge: items.length },
{ id: 'active', label: 'Active', active: activeFilter.value === 'active', badge: items.filter(m => m.active).length },
{ id: 'ready', label: 'Ready to Deploy', active: phaseFilter.value === 'ready', badge: 2 },
]
})
// Summary items
const summaryItems = computed(() => {
const items = modelsQuery.data.value?.items || []
const avgPbo = items.length > 0 ? (items.reduce((sum, m) => sum + m.pbo, 0) / items.length).toFixed(1) : '0'
return [
{ label: 'Total Models', value: items.length },
{ label: 'Active', value: items.filter(m => m.active).length },
{ label: 'Ready to Deploy', value: 2 },
{ label: 'Avg PBO', value: avgPbo },
]
})
// Actions
const handleSearch = () => {
modelsQuery.refetch()
}
const handleNewModel = () => {
router.push('/model-ops/models/new')
}
const handleRowClick = (modelId: string) => {
router.push(`/model-ops/models/${modelId}`)
}
const handleQuickFilter = (filterId: string) => {
if (filterId === 'active') {
activeFilter.value = activeFilter.value === 'active' ? 'all' : 'active'
} else {
phaseFilter.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()
handleNewModel()
}
}
onMounted(() => {
window.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeydown)
})
</script>
<template>
<div v-if="screenDef" class="models-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 Model"
variant="primary"
@click="handleNewModel"
/>
</template>
<!-- Search Panel -->
<template #search>
<div class="models-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">
<select v-model="phaseFilter" class="phase-filter">
<option value="all">All Phases</option>
<option value="freeze">Freeze</option>
<option value="mature">Mature</option>
<option value="score">Score</option>
<option value="diagnose">Diagnose</option>
<option value="hypothesis">Hypothesis</option>
<option value="challenger">Challenger</option>
<option value="validate">Validate</option>
<option value="review">Review</option>
</select>
<select v-model="activeFilter" class="active-filter">
<option value="all">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
</template>
<!-- Content Area -->
<template #content>
<KbxDataGrid
v-if="screenDef.grid && modelsQuery.data.value?.items"
:columns="screenDef.grid.columnDefs"
:rows="modelsQuery.data.value.items"
:loading="modelsQuery.isPending.value"
@row-click="handleRowClick"
/>
</template>
</KbxListPage>
</div>
</template>
<style scoped>
.models-list {
display: flex;
flex-direction: column;
height: 100%;
}
.models-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);
}
.phase-filter,
.active-filter {
flex: 0 0 140px;
}
.badge {
background: var(--kbx-color-primary, #3b82f6);
color: white;
padding: 2px 6px;
border-radius: 12px;
font-size: 11px;
margin-left: 4px;
}
</style>