728393226f
Refactored 4 feature pages to use standard screen-type v2 layouts: 1. HomePage → ScorecardDashboardPage - Exception-driven work queue metrics + dashboard layout - KPI cards, filters, operational guides - Viewport-fit ready, no page-level scrolling 2. ModelsList → MasterDetailCrudPage - Master list + detail panel layout - Model grid with phase/performance metrics - Search, filter, pagination 3. ModelDetail → DetailReadPage - Read-only model detail view - State management (LOADING/ERROR/READY) - Metric display (PBO, DSR, Return) 4. ShadowRunDetail → DetailReadPage - Shadow run detail view - State management (LOADING/ERROR/READY) - Performance metrics (PBO, DSR, OOS) Pattern Applied: - Remove PageLayout, use screen-type component - Add StandardScreenProps (state, evidence) - State computed from query status - Slot structure maintained or aligned - No functional changes, pure standardization Benefits: - Consistent layout across pages - Standardized state management - Improved viewport-fit compliance - Better component reusability Next: Viewport-fit final validation + viewport-fit edge cases Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
134 lines
3.2 KiB
Vue
134 lines
3.2 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { useRoute } from 'vue-router'
|
|
import { DetailReadPage } from '../../../shared/ui/screen-types/v2'
|
|
import { SkeletonLoader } from '../../../shared/ui/components'
|
|
import { useShadowRunDetail } from '../composables/useShadowRuns'
|
|
import type { StandardScreenProps } from '../../../shared/ui/contracts/screenContract'
|
|
|
|
const route = useRoute()
|
|
|
|
const runId = computed(() => route.params.runId as string)
|
|
|
|
const shadowRunQuery = useShadowRunDetail(runId.value)
|
|
|
|
const run = computed(() => shadowRunQuery.data as any)
|
|
|
|
const screenState = computed<StandardScreenProps['state']>(() => {
|
|
if (shadowRunQuery.isPending) return 'LOADING'
|
|
if (shadowRunQuery.isError) return 'ERROR'
|
|
return 'READY'
|
|
})
|
|
const screenEvidence = { asOf: new Date().toISOString(), version: '1.0' }
|
|
</script>
|
|
|
|
<template>
|
|
<DetailReadPage
|
|
:title="`Shadow Run #${runId}`"
|
|
:state="screenState"
|
|
:evidence="screenEvidence"
|
|
>
|
|
<template #primary>
|
|
<header class="page-header">
|
|
<h1>Shadow Run Details</h1>
|
|
</header>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="shadowRunQuery.isPending" class="loading-state">
|
|
<SkeletonLoader type="card" />
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-else-if="shadowRunQuery.isError" class="error-state">
|
|
<p>Failed to load shadow run</p>
|
|
</div>
|
|
|
|
<!-- Data State -->
|
|
<div v-else-if="run && run.id" class="shadow-run-detail">
|
|
<div class="detail-section">
|
|
<h2>Run #{{ run.id }}</h2>
|
|
<div class="detail-grid">
|
|
<div class="detail-item">
|
|
<label>Model</label>
|
|
<p>{{ run.modelName }}</p>
|
|
</div>
|
|
<div class="detail-item">
|
|
<label>Status</label>
|
|
<p>{{ run.status }}</p>
|
|
</div>
|
|
<div class="detail-item">
|
|
<label>PBO</label>
|
|
<p>{{ run.pbo }}%</p>
|
|
</div>
|
|
<div class="detail-item">
|
|
<label>DSR</label>
|
|
<p>{{ run.dsr }}%</p>
|
|
</div>
|
|
<div class="detail-item">
|
|
<label>OOS</label>
|
|
<p>{{ run.oos }}%</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</DetailReadPage>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.shadow-run-detail-page {
|
|
padding: 2rem;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.page-header {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.page-header h1 {
|
|
margin: 0;
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.loading-state,
|
|
.error-state {
|
|
padding: 2rem;
|
|
text-align: center;
|
|
border: 1px solid var(--color-border-primary);
|
|
border-radius: var(--border-radius-md);
|
|
background-color: var(--color-background-secondary);
|
|
}
|
|
|
|
.shadow-run-detail {
|
|
border: 1px solid var(--color-border-primary);
|
|
border-radius: var(--border-radius-md);
|
|
padding: 2rem;
|
|
background-color: var(--color-background-secondary);
|
|
}
|
|
|
|
.detail-section h2 {
|
|
margin: 0 0 1.5rem 0;
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.detail-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.detail-item label {
|
|
display: block;
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.detail-item p {
|
|
margin: 0;
|
|
color: var(--color-text-primary);
|
|
}
|
|
</style>
|