|
| 1 | +<template> |
| 2 | + <div class="redis-optimization-panel"> |
| 3 | + <!-- Summary Header --> |
| 4 | + <div class="bg-white rounded-lg shadow-md p-6 mb-6"> |
| 5 | + <div class="flex flex-wrap items-center justify-between"> |
| 6 | + <div> |
| 7 | + <h2 class="text-xl font-semibold text-blueGray-700">Redis Optimizations</h2> |
| 8 | + <p class="text-sm text-blueGray-500 mt-1"> |
| 9 | + {{ optimizations.length }} optimization opportunities found |
| 10 | + </p> |
| 11 | + </div> |
| 12 | + <div class="flex items-center space-x-4 mt-4 sm:mt-0"> |
| 13 | + <select |
| 14 | + v-model="categoryFilter" |
| 15 | + class="px-3 py-2 border border-blueGray-300 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500" |
| 16 | + > |
| 17 | + <option value="all">All Types</option> |
| 18 | + <option value="pipeline">Pipeline</option> |
| 19 | + <option value="lua_script">Lua Script</option> |
| 20 | + <option value="data_structure">Data Structure</option> |
| 21 | + <option value="connection">Connection</option> |
| 22 | + <option value="cache">Cache</option> |
| 23 | + </select> |
| 24 | + <button |
| 25 | + @click="$emit('refresh')" |
| 26 | + :disabled="loading" |
| 27 | + class="px-4 py-2 text-indigo-600 hover:text-indigo-800 flex items-center" |
| 28 | + > |
| 29 | + <i class="fas fa-sync-alt mr-2" :class="{ 'animate-spin': loading }"></i> |
| 30 | + Refresh |
| 31 | + </button> |
| 32 | + </div> |
| 33 | + </div> |
| 34 | + </div> |
| 35 | + |
| 36 | + <!-- Loading State --> |
| 37 | + <div v-if="loading" class="bg-white rounded-lg shadow-md p-12 text-center"> |
| 38 | + <i class="fas fa-spinner fa-spin text-4xl text-indigo-500 mb-4"></i> |
| 39 | + <p class="text-blueGray-600">Analyzing Redis usage patterns...</p> |
| 40 | + </div> |
| 41 | + |
| 42 | + <!-- Empty State --> |
| 43 | + <div v-else-if="filteredOptimizations.length === 0" class="bg-white rounded-lg shadow-md p-12 text-center"> |
| 44 | + <i class="fas fa-database text-4xl text-green-500 mb-4"></i> |
| 45 | + <p class="text-lg font-medium text-blueGray-700">No Redis optimizations needed</p> |
| 46 | + <p class="text-sm text-blueGray-500 mt-2">Redis usage patterns look optimal!</p> |
| 47 | + </div> |
| 48 | + |
| 49 | + <!-- Optimizations List --> |
| 50 | + <div v-else class="space-y-4"> |
| 51 | + <div |
| 52 | + v-for="(opt, index) in filteredOptimizations" |
| 53 | + :key="index" |
| 54 | + class="bg-white rounded-lg shadow-md p-6 border-l-4" |
| 55 | + :class="getSeverityBorderClass(opt.severity)" |
| 56 | + > |
| 57 | + <div class="flex items-center flex-wrap gap-2 mb-2"> |
| 58 | + <span |
| 59 | + class="px-2 py-1 text-xs font-medium rounded uppercase" |
| 60 | + :class="getSeverityBadgeClass(opt.severity)" |
| 61 | + > |
| 62 | + {{ opt.severity }} |
| 63 | + </span> |
| 64 | + <span class="text-sm font-medium text-blueGray-700"> |
| 65 | + {{ formatOptType(opt.optimization_type) }} |
| 66 | + </span> |
| 67 | + <span |
| 68 | + v-if="opt.category" |
| 69 | + class="px-2 py-1 text-xs bg-purple-100 text-purple-700 rounded" |
| 70 | + > |
| 71 | + {{ opt.category }} |
| 72 | + </span> |
| 73 | + </div> |
| 74 | + <p class="text-sm text-blueGray-600">{{ opt.description }}</p> |
| 75 | + <div class="mt-3 flex items-center text-sm text-blueGray-500"> |
| 76 | + <i class="fas fa-file-code mr-2"></i> |
| 77 | + <span class="font-mono">{{ formatFilePath(opt.file_path) }}</span> |
| 78 | + <span v-if="opt.line" class="ml-2">:{{ opt.line }}</span> |
| 79 | + </div> |
| 80 | + <div v-if="opt.code_snippet" class="mt-3 p-3 bg-blueGray-50 rounded-lg font-mono text-xs overflow-x-auto"> |
| 81 | + <pre>{{ opt.code_snippet }}</pre> |
| 82 | + </div> |
| 83 | + <div v-if="opt.recommendation" class="mt-3 p-3 bg-green-50 rounded-lg border border-green-200"> |
| 84 | + <p class="text-sm text-green-800"> |
| 85 | + <i class="fas fa-lightbulb mr-2"></i>{{ opt.recommendation }} |
| 86 | + </p> |
| 87 | + </div> |
| 88 | + </div> |
| 89 | + </div> |
| 90 | + </div> |
| 91 | +</template> |
| 92 | + |
| 93 | +<script setup lang="ts"> |
| 94 | +import { ref, computed } from 'vue' |
| 95 | +
|
| 96 | +interface Optimization { |
| 97 | + severity: string |
| 98 | + optimization_type: string |
| 99 | + category?: string |
| 100 | + description: string |
| 101 | + file_path: string |
| 102 | + line?: number |
| 103 | + code_snippet?: string |
| 104 | + recommendation?: string |
| 105 | +} |
| 106 | +
|
| 107 | +interface Props { |
| 108 | + optimizations: Optimization[] |
| 109 | + loading: boolean |
| 110 | + summary: any |
| 111 | +} |
| 112 | +
|
| 113 | +const props = defineProps<Props>() |
| 114 | +defineEmits<{ 'scan-file': [path: string]; 'refresh': [] }>() |
| 115 | +
|
| 116 | +const categoryFilter = ref('all') |
| 117 | +
|
| 118 | +const filteredOptimizations = computed(() => { |
| 119 | + if (categoryFilter.value === 'all') return props.optimizations |
| 120 | + return props.optimizations.filter(o => o.category === categoryFilter.value) |
| 121 | +}) |
| 122 | +
|
| 123 | +function getSeverityBorderClass(severity: string): string { |
| 124 | + const classes: Record<string, string> = { |
| 125 | + critical: 'border-red-500', high: 'border-orange-500', |
| 126 | + medium: 'border-yellow-500', low: 'border-blue-500', info: 'border-gray-400' |
| 127 | + } |
| 128 | + return classes[severity] || 'border-gray-300' |
| 129 | +} |
| 130 | +
|
| 131 | +function getSeverityBadgeClass(severity: string): string { |
| 132 | + const classes: Record<string, string> = { |
| 133 | + critical: 'bg-red-100 text-red-800', high: 'bg-orange-100 text-orange-800', |
| 134 | + medium: 'bg-yellow-100 text-yellow-800', low: 'bg-blue-100 text-blue-800', |
| 135 | + info: 'bg-gray-100 text-gray-800' |
| 136 | + } |
| 137 | + return classes[severity] || 'bg-gray-100 text-gray-800' |
| 138 | +} |
| 139 | +
|
| 140 | +function formatOptType(type: string): string { |
| 141 | + return type?.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ') || 'Unknown' |
| 142 | +} |
| 143 | +
|
| 144 | +function formatFilePath(path: string): string { |
| 145 | + const parts = path?.split('/') || [] |
| 146 | + return parts.length <= 3 ? path : '.../' + parts.slice(-3).join('/') |
| 147 | +} |
| 148 | +</script> |
| 149 | + |
| 150 | +<style scoped> |
| 151 | +.animate-spin { animation: spin 1s linear infinite; } |
| 152 | +@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| 153 | +</style> |
0 commit comments