Files
Nas-Notification/pages/notification/queue/performance.vue

549 lines
18 KiB
Vue

<template>
<div>
<LayoutsBreadcrumb />
<!-- Header Section -->
<rs-card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon class="mr-2 text-primary" name="ic:outline-speed"></Icon>
<h1 class="text-xl font-bold text-primary">Performance</h1>
</div>
<rs-button
variant="outline"
size="sm"
@click="refreshMetrics"
:loading="isLoading"
:disabled="isLoading"
>
<Icon class="mr-1" name="ic:outline-refresh"></Icon>
Refresh
</rs-button>
</div>
</template>
<template #body>
<p class="text-gray-600">Monitor system performance and queue processing metrics.</p>
</template>
</rs-card>
<!-- Error Alert -->
<rs-alert v-if="error" variant="danger" class="mb-6">
{{ error }}
</rs-alert>
<!-- Loading State -->
<div v-if="isLoading" class="flex justify-center items-center py-12 text-gray-600">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary"></div>
<span class="ml-2">Loading metrics...</span>
</div>
<div v-else>
<!-- Key Metrics -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<rs-card>
<div class="p-4 text-center">
<h3 class="text-2xl font-bold text-blue-600">
{{ metrics.throughput }}
</h3>
<p class="text-sm text-gray-600">Messages/min</p>
</div>
</rs-card>
<rs-card>
<div class="p-4 text-center">
<h3 class="text-2xl font-bold text-green-600">{{ metrics.uptime }}%</h3>
<p class="text-sm text-gray-600">System Uptime</p>
</div>
</rs-card>
<rs-card>
<div class="p-4 text-center">
<h3 class="text-2xl font-bold text-purple-600">
{{ metrics.workers }}
</h3>
<p class="text-sm text-gray-600">Active Workers</p>
</div>
</rs-card>
<rs-card>
<div class="p-4 text-center">
<h3 class="text-2xl font-bold text-orange-600">{{ metrics.queueLoad }}%</h3>
<p class="text-sm text-gray-600">Queue Load</p>
</div>
</rs-card>
</div>
<!-- Performance Summary -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Throughput Summary -->
<rs-card>
<template #header>
<h3 class="text-lg font-semibold text-primary">Throughput Summary</h3>
</template>
<template #body>
<div class="space-y-4">
<div class="flex justify-between items-center p-3 bg-gray-50 rounded">
<span class="text-gray-600">Current Rate</span>
<span class="font-bold">{{ throughput.current }}/min</span>
</div>
<div class="flex justify-between items-center p-3 bg-gray-50 rounded">
<span class="text-gray-600">Peak Today</span>
<span class="font-bold">{{ throughput.peak }}/min</span>
</div>
<div class="flex justify-between items-center p-3 bg-gray-50 rounded">
<span class="text-gray-600">Average</span>
<span class="font-bold">{{ throughput.average }}/min</span>
</div>
</div>
</template>
</rs-card>
<!-- System Status -->
<rs-card>
<template #header>
<h3 class="text-lg font-semibold text-primary">System Status</h3>
</template>
<template #body>
<div class="space-y-4">
<div class="flex justify-between items-center p-3 bg-gray-50 rounded">
<span class="text-gray-600">Uptime Today</span>
<span class="font-bold text-green-600">
{{ systemStatus.uptimeToday }}%
</span>
</div>
<div class="flex justify-between items-center p-3 bg-gray-50 rounded">
<span class="text-gray-600">Response Time</span>
<span class="font-bold">{{ systemStatus.responseTime }}ms</span>
</div>
<div class="flex justify-between items-center p-3 bg-gray-50 rounded">
<span class="text-gray-600">Error Rate</span>
<span class="font-bold text-red-600">{{ systemStatus.errorRate }}%</span>
</div>
</div>
</template>
</rs-card>
</div>
<!-- Historical Performance Chart -->
<rs-card class="mt-6">
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">Historical Performance</h3>
<div class="flex items-center space-x-2">
<select
v-model="historyFilter.metric"
class="px-2 py-1 text-sm border border-gray-300 rounded"
@change="fetchHistoricalData"
>
<option value="throughput">Throughput</option>
<option value="error_rate">Error Rate</option>
<option value="response_time">Response Time</option>
</select>
<select
v-model="historyFilter.period"
class="px-2 py-1 text-sm border border-gray-300 rounded"
@change="fetchHistoricalData"
>
<option value="hour">Last Hour</option>
<option value="day">Last 24 Hours</option>
<option value="week">Last Week</option>
<option value="month">Last Month</option>
</select>
</div>
</div>
</template>
<template #body>
<div v-if="isLoadingHistory" class="flex flex-col items-center justify-center h-64 bg-gray-50">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary"></div>
<p class="mt-4 text-gray-500">Loading historical data...</p>
</div>
<div v-else-if="historyError" class="flex flex-col items-center justify-center h-64 bg-red-50">
<Icon name="ic:outline-error" class="text-6xl text-red-300" />
<p class="mt-4 text-red-500">{{ historyError }}</p>
<rs-button variant="outline" size="sm" class="mt-4" @click="fetchHistoricalData">
<Icon name="ic:outline-refresh" class="mr-1" />
Retry
</rs-button>
</div>
<div v-else-if="!historicalData.dataPoints || historicalData.dataPoints.length === 0" class="flex flex-col items-center justify-center h-64 bg-gray-50">
<Icon name="ic:outline-insert-chart" class="text-6xl text-gray-300" />
<p class="mt-4 text-gray-500">No historical data available for the selected period</p>
</div>
<div v-else>
<!-- Chart Summary -->
<div class="grid grid-cols-3 gap-4 mb-4">
<div class="bg-green-50 p-2 rounded text-center">
<p class="text-sm text-green-700">Maximum</p>
<p class="text-lg font-bold text-green-600">{{ historicalData.summary.max }}{{ getMetricUnit() }}</p>
</div>
<div class="bg-blue-50 p-2 rounded text-center">
<p class="text-sm text-blue-700">Average</p>
<p class="text-lg font-bold text-blue-600">{{ historicalData.summary.avg }}{{ getMetricUnit() }}</p>
</div>
<div class="bg-purple-50 p-2 rounded text-center">
<p class="text-sm text-purple-700">Trend</p>
<p class="text-lg font-bold text-purple-600">
<Icon
:name="historicalData.summary.trend === 'increasing' ? 'ic:outline-trending-up' : 'ic:outline-trending-down'"
class="inline-block mr-1"
/>
{{ historicalData.summary.trend }}
</p>
</div>
</div>
<!-- Simple Line Chart -->
<div class="h-64 w-full relative">
<!-- Y-axis labels -->
<div class="absolute top-0 left-0 h-full flex flex-col justify-between text-xs text-gray-500">
<div>{{ Math.ceil(historicalData.summary.max * 1.1) }}{{ getMetricUnit() }}</div>
<div>{{ Math.ceil(historicalData.summary.max * 0.75) }}{{ getMetricUnit() }}</div>
<div>{{ Math.ceil(historicalData.summary.max * 0.5) }}{{ getMetricUnit() }}</div>
<div>{{ Math.ceil(historicalData.summary.max * 0.25) }}{{ getMetricUnit() }}</div>
<div>0{{ getMetricUnit() }}</div>
</div>
<!-- Chart area -->
<div class="ml-10 h-full relative">
<!-- Horizontal grid lines -->
<div class="absolute top-0 left-0 w-full h-full border-b border-gray-200">
<div class="border-t border-gray-200 h-1/4"></div>
<div class="border-t border-gray-200 h-1/4"></div>
<div class="border-t border-gray-200 h-1/4"></div>
<div class="border-t border-gray-200 h-1/4"></div>
</div>
<!-- Line chart -->
<svg class="absolute top-0 left-0 w-full h-full overflow-visible">
<path
:d="getChartPath()"
fill="none"
stroke="#3b82f6"
stroke-width="2"
></path>
<!-- Data points -->
<circle
v-for="(point, i) in chartPoints"
:key="i"
:cx="point.x"
:cy="point.y"
r="3"
fill="#3b82f6"
></circle>
</svg>
</div>
</div>
<!-- X-axis labels (show some key timestamps) -->
<div class="mt-2 ml-10 flex justify-between text-xs text-gray-500">
<div v-for="(label, i) in getXAxisLabels()" :key="i">
{{ label }}
</div>
</div>
</div>
</template>
</rs-card>
</div>
</div>
</template>
<script setup>
import { onMounted } from "vue";
definePageMeta({
title: "Performance",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Notification",
path: "/notification",
},
{
name: "Queue",
path: "/notification/queue",
},
{
name: "Performance",
path: "/notification/queue/performance",
},
],
});
// Reactive state
const isLoading = ref(true);
const error = ref(null);
const metrics = ref({
throughput: "0",
uptime: "0",
workers: "0",
queueLoad: "0",
});
const throughput = ref({
current: "0",
peak: "0",
average: "0",
});
const systemStatus = ref({
uptimeToday: "0",
responseTime: "0",
errorRate: "0",
});
// Historical data state
const isLoadingHistory = ref(true);
const historyError = ref(null);
const historicalData = ref({
dataPoints: [],
summary: {
max: "0",
avg: "0",
trend: "stable", // 'increasing', 'decreasing', or 'stable'
},
});
const historyFilter = ref({
metric: "throughput",
period: "hour",
});
// Fetch metrics from API
const fetchMetrics = async () => {
try {
isLoading.value = true;
error.value = null;
const { data } = await useFetch("/api/notifications/queue/performance", {
method: "GET",
});
if (!data.value) {
throw new Error("Failed to fetch metrics data");
}
if (data.value?.success) {
const performanceData = data.value.data;
// Update metrics with fetched data
metrics.value = performanceData.metrics || {
throughput: "0",
uptime: "0",
workers: "0",
queueLoad: "0",
};
// Update throughput data
throughput.value = performanceData.throughput || {
current: "0",
peak: "0",
average: "0",
};
// Update system status
systemStatus.value = performanceData.systemStatus || {
uptimeToday: "0",
responseTime: "0",
errorRate: "0",
};
} else {
throw new Error(data.value?.statusMessage || "Failed to fetch metrics");
}
} catch (err) {
console.error("Error fetching metrics:", err);
error.value = err.message || "Failed to load performance metrics";
} finally {
isLoading.value = false;
}
};
// Refresh metrics
const refreshMetrics = () => {
fetchMetrics();
};
// Fetch historical data
const fetchHistoricalData = async () => {
isLoadingHistory.value = true;
historyError.value = null;
try {
const { data } = await useFetch("/api/notifications/queue/history", {
method: "GET",
query: {
metric: historyFilter.value.metric,
period: historyFilter.value.period,
},
});
if (!data.value) {
throw new Error("Failed to fetch historical data");
}
if (data.value?.success) {
historicalData.value = data.value.data;
} else {
throw new Error(data.value?.statusMessage || "Failed to fetch historical data");
}
} catch (err) {
console.error("Error fetching historical data:", err);
historyError.value = err.message || "Failed to load historical data";
} finally {
isLoadingHistory.value = false;
}
};
// Get chart path for the selected metric
const getChartPath = () => {
if (!historicalData.value.dataPoints || historicalData.value.dataPoints.length < 2) {
return "M 0 0"; // Return an empty path if no data points
}
const points = historicalData.value.dataPoints;
const maxVal = historicalData.value.summary.max || 1;
const width = 100 * (points.length - 1); // Total width based on points
const path = [];
points.forEach((point, i) => {
// Calculate x position (evenly spaced)
const x = (i / (points.length - 1)) * width;
// Calculate y position (inverted, as SVG y=0 is top)
// Scale value to fit in the chart height (0-100%)
const y = 100 - ((point.value / maxVal) * 100);
if (i === 0) {
path.push(`M ${x} ${y}`);
} else {
path.push(`L ${x} ${y}`);
}
});
return path.join(" ");
};
// Get data points for the chart
const chartPoints = computed(() => {
if (!historicalData.value.dataPoints) {
return [];
}
const points = historicalData.value.dataPoints;
const maxVal = historicalData.value.summary.max || 1;
const width = 100 * (points.length - 1); // Total width based on points
return points.map((point, i) => ({
x: (i / (points.length - 1)) * width,
y: 100 - ((point.value / maxVal) * 100),
}));
});
// Get Y-axis labels
const getYAxisLabels = () => {
if (!historicalData.value.dataPoints) {
return [];
}
const minY = Math.min(...historicalData.value.dataPoints.map(p => p.y));
const maxY = Math.max(...historicalData.value.dataPoints.map(p => p.y));
const yRange = maxY - minY;
const labels = [];
if (yRange > 0) {
const step = yRange / 4;
for (let i = 0; i <= 4; i++) {
labels.push(Math.ceil(minY + i * step));
}
} else {
labels.push(minY);
labels.push(0);
}
return labels;
};
// Get X-axis labels
const getXAxisLabels = () => {
if (!historicalData.value.dataPoints || historicalData.value.dataPoints.length === 0) {
return [];
}
const points = historicalData.value.dataPoints;
const numLabels = 5; // Number of labels to show
const step = Math.max(1, Math.floor(points.length / (numLabels - 1)));
const labels = [];
for (let i = 0; i < points.length; i += step) {
if (labels.length < numLabels) {
const date = new Date(points[i].timestamp);
// Format based on the period
let label;
switch (historyFilter.value.period) {
case 'hour':
label = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
break;
case 'day':
label = date.toLocaleTimeString([], { hour: '2-digit' }) + 'h';
break;
case 'week':
case 'month':
label = date.toLocaleDateString([], { day: 'numeric', month: 'short' });
break;
}
labels.push(label);
}
}
// Make sure we add the last point
if (labels.length < numLabels) {
const date = new Date(points[points.length - 1].timestamp);
let label;
switch (historyFilter.value.period) {
case 'hour':
label = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
break;
case 'day':
label = date.toLocaleTimeString([], { hour: '2-digit' }) + 'h';
break;
case 'week':
case 'month':
label = date.toLocaleDateString([], { day: 'numeric', month: 'short' });
break;
}
labels.push(label);
}
return labels;
};
// Get metric unit based on selected metric
const getMetricUnit = () => {
if (historyFilter.value.metric === "throughput") {
return "/min";
} else if (historyFilter.value.metric === "error_rate") {
return "%";
} else if (historyFilter.value.metric === "response_time") {
return "ms";
}
return "";
};
// Fetch metrics on component mount
onMounted(() => {
fetchMetrics();
fetchHistoricalData(); // Fetch initial historical data
});
// Auto-refresh every 60 seconds
let refreshInterval;
onMounted(() => {
refreshInterval = setInterval(fetchMetrics, 60000);
});
onUnmounted(() => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
});
</script>
<style lang="scss" scoped></style>