Initial commit of akmon project
This commit is contained in:
898
uni_modules/ak-ai-news/components/AINewsDashboard.vue
Normal file
898
uni_modules/ak-ai-news/components/AINewsDashboard.vue
Normal file
@@ -0,0 +1,898 @@
|
||||
<!-- Real-time AI News System Monitoring Dashboard -->
|
||||
<template>
|
||||
<view class="dashboard-container">
|
||||
<!-- Header -->
|
||||
<view class="dashboard-header">
|
||||
<text class="dashboard-title">AI News System Dashboard</text>
|
||||
<view class="status-indicator" :class="systemHealth.status">
|
||||
<text class="status-text">{{ systemHealth.status.toUpperCase() }}</text>
|
||||
<text class="health-score">{{ systemHealth.score }}/100</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<view class="quick-stats">
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ formatNumber(stats.requests.total) }}</text>
|
||||
<text class="stat-label">Total Requests</text>
|
||||
<text class="stat-change" :class="getChangeClass(requestsChange)">
|
||||
{{ formatChange(requestsChange) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ (stats.requests.successRate * 100).toFixed(1) }}%</text>
|
||||
<text class="stat-label">Success Rate</text>
|
||||
<text class="stat-change" :class="getChangeClass(successRateChange)">
|
||||
{{ formatChange(successRateChange) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{ stats.timing.averageLatency.toFixed(0) }}ms</text>
|
||||
<text class="stat-label">Avg Latency</text>
|
||||
<text class="stat-change" :class="getChangeClass(-latencyChange)">
|
||||
{{ formatChange(latencyChange) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">${{ stats.costs.total.toFixed(2) }}</text>
|
||||
<text class="stat-label">Total Cost</text>
|
||||
<text class="stat-change" :class="getChangeClass(-costChange)">
|
||||
${{ costChange.toFixed(2) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Health Checks -->
|
||||
<view class="health-section">
|
||||
<text class="section-title">System Health Checks</text>
|
||||
<view class="health-checks">
|
||||
<view class="health-check"
|
||||
v-for="(check, key) in systemHealth.checks"
|
||||
:key="key"
|
||||
:class="getHealthCheckClass(key, check)">
|
||||
<text class="check-name">{{ formatCheckName(key) }}</text>
|
||||
<text class="check-value">{{ formatCheckValue(key, check) }}</text>
|
||||
<view class="check-indicator" :class="getHealthCheckClass(key, check)"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Active Alerts -->
|
||||
<view class="alerts-section" v-if="systemHealth.alerts.length > 0">
|
||||
<text class="section-title">Active Alerts ({{ systemHealth.alerts.length }})</text>
|
||||
<scroll-view class="alerts-list" scroll-y="true">
|
||||
<view class="alert-item"
|
||||
v-for="alert in systemHealth.alerts"
|
||||
:key="alert.id"
|
||||
:class="alert.severity">
|
||||
<view class="alert-header">
|
||||
<text class="alert-severity">{{ alert.severity.toUpperCase() }}</text>
|
||||
<text class="alert-time">{{ formatTime(alert.timestamp) }}</text>
|
||||
</view>
|
||||
<text class="alert-message">{{ alert.message }}</text>
|
||||
<text class="alert-source">Source: {{ alert.source }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- Performance Charts -->
|
||||
<view class="charts-section">
|
||||
<text class="section-title">Performance Trends</text>
|
||||
|
||||
<!-- Response Time Chart -->
|
||||
<view class="chart-container">
|
||||
<text class="chart-title">Response Time (Last Hour)</text>
|
||||
<view class="chart-area">
|
||||
<canvas class="chart-canvas"
|
||||
canvas-id="responseTimeChart"
|
||||
@touchstart="onChartTouch"
|
||||
@touchend="onChartTouchEnd"></canvas>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Request Volume Chart -->
|
||||
<view class="chart-container">
|
||||
<text class="chart-title">Request Volume</text>
|
||||
<view class="chart-area">
|
||||
<canvas class="chart-canvas"
|
||||
canvas-id="requestVolumeChart"></canvas>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Cost Distribution -->
|
||||
<view class="chart-container">
|
||||
<text class="chart-title">Cost by Provider</text>
|
||||
<view class="cost-breakdown">
|
||||
<view class="cost-item" v-for="(cost, provider) in stats.costs.byProvider" :key="provider">
|
||||
<view class="cost-bar-container">
|
||||
<text class="provider-name">{{ provider }}</text>
|
||||
<view class="cost-bar">
|
||||
<view class="cost-fill"
|
||||
:style="{ width: (cost / stats.costs.total * 100) + '%' }"></view>
|
||||
</view>
|
||||
<text class="cost-value">${{ cost.toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Optimization Recommendations -->
|
||||
<view class="recommendations-section" v-if="recommendations.length > 0">
|
||||
<text class="section-title">Optimization Recommendations</text>
|
||||
<scroll-view class="recommendations-list" scroll-y="true">
|
||||
<view class="recommendation-item"
|
||||
v-for="(rec, index) in recommendations"
|
||||
:key="index"
|
||||
:class="rec.priority">
|
||||
<view class="rec-header">
|
||||
<text class="rec-type">{{ rec.type.toUpperCase() }}</text>
|
||||
<text class="rec-priority">{{ rec.priority.toUpperCase() }}</text>
|
||||
</view>
|
||||
<text class="rec-description">{{ rec.description }}</text>
|
||||
<view class="rec-impact">
|
||||
<text class="impact-title">Expected Impact:</text>
|
||||
<text v-if="rec.expectedImpact.performanceGain" class="impact-item">
|
||||
🚀 {{ rec.expectedImpact.performanceGain }}
|
||||
</text>
|
||||
<text v-if="rec.expectedImpact.costSaving" class="impact-item">
|
||||
💰 {{ rec.expectedImpact.costSaving }}
|
||||
</text>
|
||||
<text v-if="rec.expectedImpact.reliabilityImprovement" class="impact-item">
|
||||
🛡️ {{ rec.expectedImpact.reliabilityImprovement }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="rec-actions">
|
||||
<button class="btn-apply" @click="applyRecommendation(rec)" :disabled="rec.applying">
|
||||
{{ rec.applying ? 'Applying...' : 'Apply' }}
|
||||
</button>
|
||||
<button class="btn-dismiss" @click="dismissRecommendation(rec)">
|
||||
Dismiss
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- Control Panel -->
|
||||
<view class="controls-section">
|
||||
<text class="section-title">Controls</text>
|
||||
<view class="control-buttons">
|
||||
<button class="control-btn" @click="refreshData" :disabled="isRefreshing">
|
||||
{{ isRefreshing ? 'Refreshing...' : 'Refresh Data' }}
|
||||
</button>
|
||||
<button class="control-btn" @click="exportData">
|
||||
Export Metrics
|
||||
</button>
|
||||
<button class="control-btn" @click="toggleAutoRefresh">
|
||||
{{ autoRefresh ? 'Stop Auto-Refresh' : 'Start Auto-Refresh' }}
|
||||
</button>
|
||||
<button class="control-btn danger" @click="clearAlerts">
|
||||
Clear Alerts
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
|
||||
import {
|
||||
AIPerformanceMonitor,
|
||||
type PerformanceStats,
|
||||
type SystemHealth,
|
||||
type OptimizationRecommendation,
|
||||
defaultPerformanceConfig
|
||||
} from '../services/AIPerformanceMonitor.uts'
|
||||
|
||||
// Reactive data
|
||||
const monitor = new AIPerformanceMonitor(defaultPerformanceConfig)
|
||||
const stats = ref<PerformanceStats>({
|
||||
timeRange: { start: 0, end: 0, duration: 0 },
|
||||
requests: { total: 0, successful: 0, failed: 0, successRate: 0 },
|
||||
timing: { averageLatency: 0, medianLatency: 0, p95Latency: 0, p99Latency: 0 },
|
||||
costs: { total: 0, average: 0, byProvider: {} },
|
||||
cache: { hitRate: 0, totalRequests: 0, hits: 0, misses: 0 },
|
||||
errors: { byType: {}, byProvider: {}, topErrors: [] }
|
||||
})
|
||||
|
||||
const systemHealth = ref<SystemHealth>({
|
||||
status: 'healthy',
|
||||
score: 100,
|
||||
checks: {
|
||||
apiConnectivity: true,
|
||||
memoryUsage: 0,
|
||||
errorRate: 0,
|
||||
responseTime: 0,
|
||||
costBudget: 0,
|
||||
cacheEfficiency: 0
|
||||
},
|
||||
alerts: []
|
||||
})
|
||||
|
||||
const recommendations = ref<OptimizationRecommendation[]>([])
|
||||
const isRefreshing = ref(false)
|
||||
const autoRefresh = ref(true)
|
||||
let refreshInterval: number | null = null
|
||||
|
||||
// Previous values for change calculation
|
||||
const previousStats = ref<PerformanceStats | null>(null)
|
||||
|
||||
// Computed properties for changes
|
||||
const requestsChange = computed(() => {
|
||||
if (!previousStats.value) return 0
|
||||
return stats.value.requests.total - previousStats.value.requests.total
|
||||
})
|
||||
|
||||
const successRateChange = computed(() => {
|
||||
if (!previousStats.value) return 0
|
||||
return stats.value.requests.successRate - previousStats.value.requests.successRate
|
||||
})
|
||||
|
||||
const latencyChange = computed(() => {
|
||||
if (!previousStats.value) return 0
|
||||
return stats.value.timing.averageLatency - previousStats.value.timing.averageLatency
|
||||
})
|
||||
|
||||
const costChange = computed(() => {
|
||||
if (!previousStats.value) return 0
|
||||
return stats.value.costs.total - previousStats.value.costs.total
|
||||
})
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(async () => {
|
||||
console.log('🚀 Starting monitoring dashboard...')
|
||||
monitor.startMonitoring()
|
||||
await refreshData()
|
||||
|
||||
if (autoRefresh.value) {
|
||||
startAutoRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
console.log('🛑 Stopping monitoring dashboard...')
|
||||
stopAutoRefresh()
|
||||
monitor.stopMonitoring()
|
||||
})
|
||||
|
||||
// Methods
|
||||
const refreshData = async () => {
|
||||
if (isRefreshing.value) return
|
||||
|
||||
isRefreshing.value = true
|
||||
try {
|
||||
// Store previous stats for change calculation
|
||||
previousStats.value = { ...stats.value }
|
||||
|
||||
// Get fresh data
|
||||
const now = Date.now()
|
||||
const oneHourAgo = now - 3600000
|
||||
|
||||
stats.value = monitor.getPerformanceStats(oneHourAgo, now)
|
||||
systemHealth.value = monitor.getSystemHealth()
|
||||
recommendations.value = monitor.getOptimizationRecommendations()
|
||||
|
||||
console.log('📊 Dashboard data refreshed')
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to refresh dashboard data:', error)
|
||||
} finally {
|
||||
isRefreshing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const startAutoRefresh = () => {
|
||||
if (refreshInterval) return
|
||||
|
||||
refreshInterval = setInterval(async () => {
|
||||
await refreshData()
|
||||
}, 30000) // Refresh every 30 seconds
|
||||
|
||||
console.log('🔄 Auto-refresh started')
|
||||
}
|
||||
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval)
|
||||
refreshInterval = null
|
||||
console.log('⏹️ Auto-refresh stopped')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleAutoRefresh = () => {
|
||||
autoRefresh.value = !autoRefresh.value
|
||||
|
||||
if (autoRefresh.value) {
|
||||
startAutoRefresh()
|
||||
} else {
|
||||
stopAutoRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
const applyRecommendation = async (rec: OptimizationRecommendation) => {
|
||||
rec.applying = true
|
||||
try {
|
||||
const result = await monitor.applyOptimizations([rec])
|
||||
if (result.applied > 0) {
|
||||
console.log('✅ Recommendation applied successfully')
|
||||
// Remove from list
|
||||
const index = recommendations.value.indexOf(rec)
|
||||
if (index > -1) {
|
||||
recommendations.value.splice(index, 1)
|
||||
}
|
||||
} else {
|
||||
console.error('❌ Failed to apply recommendation')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('💥 Error applying recommendation:', error)
|
||||
} finally {
|
||||
rec.applying = false
|
||||
}
|
||||
}
|
||||
|
||||
const dismissRecommendation = (rec: OptimizationRecommendation) => {
|
||||
const index = recommendations.value.indexOf(rec)
|
||||
if (index > -1) {
|
||||
recommendations.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const exportData = () => {
|
||||
try {
|
||||
const data = monitor.exportPerformanceData('json')
|
||||
|
||||
// In real implementation, this would trigger a download
|
||||
console.log('📤 Exporting performance data...')
|
||||
console.log(data)
|
||||
|
||||
// For uni-app, you might want to save to local storage or share
|
||||
uni.setStorageSync('ai-news-performance-data', data)
|
||||
|
||||
uni.showToast({
|
||||
title: 'Data exported to storage',
|
||||
icon: 'success'
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to export data:', error)
|
||||
uni.showToast({
|
||||
title: 'Export failed',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const clearAlerts = () => {
|
||||
systemHealth.value.alerts = []
|
||||
uni.showToast({
|
||||
title: 'Alerts cleared',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M'
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'K'
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
const formatChange = (change: number): string => {
|
||||
if (change === 0) return '0'
|
||||
const sign = change > 0 ? '+' : ''
|
||||
return `${sign}${change.toFixed(1)}`
|
||||
}
|
||||
|
||||
const getChangeClass = (change: number): string => {
|
||||
if (change > 0) return 'positive'
|
||||
if (change < 0) return 'negative'
|
||||
return 'neutral'
|
||||
}
|
||||
|
||||
const formatCheckName = (key: string): string => {
|
||||
return key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase())
|
||||
}
|
||||
|
||||
const formatCheckValue = (key: string, value: any): string => {
|
||||
switch (key) {
|
||||
case 'apiConnectivity':
|
||||
return value ? 'Connected' : 'Disconnected'
|
||||
case 'memoryUsage':
|
||||
return (value * 100).toFixed(1) + '%'
|
||||
case 'errorRate':
|
||||
return (value * 100).toFixed(2) + '%'
|
||||
case 'responseTime':
|
||||
return value.toFixed(0) + 'ms'
|
||||
case 'costBudget':
|
||||
return (value * 100).toFixed(1) + '%'
|
||||
case 'cacheEfficiency':
|
||||
return (value * 100).toFixed(1) + '%'
|
||||
default:
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
const getHealthCheckClass = (key: string, value: any): string => {
|
||||
switch (key) {
|
||||
case 'apiConnectivity':
|
||||
return value ? 'healthy' : 'critical'
|
||||
case 'memoryUsage':
|
||||
return value > 0.8 ? 'critical' : value > 0.6 ? 'warning' : 'healthy'
|
||||
case 'errorRate':
|
||||
return value > 0.1 ? 'critical' : value > 0.05 ? 'warning' : 'healthy'
|
||||
case 'responseTime':
|
||||
return value > 5000 ? 'critical' : value > 3000 ? 'warning' : 'healthy'
|
||||
case 'costBudget':
|
||||
return value > 0.9 ? 'critical' : value > 0.7 ? 'warning' : 'healthy'
|
||||
case 'cacheEfficiency':
|
||||
return value < 0.3 ? 'warning' : value < 0.5 ? 'healthy' : 'excellent'
|
||||
default:
|
||||
return 'neutral'
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timestamp: number): string => {
|
||||
const now = Date.now()
|
||||
const diff = now - timestamp
|
||||
|
||||
if (diff < 60000) return 'Just now'
|
||||
if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago'
|
||||
if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago'
|
||||
return new Date(timestamp).toLocaleDateString()
|
||||
}
|
||||
|
||||
// Chart event handlers
|
||||
const onChartTouch = (e: any) => {
|
||||
console.log('Chart touched:', e)
|
||||
}
|
||||
|
||||
const onChartTouchEnd = (e: any) => {
|
||||
console.log('Chart touch ended:', e)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.status-indicator.healthy {
|
||||
background-color: #e8f5e8;
|
||||
border: 2px solid #4caf50;
|
||||
}
|
||||
|
||||
.status-indicator.warning {
|
||||
background-color: #fff3e0;
|
||||
border: 2px solid #ff9800;
|
||||
}
|
||||
|
||||
.status-indicator.critical {
|
||||
background-color: #ffebee;
|
||||
border: 2px solid #f44336;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.health-score {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.quick-stats {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stat-change.positive {
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.stat-change.negative {
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.stat-change.neutral {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.health-section,
|
||||
.alerts-section,
|
||||
.charts-section,
|
||||
.recommendations-section,
|
||||
.controls-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.health-checks {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.health-check {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ddd;
|
||||
}
|
||||
|
||||
.health-check.healthy {
|
||||
background-color: #e8f5e8;
|
||||
border-left-color: #4caf50;
|
||||
}
|
||||
|
||||
.health-check.warning {
|
||||
background-color: #fff3e0;
|
||||
border-left-color: #ff9800;
|
||||
}
|
||||
|
||||
.health-check.critical {
|
||||
background-color: #ffebee;
|
||||
border-left-color: #f44336;
|
||||
}
|
||||
|
||||
.check-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.check-value {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.alerts-list {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ddd;
|
||||
}
|
||||
|
||||
.alert-item.info {
|
||||
background-color: #e3f2fd;
|
||||
border-left-color: #2196f3;
|
||||
}
|
||||
|
||||
.alert-item.warning {
|
||||
background-color: #fff3e0;
|
||||
border-left-color: #ff9800;
|
||||
}
|
||||
|
||||
.alert-item.error {
|
||||
background-color: #ffebee;
|
||||
border-left-color: #f44336;
|
||||
}
|
||||
|
||||
.alert-item.critical {
|
||||
background-color: #fce4ec;
|
||||
border-left-color: #e91e63;
|
||||
}
|
||||
|
||||
.alert-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.alert-severity {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.alert-time {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.alert-message {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.alert-source {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.chart-area {
|
||||
height: 200px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cost-breakdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cost-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.cost-bar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
min-width: 80px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cost-bar {
|
||||
flex: 1;
|
||||
height: 20px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cost-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4caf50, #2196f3);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.cost-value {
|
||||
min-width: 60px;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.recommendations-list {
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.recommendation-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #ddd;
|
||||
}
|
||||
|
||||
.recommendation-item.low {
|
||||
background-color: #f9f9f9;
|
||||
border-left-color: #9e9e9e;
|
||||
}
|
||||
|
||||
.recommendation-item.medium {
|
||||
background-color: #fff3e0;
|
||||
border-left-color: #ff9800;
|
||||
}
|
||||
|
||||
.recommendation-item.high {
|
||||
background-color: #ffebee;
|
||||
border-left-color: #f44336;
|
||||
}
|
||||
|
||||
.recommendation-item.critical {
|
||||
background-color: #fce4ec;
|
||||
border-left-color: #e91e63;
|
||||
}
|
||||
|
||||
.rec-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.rec-type {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
background-color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.rec-priority {
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.rec-description {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.rec-impact {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.impact-title {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.impact-item {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.rec-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-apply,
|
||||
.btn-dismiss {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-apply {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-apply:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-dismiss {
|
||||
background-color: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
padding: 12px 24px;
|
||||
border: 2px solid #2196f3;
|
||||
background-color: #2196f3;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background-color: #1976d2;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
.control-btn:disabled {
|
||||
background-color: #ccc;
|
||||
border-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.control-btn.danger {
|
||||
background-color: #f44336;
|
||||
border-color: #f44336;
|
||||
}
|
||||
|
||||
.control-btn.danger:hover {
|
||||
background-color: #d32f2f;
|
||||
border-color: #d32f2f;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user