81 lines
3.5 KiB
TypeScript
81 lines
3.5 KiB
TypeScript
import React, { useState, useCallback } from 'react'
|
|
import { View, Text, ScrollView, StyleSheet, RefreshControl, TouchableOpacity } from 'react-native'
|
|
import { useFocusEffect } from 'expo-router'
|
|
import { COLORS } from '../../constants/Config'
|
|
import client from '../../services/api'
|
|
|
|
type Period = '30d' | '60d' | '90d'
|
|
|
|
export default function CapacityPlanScreen() {
|
|
const [data, setData] = useState<any>(null)
|
|
const [period, setPeriod] = useState<Period>('30d')
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true)
|
|
try { const r = await client.get(`/api/capacity/predictions?days=${period.replace('d', '')}`); setData(r.data) }
|
|
catch { setData(null) }
|
|
finally { setLoading(false) }
|
|
}, [period])
|
|
|
|
useFocusEffect(useCallback(() => { load() }, [load]))
|
|
|
|
const predictions = data?.predictions ?? data?.items ?? []
|
|
|
|
return (
|
|
<ScrollView style={s.container} refreshControl={<RefreshControl refreshing={loading} onRefresh={load} />}>
|
|
<View style={s.tabs}>
|
|
{(['30d','60d','90d'] as Period[]).map(p => (
|
|
<TouchableOpacity key={p} style={[s.tab, period===p && s.tabActive]} onPress={() => setPeriod(p)}>
|
|
<Text style={[s.tabText, period===p && s.tabTextActive]}>{p}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
|
|
{predictions.map((item: any, i: number) => {
|
|
const util = item.predicted_utilization ?? item.cpu_avg ?? 0
|
|
const color = util >= 90 ? COLORS.danger : util >= 70 ? COLORS.warning : COLORS.success
|
|
return (
|
|
<View key={i} style={s.card}>
|
|
<View style={s.row}>
|
|
<View style={{ flex: 1 }}>
|
|
<Text style={s.serverName}>{item.server_name ?? item.name ?? 'N/A'}</Text>
|
|
<Text style={s.meta}>{item.inst_name ?? ''} · {item.resource_type ?? 'CPU'}</Text>
|
|
</View>
|
|
<Text style={[s.pct, { color }]}>{util}%</Text>
|
|
</View>
|
|
<View style={s.barBg}>
|
|
<View style={[s.barFill, { width: `${Math.min(100, util)}%`, backgroundColor: color }]} />
|
|
</View>
|
|
{item.recommendation && (
|
|
<Text style={s.rec}>AI 권고: {item.recommendation}</Text>
|
|
)}
|
|
</View>
|
|
)
|
|
})}
|
|
|
|
{predictions.length === 0 && !loading && (
|
|
<Text style={s.empty}>예측 데이터가 없습니다.</Text>
|
|
)}
|
|
</ScrollView>
|
|
)
|
|
}
|
|
|
|
const s = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: COLORS.bg },
|
|
tabs: { flexDirection: 'row', backgroundColor: '#fff', borderBottomWidth: 1, borderBottomColor: COLORS.border },
|
|
tab: { flex: 1, paddingVertical: 12, alignItems: 'center' },
|
|
tabActive: { borderBottomWidth: 2, borderBottomColor: COLORS.accent },
|
|
tabText: { fontSize: 13, color: COLORS.muted },
|
|
tabTextActive:{ color: COLORS.accent, fontWeight: '700' },
|
|
card: { backgroundColor: '#fff', margin: 8, marginBottom: 0, borderRadius: 10, padding: 14, elevation: 1 },
|
|
row: { flexDirection: 'row', alignItems: 'center', marginBottom: 8 },
|
|
serverName: { fontSize: 14, fontWeight: '700', color: COLORS.text },
|
|
meta: { fontSize: 12, color: COLORS.muted, marginTop: 2 },
|
|
pct: { fontSize: 22, fontWeight: '800' },
|
|
barBg: { height: 6, backgroundColor: COLORS.border, borderRadius: 3, marginBottom: 6 },
|
|
barFill: { height: 6, borderRadius: 3 },
|
|
rec: { fontSize: 12, color: COLORS.blue, fontStyle: 'italic' },
|
|
empty: { textAlign: 'center', color: COLORS.muted, marginTop: 40 },
|
|
})
|