Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
548
pages/notification/queue/performance.vue
Normal file
548
pages/notification/queue/performance.vue
Normal file
@@ -0,0 +1,548 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user