Merge feature/Logging into dev
This commit is contained in:
Binary file not shown.
+13
-1
@@ -8,12 +8,24 @@ CREATE TABLE IF NOT EXISTS redeploy_logs (
|
|||||||
stack_name TEXT NOT NULL,
|
stack_name TEXT NOT NULL,
|
||||||
status TEXT NOT NULL,
|
status TEXT NOT NULL,
|
||||||
message TEXT,
|
message TEXT,
|
||||||
endpoint INTEGER
|
endpoint INTEGER,
|
||||||
|
redeploy_type TEXT
|
||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
db.exec(createRedeployLogsTable);
|
db.exec(createRedeployLogsTable);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const columns = db.prepare('PRAGMA table_info(redeploy_logs)').all();
|
||||||
|
const hasRedeployType = columns.some((column) => column.name === 'redeploy_type');
|
||||||
|
if (!hasRedeployType) {
|
||||||
|
db.exec('ALTER TABLE redeploy_logs ADD COLUMN redeploy_type TEXT');
|
||||||
|
console.log('ℹ️ redeploy_type column hinzugefügt');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('⚠️ Konnte redeploy_type Spalte nicht prüfen/erstellen:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('✅ redeploy_logs table ready');
|
console.log('✅ redeploy_logs table ready');
|
||||||
|
|
||||||
db.close();
|
db.close();
|
||||||
|
|||||||
@@ -57,6 +57,15 @@ export function buildLogFilter(queryParams = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const redeployTypes = valueToArray(queryParams.redeployTypes ?? queryParams.redeployType);
|
||||||
|
if (redeployTypes.length) {
|
||||||
|
const placeholders = redeployTypes.map((_, idx) => `@redeployType${idx}`);
|
||||||
|
filters.push(`redeploy_type IN (${placeholders.join(', ')})`);
|
||||||
|
redeployTypes.forEach((entry, idx) => {
|
||||||
|
params[`redeployType${idx}`] = entry;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const messageQuery = singleValue(queryParams.message);
|
const messageQuery = singleValue(queryParams.message);
|
||||||
if (messageQuery && String(messageQuery).trim()) {
|
if (messageQuery && String(messageQuery).trim()) {
|
||||||
filters.push('message LIKE @message');
|
filters.push('message LIKE @message');
|
||||||
@@ -82,18 +91,19 @@ export function buildLogFilter(queryParams = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const insertRedeployLogStmt = db.prepare(`
|
const insertRedeployLogStmt = db.prepare(`
|
||||||
INSERT INTO redeploy_logs (stack_id, stack_name, status, message, endpoint)
|
INSERT INTO redeploy_logs (stack_id, stack_name, status, message, endpoint, redeploy_type)
|
||||||
VALUES (@stackId, @stackName, @status, @message, @endpoint)
|
VALUES (@stackId, @stackName, @status, @message, @endpoint, @redeployType)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
export function logRedeployEvent({ stackId, stackName, status, message = null, endpoint = null }) {
|
export function logRedeployEvent({ stackId, stackName, status, message = null, endpoint = null, redeployType = null }) {
|
||||||
try {
|
try {
|
||||||
insertRedeployLogStmt.run({
|
insertRedeployLogStmt.run({
|
||||||
stackId: String(stackId),
|
stackId: String(stackId),
|
||||||
stackName: stackName ?? 'Unknown',
|
stackName: stackName ?? 'Unknown',
|
||||||
status,
|
status,
|
||||||
message,
|
message,
|
||||||
endpoint
|
endpoint,
|
||||||
|
redeployType: redeployType ?? null
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('❌ Fehler beim Speichern des Redeploy-Logs:', err.message);
|
console.error('❌ Fehler beim Speichern des Redeploy-Logs:', err.message);
|
||||||
@@ -116,7 +126,7 @@ export function deleteLogsByFilters(queryParams = {}) {
|
|||||||
export function exportLogsByFilters(queryParams = {}, format = 'txt') {
|
export function exportLogsByFilters(queryParams = {}, format = 'txt') {
|
||||||
const { whereClause, params } = buildLogFilter(queryParams);
|
const { whereClause, params } = buildLogFilter(queryParams);
|
||||||
const rows = db.prepare(`
|
const rows = db.prepare(`
|
||||||
SELECT id, timestamp, stack_id AS stackId, stack_name AS stackName, status, message, endpoint
|
SELECT id, timestamp, stack_id AS stackId, stack_name AS stackName, status, message, endpoint, redeploy_type AS redeployType
|
||||||
FROM redeploy_logs
|
FROM redeploy_logs
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY datetime(timestamp) DESC
|
ORDER BY datetime(timestamp) DESC
|
||||||
@@ -126,7 +136,7 @@ export function exportLogsByFilters(queryParams = {}, format = 'txt') {
|
|||||||
|
|
||||||
if (format === 'sql') {
|
if (format === 'sql') {
|
||||||
const statements = rows.map((row) => {
|
const statements = rows.map((row) => {
|
||||||
const columns = ['id', 'timestamp', 'stack_id', 'stack_name', 'status', 'message', 'endpoint'];
|
const columns = ['id', 'timestamp', 'stack_id', 'stack_name', 'status', 'message', 'endpoint', 'redeploy_type'];
|
||||||
const values = [
|
const values = [
|
||||||
row.id,
|
row.id,
|
||||||
row.timestamp,
|
row.timestamp,
|
||||||
@@ -134,7 +144,8 @@ export function exportLogsByFilters(queryParams = {}, format = 'txt') {
|
|||||||
row.stackName,
|
row.stackName,
|
||||||
row.status,
|
row.status,
|
||||||
row.message,
|
row.message,
|
||||||
row.endpoint
|
row.endpoint,
|
||||||
|
row.redeployType
|
||||||
].map((value) => {
|
].map((value) => {
|
||||||
if (value === null || value === undefined) return 'NULL';
|
if (value === null || value === undefined) return 'NULL';
|
||||||
return `'${String(value).replace(/'/g, "''")}'`;
|
return `'${String(value).replace(/'/g, "''")}'`;
|
||||||
@@ -156,7 +167,8 @@ export function exportLogsByFilters(queryParams = {}, format = 'txt') {
|
|||||||
`Stack: ${row.stackName ?? 'Unbekannt'} (ID: ${row.stackId})`,
|
`Stack: ${row.stackName ?? 'Unbekannt'} (ID: ${row.stackId})`,
|
||||||
`Status: ${row.status}`,
|
`Status: ${row.status}`,
|
||||||
`Endpoint: ${row.endpoint ?? '-'}`,
|
`Endpoint: ${row.endpoint ?? '-'}`,
|
||||||
`Nachricht: ${row.message ?? '-'}`
|
`Nachricht: ${row.message ?? '-'}`,
|
||||||
|
`Redeploy: ${row.redeployType ?? '-'}`
|
||||||
];
|
];
|
||||||
return parts.join(' | ');
|
return parts.join(' | ');
|
||||||
});
|
});
|
||||||
|
|||||||
+138
-9
@@ -117,7 +117,8 @@ app.get('/api/logs', (req, res) => {
|
|||||||
stack_name AS stackName,
|
stack_name AS stackName,
|
||||||
status,
|
status,
|
||||||
message,
|
message,
|
||||||
endpoint
|
endpoint,
|
||||||
|
redeploy_type AS redeployType
|
||||||
FROM redeploy_logs
|
FROM redeploy_logs
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY datetime(timestamp) DESC
|
ORDER BY datetime(timestamp) DESC
|
||||||
@@ -224,7 +225,8 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => {
|
|||||||
stackName: stack.Name,
|
stackName: stack.Name,
|
||||||
status: 'started',
|
status: 'started',
|
||||||
message: 'Redeploy gestartet',
|
message: 'Redeploy gestartet',
|
||||||
endpoint: stack.EndpointId
|
endpoint: stack.EndpointId,
|
||||||
|
redeployType: 'Einzeln'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (stack.Type === 1) {
|
if (stack.Type === 1) {
|
||||||
@@ -262,7 +264,8 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => {
|
|||||||
stackName: stack.Name,
|
stackName: stack.Name,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
message: 'Redeploy erfolgreich abgeschlossen',
|
message: 'Redeploy erfolgreich abgeschlossen',
|
||||||
endpoint: stack.EndpointId
|
endpoint: stack.EndpointId,
|
||||||
|
redeployType: 'Einzeln'
|
||||||
});
|
});
|
||||||
console.log(`✅ PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`);
|
console.log(`✅ PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`);
|
||||||
res.json({ success: true, message: 'Stack redeployed' });
|
res.json({ success: true, message: 'Stack redeployed' });
|
||||||
@@ -274,7 +277,8 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => {
|
|||||||
stackName: stack?.Name || `Stack ${id}`,
|
stackName: stack?.Name || `Stack ${id}`,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
endpoint: stack?.EndpointId || ENDPOINT_ID
|
endpoint: stack?.EndpointId || ENDPOINT_ID,
|
||||||
|
redeployType: 'Einzeln'
|
||||||
});
|
});
|
||||||
console.error(`❌ Fehler beim Redeploy von Stack ${id}:`, errorMessage);
|
console.error(`❌ Fehler beim Redeploy von Stack ${id}:`, errorMessage);
|
||||||
res.status(500).json({ error: errorMessage });
|
res.status(500).json({ error: errorMessage });
|
||||||
@@ -292,6 +296,17 @@ app.put('/api/stacks/redeploy-all', async (req, res) => {
|
|||||||
console.log("📦 Redeploy ALL für folgende Stacks:");
|
console.log("📦 Redeploy ALL für folgende Stacks:");
|
||||||
filteredStacks.forEach(s => console.log(` - ${s.Name}`));
|
filteredStacks.forEach(s => console.log(` - ${s.Name}`));
|
||||||
|
|
||||||
|
const stackSummaryList = filteredStacks.map((stack) => `${stack.Name} (${stack.Id})`);
|
||||||
|
const stackSummary = stackSummaryList.length ? stackSummaryList.join(', ') : 'keine Stacks';
|
||||||
|
logRedeployEvent({
|
||||||
|
stackId: '---',
|
||||||
|
stackName: '---',
|
||||||
|
status: 'started',
|
||||||
|
message: `Redeploy ALL gestartet für: ${stackSummary}`,
|
||||||
|
endpoint: ENDPOINT_ID,
|
||||||
|
redeployType: 'Alle'
|
||||||
|
});
|
||||||
|
|
||||||
filteredStacks.forEach(async (stack) => {
|
filteredStacks.forEach(async (stack) => {
|
||||||
try {
|
try {
|
||||||
broadcastRedeployStatus(stack.Id, true);
|
broadcastRedeployStatus(stack.Id, true);
|
||||||
@@ -299,8 +314,9 @@ app.put('/api/stacks/redeploy-all', async (req, res) => {
|
|||||||
stackId: stack.Id,
|
stackId: stack.Id,
|
||||||
stackName: stack.Name,
|
stackName: stack.Name,
|
||||||
status: 'started',
|
status: 'started',
|
||||||
message: 'Redeploy über Redeploy ALL gestartet',
|
message: 'Redeploy ALL gestartet',
|
||||||
endpoint: stack.EndpointId
|
endpoint: stack.EndpointId,
|
||||||
|
redeployType: 'Alle'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (stack.Type === 1) {
|
if (stack.Type === 1) {
|
||||||
@@ -323,8 +339,9 @@ app.put('/api/stacks/redeploy-all', async (req, res) => {
|
|||||||
stackId: stack.Id,
|
stackId: stack.Id,
|
||||||
stackName: stack.Name,
|
stackName: stack.Name,
|
||||||
status: 'success',
|
status: 'success',
|
||||||
message: 'Redeploy über Redeploy ALL abgeschlossen',
|
message: 'Redeploy ALL abgeschlossen',
|
||||||
endpoint: stack.EndpointId
|
endpoint: stack.EndpointId,
|
||||||
|
redeployType: 'Alle'
|
||||||
});
|
});
|
||||||
console.log(`✅ Redeploy abgeschlossen: ${stack.Name}`);
|
console.log(`✅ Redeploy abgeschlossen: ${stack.Name}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -335,7 +352,8 @@ app.put('/api/stacks/redeploy-all', async (req, res) => {
|
|||||||
stackName: stack.Name,
|
stackName: stack.Name,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
endpoint: stack.EndpointId
|
endpoint: stack.EndpointId,
|
||||||
|
redeployType: 'Alle'
|
||||||
});
|
});
|
||||||
console.error(`❌ Fehler beim Redeploy von Stack ${stack.Name}:`, errorMessage);
|
console.error(`❌ Fehler beim Redeploy von Stack ${stack.Name}:`, errorMessage);
|
||||||
}
|
}
|
||||||
@@ -344,10 +362,121 @@ app.put('/api/stacks/redeploy-all', async (req, res) => {
|
|||||||
res.json({ success: true, message: 'Redeploy ALL gestartet' });
|
res.json({ success: true, message: 'Redeploy ALL gestartet' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`❌ Fehler beim Redeploy ALL:`, err.message);
|
console.error(`❌ Fehler beim Redeploy ALL:`, err.message);
|
||||||
|
logRedeployEvent({
|
||||||
|
stackId: '---',
|
||||||
|
stackName: '---',
|
||||||
|
status: 'error',
|
||||||
|
message: err.message,
|
||||||
|
endpoint: ENDPOINT_ID,
|
||||||
|
redeployType: 'Alle'
|
||||||
|
});
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.put('/api/stacks/redeploy-selection', async (req, res) => {
|
||||||
|
const { stackIds } = req.body || {};
|
||||||
|
console.log(`🚀 PUT /api/stacks/redeploy-selection: Redeploy Auswahl gestartet (${Array.isArray(stackIds) ? stackIds.length : 0} Stacks)`);
|
||||||
|
|
||||||
|
if (!Array.isArray(stackIds) || !stackIds.length) {
|
||||||
|
return res.status(400).json({ error: 'stackIds (array) erforderlich' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedIds = stackIds.map((id) => String(id));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stacksRes = await axiosInstance.get('/api/stacks');
|
||||||
|
const endpointStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID);
|
||||||
|
const selectedStacks = endpointStacks.filter((stack) => normalizedIds.includes(String(stack.Id)));
|
||||||
|
|
||||||
|
if (!selectedStacks.length) {
|
||||||
|
return res.status(400).json({ error: 'Keine gültigen Stacks für Redeploy Auswahl gefunden' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingIds = normalizedIds.filter((id) => !selectedStacks.some((stack) => String(stack.Id) === id));
|
||||||
|
if (missingIds.length) {
|
||||||
|
return res.status(400).json({ error: `Ungültige Stack-IDs: ${missingIds.join(', ')}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const stackSummaryList = selectedStacks.map((stack) => `${stack.Name} (${stack.Id})`);
|
||||||
|
const stackSummary = stackSummaryList.length ? stackSummaryList.join(', ') : 'keine Stacks';
|
||||||
|
logRedeployEvent({
|
||||||
|
stackId: '---',
|
||||||
|
stackName: '---',
|
||||||
|
status: 'started',
|
||||||
|
message: `Redeploy Auswahl gestartet für: ${stackSummary}`,
|
||||||
|
endpoint: ENDPOINT_ID,
|
||||||
|
redeployType: 'Auswahl'
|
||||||
|
});
|
||||||
|
|
||||||
|
selectedStacks.forEach(async (stack) => {
|
||||||
|
try {
|
||||||
|
broadcastRedeployStatus(stack.Id, true);
|
||||||
|
logRedeployEvent({
|
||||||
|
stackId: stack.Id,
|
||||||
|
stackName: stack.Name,
|
||||||
|
status: 'started',
|
||||||
|
message: 'Redeploy Auswahl gestartet',
|
||||||
|
endpoint: stack.EndpointId,
|
||||||
|
redeployType: 'Auswahl'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stack.Type === 1) {
|
||||||
|
console.log(`🔄 [Redeploy Auswahl] Git Stack "${stack.Name}" (${stack.Id})`);
|
||||||
|
await axiosInstance.put(`/api/stacks/${stack.Id}/git/redeploy?endpointId=${stack.EndpointId}`);
|
||||||
|
} else if (stack.Type === 2) {
|
||||||
|
console.log(`🔄 [Redeploy Auswahl] Compose Stack "${stack.Name}" (${stack.Id})`);
|
||||||
|
const fileRes = await axiosInstance.get(`/api/stacks/${stack.Id}/file`);
|
||||||
|
const stackFileContent = fileRes.data?.StackFileContent;
|
||||||
|
if (stackFileContent) {
|
||||||
|
await axiosInstance.put(`/api/stacks/${stack.Id}`,
|
||||||
|
{ StackFileContent: stackFileContent, Prune: false, PullImage: true },
|
||||||
|
{ params: { endpointId: stack.EndpointId } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastRedeployStatus(stack.Id, false);
|
||||||
|
logRedeployEvent({
|
||||||
|
stackId: stack.Id,
|
||||||
|
stackName: stack.Name,
|
||||||
|
status: 'success',
|
||||||
|
message: 'Redeploy Auswahl erfolgreich abgeschlossen',
|
||||||
|
endpoint: stack.EndpointId,
|
||||||
|
redeployType: 'Auswahl'
|
||||||
|
});
|
||||||
|
console.log(`✅ Redeploy Auswahl abgeschlossen: ${stack.Name}`);
|
||||||
|
} catch (err) {
|
||||||
|
broadcastRedeployStatus(stack.Id, false);
|
||||||
|
const errorMessage = err.response?.data?.message || err.message;
|
||||||
|
logRedeployEvent({
|
||||||
|
stackId: stack.Id,
|
||||||
|
stackName: stack.Name,
|
||||||
|
status: 'error',
|
||||||
|
message: errorMessage,
|
||||||
|
endpoint: stack.EndpointId,
|
||||||
|
redeployType: 'Auswahl'
|
||||||
|
});
|
||||||
|
console.error(`❌ Fehler beim Redeploy Auswahl für Stack ${stack.Name}:`, errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, message: 'Redeploy Auswahl gestartet' });
|
||||||
|
} catch (err) {
|
||||||
|
const errorMessage = err.response?.data?.message || err.message;
|
||||||
|
console.error(`❌ Fehler beim Redeploy Auswahl:`, errorMessage);
|
||||||
|
logRedeployEvent({
|
||||||
|
stackId: '---',
|
||||||
|
stackName: '---',
|
||||||
|
status: 'error',
|
||||||
|
message: errorMessage,
|
||||||
|
endpoint: ENDPOINT_ID,
|
||||||
|
redeployType: 'Auswahl'
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: errorMessage });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.listen(PORT, '0.0.0.0', () => {
|
server.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`🚀 Backend läuft auf Port ${PORT}`);
|
console.log(`🚀 Backend läuft auf Port ${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -2,6 +2,7 @@ import React from "react";
|
|||||||
import { NavLink, Route, Routes } from "react-router-dom";
|
import { NavLink, Route, Routes } from "react-router-dom";
|
||||||
import Stacks from "./Stacks.jsx";
|
import Stacks from "./Stacks.jsx";
|
||||||
import Logs from "./Logs.jsx";
|
import Logs from "./Logs.jsx";
|
||||||
|
import logo from "./assets/images/stackpulse.png";
|
||||||
|
|
||||||
const navLinkBase =
|
const navLinkBase =
|
||||||
"px-4 py-2 rounded-md font-medium transition-colors duration-150";
|
"px-4 py-2 rounded-md font-medium transition-colors duration-150";
|
||||||
@@ -14,12 +15,12 @@ export default function App() {
|
|||||||
<div className="min-h-screen bg-gray-900 text-white">
|
<div className="min-h-screen bg-gray-900 text-white">
|
||||||
<header className="bg-gray-800 shadow-md">
|
<header className="bg-gray-800 shadow-md">
|
||||||
<div className="max-w-6xl mx-auto px-6 py-6">
|
<div className="max-w-6xl mx-auto px-6 py-6">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
||||||
<div>
|
<div className="flex flex-col items-end gap-1">
|
||||||
<h1 className="text-2xl font-bold text-white">StackPulse</h1>
|
<span className="text-xs text-gray-500">v0.2</span>
|
||||||
<p className="text-gray-400 mt-1">Verwalte deine Docker Stacks</p>
|
<img src={logo} alt="StackPulse" className="h-10 w-auto" />
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex gap-2">
|
<nav className="flex gap-2 items-end">
|
||||||
<NavLink to="/" end className={getNavClass}>
|
<NavLink to="/" end className={getNavClass}>
|
||||||
Stacks
|
Stacks
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|||||||
+148
-48
@@ -40,10 +40,20 @@ const PER_PAGE_OPTIONS = [
|
|||||||
];
|
];
|
||||||
const VALID_PER_PAGE_VALUES = new Set(PER_PAGE_OPTIONS.map((option) => option.value));
|
const VALID_PER_PAGE_VALUES = new Set(PER_PAGE_OPTIONS.map((option) => option.value));
|
||||||
|
|
||||||
|
const REDEPLOY_TYPE_LABELS = {
|
||||||
|
Einzeln: "Einzeln",
|
||||||
|
Alle: "Alle",
|
||||||
|
Auswahl: "Auswahl",
|
||||||
|
single: "Einzeln",
|
||||||
|
all: "Alle",
|
||||||
|
selection: "Auswahl"
|
||||||
|
};
|
||||||
|
|
||||||
const hasActiveFilters = (filters) => Boolean(
|
const hasActiveFilters = (filters) => Boolean(
|
||||||
(filters.stacks && filters.stacks.length) ||
|
(filters.stacks && filters.stacks.length) ||
|
||||||
(filters.statuses && filters.statuses.length) ||
|
(filters.statuses && filters.statuses.length) ||
|
||||||
(filters.endpoints && filters.endpoints.length) ||
|
(filters.endpoints && filters.endpoints.length) ||
|
||||||
|
(filters.redeployTypes && filters.redeployTypes.length) ||
|
||||||
(filters.message && filters.message.trim()) ||
|
(filters.message && filters.message.trim()) ||
|
||||||
(filters.from && filters.from.trim()) ||
|
(filters.from && filters.from.trim()) ||
|
||||||
(filters.to && filters.to.trim())
|
(filters.to && filters.to.trim())
|
||||||
@@ -59,16 +69,19 @@ export default function Logs() {
|
|||||||
const [stackOptions, setStackOptions] = useState([]);
|
const [stackOptions, setStackOptions] = useState([]);
|
||||||
const [statusOptions, setStatusOptions] = useState([]);
|
const [statusOptions, setStatusOptions] = useState([]);
|
||||||
const [endpointOptions, setEndpointOptions] = useState([]);
|
const [endpointOptions, setEndpointOptions] = useState([]);
|
||||||
|
const [redeployTypeOptions, setRedeployTypeOptions] = useState([]);
|
||||||
|
|
||||||
const [selectedStacks, setSelectedStacks] = useState([]);
|
const [selectedStacks, setSelectedStacks] = useState([]);
|
||||||
const [selectedStatuses, setSelectedStatuses] = useState([]);
|
const [selectedStatuses, setSelectedStatuses] = useState([]);
|
||||||
const [selectedEndpoints, setSelectedEndpoints] = useState([]);
|
const [selectedEndpoints, setSelectedEndpoints] = useState([]);
|
||||||
|
const [selectedRedeployTypes, setSelectedRedeployTypes] = useState([]);
|
||||||
const [messageQuery, setMessageQuery] = useState("");
|
const [messageQuery, setMessageQuery] = useState("");
|
||||||
const [fromDate, setFromDate] = useState("");
|
const [fromDate, setFromDate] = useState("");
|
||||||
const [toDate, setToDate] = useState("");
|
const [toDate, setToDate] = useState("");
|
||||||
|
|
||||||
const [filtersOpen, setFiltersOpen] = useState(false);
|
const [filtersOpen, setFiltersOpen] = useState(false);
|
||||||
const [filtersReady, setFiltersReady] = useState(false);
|
const [filtersReady, setFiltersReady] = useState(false);
|
||||||
|
const [optionsInitialized, setOptionsInitialized] = useState(false);
|
||||||
const [refreshSignal, setRefreshSignal] = useState(0);
|
const [refreshSignal, setRefreshSignal] = useState(0);
|
||||||
|
|
||||||
const [perPage, setPerPage] = useState(PER_PAGE_DEFAULT);
|
const [perPage, setPerPage] = useState(PER_PAGE_DEFAULT);
|
||||||
@@ -77,36 +90,42 @@ export default function Logs() {
|
|||||||
const updateFilterOptions = useCallback((payload) => {
|
const updateFilterOptions = useCallback((payload) => {
|
||||||
const logsPayload = Array.isArray(payload) ? payload : payload?.items ?? [];
|
const logsPayload = Array.isArray(payload) ? payload : payload?.items ?? [];
|
||||||
|
|
||||||
setStackOptions((prev) => {
|
const stackMap = new Map();
|
||||||
const map = new Map(prev.map((entry) => [entry.value, entry.label]));
|
const statusSet = new Set();
|
||||||
|
const endpointSet = new Set();
|
||||||
|
const redeployTypeSet = new Set();
|
||||||
|
|
||||||
logsPayload.forEach((log) => {
|
logsPayload.forEach((log) => {
|
||||||
if (!log.stackId) return;
|
if (log.stackId) {
|
||||||
const value = String(log.stackId);
|
const value = String(log.stackId);
|
||||||
if (!map.has(value)) {
|
const label = log.stackName || `Stack ${value}`;
|
||||||
map.set(value, log.stackName || `Stack ${value}`);
|
stackMap.set(value, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.status) {
|
||||||
|
statusSet.add(log.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.endpoint !== null && log.endpoint !== undefined && log.endpoint !== "") {
|
||||||
|
endpointSet.add(String(log.endpoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.redeployType) {
|
||||||
|
redeployTypeSet.add(log.redeployType);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return Array.from(map.entries())
|
|
||||||
|
setStackOptions(Array.from(stackMap.entries())
|
||||||
.map(([value, label]) => ({ value, label }))
|
.map(([value, label]) => ({ value, label }))
|
||||||
.sort((a, b) => a.label.localeCompare(b.label));
|
.sort((a, b) => a.label.localeCompare(b.label)));
|
||||||
});
|
|
||||||
|
|
||||||
setStatusOptions((prev) => {
|
setStatusOptions(Array.from(statusSet).sort());
|
||||||
const next = new Set(prev);
|
|
||||||
logsPayload.forEach((log) => {
|
|
||||||
if (log.status) next.add(log.status);
|
|
||||||
});
|
|
||||||
return Array.from(next).sort();
|
|
||||||
});
|
|
||||||
|
|
||||||
setEndpointOptions((prev) => {
|
setEndpointOptions(Array.from(endpointSet)
|
||||||
const next = new Set(prev);
|
.sort((a, b) => a.localeCompare(b, undefined, { numeric: true })));
|
||||||
logsPayload.forEach((log) => {
|
|
||||||
if (log.endpoint === null || log.endpoint === undefined || log.endpoint === "") return;
|
setRedeployTypeOptions(Array.from(redeployTypeSet).sort());
|
||||||
next.add(String(log.endpoint));
|
setOptionsInitialized(true);
|
||||||
});
|
|
||||||
return Array.from(next).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const stackLabelMap = useMemo(() => {
|
const stackLabelMap = useMemo(() => {
|
||||||
@@ -117,6 +136,38 @@ export default function Logs() {
|
|||||||
return map;
|
return map;
|
||||||
}, [stackOptions]);
|
}, [stackOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!optionsInitialized) return;
|
||||||
|
setSelectedStacks((prev) => {
|
||||||
|
const valid = prev.filter((value) => stackOptions.some((option) => option.value === value));
|
||||||
|
return valid.length === prev.length ? prev : valid;
|
||||||
|
});
|
||||||
|
}, [optionsInitialized, stackOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!optionsInitialized) return;
|
||||||
|
setSelectedStatuses((prev) => {
|
||||||
|
const valid = prev.filter((value) => statusOptions.includes(value));
|
||||||
|
return valid.length === prev.length ? prev : valid;
|
||||||
|
});
|
||||||
|
}, [optionsInitialized, statusOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!optionsInitialized) return;
|
||||||
|
setSelectedEndpoints((prev) => {
|
||||||
|
const valid = prev.filter((value) => endpointOptions.includes(value));
|
||||||
|
return valid.length === prev.length ? prev : valid;
|
||||||
|
});
|
||||||
|
}, [optionsInitialized, endpointOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!optionsInitialized) return;
|
||||||
|
setSelectedRedeployTypes((prev) => {
|
||||||
|
const valid = prev.filter((value) => redeployTypeOptions.includes(value));
|
||||||
|
return valid.length === prev.length ? prev : valid;
|
||||||
|
});
|
||||||
|
}, [optionsInitialized, redeployTypeOptions]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
setFiltersReady(true);
|
setFiltersReady(true);
|
||||||
@@ -128,29 +179,17 @@ export default function Logs() {
|
|||||||
if (storedValue) {
|
if (storedValue) {
|
||||||
const parsed = JSON.parse(storedValue);
|
const parsed = JSON.parse(storedValue);
|
||||||
const storedFilters = parsed?.filters ?? parsed ?? {};
|
const storedFilters = parsed?.filters ?? parsed ?? {};
|
||||||
const storedOptions = parsed?.options ?? {};
|
|
||||||
const storedPagination = parsed?.pagination ?? {};
|
const storedPagination = parsed?.pagination ?? {};
|
||||||
|
|
||||||
setSelectedStacks(storedFilters.stacks || []);
|
setSelectedStacks(storedFilters.stacks || []);
|
||||||
setSelectedStatuses(storedFilters.statuses || []);
|
setSelectedStatuses(storedFilters.statuses || []);
|
||||||
setSelectedEndpoints(storedFilters.endpoints || []);
|
setSelectedEndpoints(storedFilters.endpoints || []);
|
||||||
|
setSelectedRedeployTypes(storedFilters.redeployTypes || []);
|
||||||
setMessageQuery(storedFilters.message || "");
|
setMessageQuery(storedFilters.message || "");
|
||||||
setFromDate(storedFilters.from || "");
|
setFromDate(storedFilters.from || "");
|
||||||
setToDate(storedFilters.to || "");
|
setToDate(storedFilters.to || "");
|
||||||
setFiltersOpen(hasActiveFilters(storedFilters));
|
setFiltersOpen(hasActiveFilters(storedFilters));
|
||||||
|
|
||||||
if (Array.isArray(storedOptions.stacks) && storedOptions.stacks.length) {
|
|
||||||
setStackOptions(storedOptions.stacks);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(storedOptions.statuses) && storedOptions.statuses.length) {
|
|
||||||
setStatusOptions(storedOptions.statuses);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(storedOptions.endpoints) && storedOptions.endpoints.length) {
|
|
||||||
setEndpointOptions(storedOptions.endpoints);
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawPerPage = storedPagination.perPage;
|
const rawPerPage = storedPagination.perPage;
|
||||||
if (rawPerPage !== undefined) {
|
if (rawPerPage !== undefined) {
|
||||||
const parsedPerPage = String(rawPerPage);
|
const parsedPerPage = String(rawPerPage);
|
||||||
@@ -186,6 +225,10 @@ export default function Logs() {
|
|||||||
params.endpoints = selectedEndpoints.join(",");
|
params.endpoints = selectedEndpoints.join(",");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (selectedRedeployTypes.length) {
|
||||||
|
params.redeployTypes = selectedRedeployTypes.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
if (messageQuery.trim()) {
|
if (messageQuery.trim()) {
|
||||||
params.message = messageQuery.trim();
|
params.message = messageQuery.trim();
|
||||||
}
|
}
|
||||||
@@ -201,7 +244,7 @@ export default function Logs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
}, [selectedStacks, selectedStatuses, selectedEndpoints, messageQuery, fromDate, toDate]);
|
}, [selectedStacks, selectedStatuses, selectedEndpoints, selectedRedeployTypes, messageQuery, fromDate, toDate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!filtersReady) return;
|
if (!filtersReady) return;
|
||||||
@@ -226,10 +269,11 @@ export default function Logs() {
|
|||||||
stacks: selectedStacks,
|
stacks: selectedStacks,
|
||||||
statuses: selectedStatuses,
|
statuses: selectedStatuses,
|
||||||
endpoints: selectedEndpoints,
|
endpoints: selectedEndpoints,
|
||||||
|
redeployTypes: selectedRedeployTypes,
|
||||||
message: messageQuery,
|
message: messageQuery,
|
||||||
from: fromDate,
|
from: fromDate,
|
||||||
to: toDate
|
to: toDate
|
||||||
}), [selectedStacks, selectedStatuses, selectedEndpoints, messageQuery, fromDate, toDate]);
|
}), [selectedStacks, selectedStatuses, selectedEndpoints, selectedRedeployTypes, messageQuery, fromDate, toDate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!filtersReady) return;
|
if (!filtersReady) return;
|
||||||
@@ -292,11 +336,6 @@ export default function Logs() {
|
|||||||
FILTER_STORAGE_KEY,
|
FILTER_STORAGE_KEY,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
filters: currentFilters,
|
filters: currentFilters,
|
||||||
options: {
|
|
||||||
stacks: stackOptions,
|
|
||||||
statuses: statusOptions,
|
|
||||||
endpoints: endpointOptions,
|
|
||||||
},
|
|
||||||
pagination: {
|
pagination: {
|
||||||
perPage,
|
perPage,
|
||||||
page
|
page
|
||||||
@@ -306,7 +345,7 @@ export default function Logs() {
|
|||||||
} catch (storageError) {
|
} catch (storageError) {
|
||||||
console.error("⚠️ Konnte Filter nicht speichern:", storageError);
|
console.error("⚠️ Konnte Filter nicht speichern:", storageError);
|
||||||
}
|
}
|
||||||
}, [filtersReady, currentFilters, stackOptions, statusOptions, endpointOptions, perPage, page]);
|
}, [filtersReady, currentFilters, perPage, page]);
|
||||||
|
|
||||||
const handleMultiSelectChange = (setter) => (event) => {
|
const handleMultiSelectChange = (setter) => (event) => {
|
||||||
const values = Array.from(event.target.selectedOptions).map((option) => option.value);
|
const values = Array.from(event.target.selectedOptions).map((option) => option.value);
|
||||||
@@ -322,6 +361,7 @@ export default function Logs() {
|
|||||||
setSelectedStacks([]);
|
setSelectedStacks([]);
|
||||||
setSelectedStatuses([]);
|
setSelectedStatuses([]);
|
||||||
setSelectedEndpoints([]);
|
setSelectedEndpoints([]);
|
||||||
|
setSelectedRedeployTypes([]);
|
||||||
setMessageQuery("");
|
setMessageQuery("");
|
||||||
setFromDate("");
|
setFromDate("");
|
||||||
setToDate("");
|
setToDate("");
|
||||||
@@ -380,11 +420,22 @@ export default function Logs() {
|
|||||||
];
|
];
|
||||||
}, [endpointOptions]);
|
}, [endpointOptions]);
|
||||||
|
|
||||||
|
const redeployTypeSelectOptions = useMemo(() => {
|
||||||
|
const entries = redeployTypeOptions
|
||||||
|
.filter((type) => type !== ALL_OPTION_VALUE)
|
||||||
|
.map((type) => ({ value: type, label: REDEPLOY_TYPE_LABELS[type] ?? type }));
|
||||||
|
return [
|
||||||
|
{ value: ALL_OPTION_VALUE, label: ALL_OPTION_LABEL },
|
||||||
|
...entries
|
||||||
|
];
|
||||||
|
}, [redeployTypeOptions]);
|
||||||
|
|
||||||
const activeFilterCount = useMemo(() => {
|
const activeFilterCount = useMemo(() => {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
if (selectedStacks.length) count += selectedStacks.length;
|
if (selectedStacks.length) count += selectedStacks.length;
|
||||||
if (selectedStatuses.length) count += selectedStatuses.length;
|
if (selectedStatuses.length) count += selectedStatuses.length;
|
||||||
if (selectedEndpoints.length) count += selectedEndpoints.length;
|
if (selectedEndpoints.length) count += selectedEndpoints.length;
|
||||||
|
if (selectedRedeployTypes.length) count += selectedRedeployTypes.length;
|
||||||
if (messageQuery.trim()) count += 1;
|
if (messageQuery.trim()) count += 1;
|
||||||
if (fromDate) count += 1;
|
if (fromDate) count += 1;
|
||||||
if (toDate) count += 1;
|
if (toDate) count += 1;
|
||||||
@@ -549,7 +600,7 @@ export default function Logs() {
|
|||||||
|
|
||||||
{filtersOpen && (
|
{filtersOpen && (
|
||||||
<div className="space-y-4 border-t border-gray-700 px-4 py-4">
|
<div className="space-y-4 border-t border-gray-700 px-4 py-4">
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-300">Stack</label>
|
<label className="mb-2 block text-sm font-medium text-gray-300">Stack</label>
|
||||||
<select
|
<select
|
||||||
@@ -664,7 +715,45 @@ export default function Logs() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:col-span-2 lg:col-span-3">
|
<div>
|
||||||
|
<label className="mb-2 block text-sm font-medium text-gray-300">Redeploy-Typ</label>
|
||||||
|
<select
|
||||||
|
multiple
|
||||||
|
value={selectedRedeployTypes}
|
||||||
|
onChange={handleMultiSelectChange(setSelectedRedeployTypes)}
|
||||||
|
className="w-full min-h-[8rem] rounded-md border border-gray-700 bg-gray-900/70 px-3 py-2 text-gray-200 focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||||
|
>
|
||||||
|
{redeployTypeSelectOptions.map(({ value, label }) => (
|
||||||
|
<option
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
className={`bg-gray-900 text-gray-200 ${value === ALL_OPTION_VALUE ? 'font-semibold text-gray-100' : ''}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="mt-2 min-h-[1.5rem] text-xs text-gray-400">
|
||||||
|
{selectedRedeployTypes.length === 0 ? (
|
||||||
|
<span className="rounded-full bg-gray-700/60 px-2 py-0.5 text-gray-300">
|
||||||
|
Alle Typen
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedRedeployTypes.map((type) => (
|
||||||
|
<span
|
||||||
|
key={type}
|
||||||
|
className="rounded-full bg-teal-500/20 px-2 py-0.5 text-teal-200"
|
||||||
|
>
|
||||||
|
{REDEPLOY_TYPE_LABELS[type] ?? type}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:col-span-2 lg:col-span-4">
|
||||||
<label className="mb-2 block text-sm font-medium text-gray-300">Nachricht (Freitext)</label>
|
<label className="mb-2 block text-sm font-medium text-gray-300">Nachricht (Freitext)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -723,6 +812,7 @@ export default function Logs() {
|
|||||||
<tr className="text-left text-sm uppercase tracking-wide text-gray-400">
|
<tr className="text-left text-sm uppercase tracking-wide text-gray-400">
|
||||||
<th className="px-4 py-3">Zeitpunkt</th>
|
<th className="px-4 py-3">Zeitpunkt</th>
|
||||||
<th className="px-4 py-3">Stack</th>
|
<th className="px-4 py-3">Stack</th>
|
||||||
|
<th className="px-4 py-3">Art</th>
|
||||||
<th className="px-4 py-3">Status</th>
|
<th className="px-4 py-3">Status</th>
|
||||||
<th className="px-4 py-3">Nachricht</th>
|
<th className="px-4 py-3">Nachricht</th>
|
||||||
<th className="px-4 py-3">Endpoint</th>
|
<th className="px-4 py-3">Endpoint</th>
|
||||||
@@ -732,13 +822,18 @@ export default function Logs() {
|
|||||||
<tbody className="divide-y divide-gray-700 text-sm">
|
<tbody className="divide-y divide-gray-700 text-sm">
|
||||||
{logs.length === 0 && !loading && (
|
{logs.length === 0 && !loading && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan="6" className="px-4 py-6 text-center text-gray-400">
|
<td colSpan="7" className="px-4 py-6 text-center text-gray-400">
|
||||||
Keine Logs vorhanden.
|
Keine Logs vorhanden.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{logs.map((log) => {
|
{logs.map((log) => {
|
||||||
const statusClass = STATUS_COLORS[log.status] || "text-blue-300";
|
const statusClass = STATUS_COLORS[log.status] || "text-blue-300";
|
||||||
|
const stackDisplayName = log.stackName || "Unbekannt";
|
||||||
|
const showStackId = stackDisplayName !== '---' && log.stackId !== undefined && log.stackId !== null;
|
||||||
|
const redeployTypeLabel = log.redeployType
|
||||||
|
? (REDEPLOY_TYPE_LABELS[log.redeployType] ?? log.redeployType)
|
||||||
|
: '---';
|
||||||
return (
|
return (
|
||||||
<tr key={log.id} className="hover:bg-gray-700/40">
|
<tr key={log.id} className="hover:bg-gray-700/40">
|
||||||
<td className="px-4 py-3 whitespace-nowrap text-gray-300">
|
<td className="px-4 py-3 whitespace-nowrap text-gray-300">
|
||||||
@@ -746,10 +841,15 @@ export default function Logs() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-gray-200">
|
<td className="px-4 py-3 text-gray-200">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium">{log.stackName || "Unbekannt"}</span>
|
<span className="font-medium">{stackDisplayName}</span>
|
||||||
|
{showStackId && (
|
||||||
<span className="text-xs text-gray-400">ID: {log.stackId}</span>
|
<span className="text-xs text-gray-400">ID: {log.stackId}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="px-4 py-3 text-gray-300">
|
||||||
|
{redeployTypeLabel}
|
||||||
|
</td>
|
||||||
<td className={`px-4 py-3 font-semibold ${statusClass}`}>
|
<td className={`px-4 py-3 font-semibold ${statusClass}`}>
|
||||||
{log.status}
|
{log.status}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
+82
-5
@@ -6,6 +6,7 @@ export default function Stacks() {
|
|||||||
const [stacks, setStacks] = useState([]);
|
const [stacks, setStacks] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [selectedStackIds, setSelectedStackIds] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const socket = io("/", {
|
const socket = io("/", {
|
||||||
@@ -42,7 +43,8 @@ const socket = io("/", {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("/api/stacks");
|
const res = await axios.get("/api/stacks");
|
||||||
setStacks(res.data.map(stack => ({ ...stack, redeploying: stack.redeploying || false })));
|
const sortedStacks = [...res.data].sort((a, b) => a.Name.localeCompare(b.Name));
|
||||||
|
setStacks(sortedStacks.map(stack => ({ ...stack, redeploying: stack.redeploying || false })));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ Fehler beim Abrufen der Stacks:", err);
|
console.error("❌ Fehler beim Abrufen der Stacks:", err);
|
||||||
setError("Fehler beim Laden der Stacks");
|
setError("Fehler beim Laden der Stacks");
|
||||||
@@ -55,7 +57,24 @@ const socket = io("/", {
|
|||||||
fetchStacks();
|
fetchStacks();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedStackIds(prev => {
|
||||||
|
const filtered = prev.filter(id => stacks.some(stack => stack.Id === id));
|
||||||
|
return filtered.length === prev.length ? prev : filtered;
|
||||||
|
});
|
||||||
|
}, [stacks]);
|
||||||
|
|
||||||
|
const toggleStackSelection = (stackId, disabled) => {
|
||||||
|
if (disabled) return;
|
||||||
|
setSelectedStackIds(prev =>
|
||||||
|
prev.includes(stackId)
|
||||||
|
? prev.filter(id => id !== stackId)
|
||||||
|
: [...prev, stackId]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const handleRedeploy = async (stackId) => {
|
const handleRedeploy = async (stackId) => {
|
||||||
|
setSelectedStackIds(prev => prev.filter(id => id !== stackId));
|
||||||
setStacks(prev =>
|
setStacks(prev =>
|
||||||
prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: true } : stack)
|
prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: true } : stack)
|
||||||
);
|
);
|
||||||
@@ -76,6 +95,7 @@ const socket = io("/", {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await axios.put("/api/stacks/redeploy-all");
|
await axios.put("/api/stacks/redeploy-all");
|
||||||
|
setSelectedStackIds([]);
|
||||||
// Statusupdates kommen über Socket.IO
|
// Statusupdates kommen über Socket.IO
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("❌ Fehler beim Redeploy ALL:", err);
|
console.error("❌ Fehler beim Redeploy ALL:", err);
|
||||||
@@ -83,6 +103,53 @@ const socket = io("/", {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRedeploySelection = async () => {
|
||||||
|
if (!selectedStackIds.length) return;
|
||||||
|
|
||||||
|
setStacks(prev =>
|
||||||
|
prev.map(stack =>
|
||||||
|
selectedStackIds.includes(stack.Id)
|
||||||
|
? { ...stack, redeploying: true }
|
||||||
|
: stack
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.put("/api/stacks/redeploy-selection", { stackIds: selectedStackIds });
|
||||||
|
setSelectedStackIds([]);
|
||||||
|
// Statusupdates kommen über Socket.IO
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Fehler beim Redeploy Auswahl:", err);
|
||||||
|
setStacks(prev =>
|
||||||
|
prev.map(stack =>
|
||||||
|
selectedStackIds.includes(stack.Id)
|
||||||
|
? { ...stack, redeploying: false }
|
||||||
|
: stack
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasSelection = selectedStackIds.length > 0;
|
||||||
|
const bulkButtonLabel = hasSelection
|
||||||
|
? `Redeploy Auswahl (${selectedStackIds.length})`
|
||||||
|
: 'Redeploy All';
|
||||||
|
|
||||||
|
const bulkActionDisabled = hasSelection
|
||||||
|
? selectedStackIds.every(id => {
|
||||||
|
const targetStack = stacks.find(stack => stack.Id === id);
|
||||||
|
return targetStack?.redeploying;
|
||||||
|
})
|
||||||
|
: stacks.every(stack => stack.redeploying);
|
||||||
|
|
||||||
|
const handleBulkRedeploy = () => {
|
||||||
|
if (hasSelection) {
|
||||||
|
handleRedeploySelection();
|
||||||
|
} else {
|
||||||
|
handleRedeployAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) return <p className="text-gray-400">Lade Stacks...</p>;
|
if (loading) return <p className="text-gray-400">Lade Stacks...</p>;
|
||||||
if (error) return <p className="text-red-400">{error}</p>;
|
if (error) return <p className="text-red-400">{error}</p>;
|
||||||
|
|
||||||
@@ -90,24 +157,34 @@ const socket = io("/", {
|
|||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex justify-end mb-4">
|
<div className="flex justify-end mb-4">
|
||||||
<button
|
<button
|
||||||
onClick={handleRedeployAll}
|
onClick={handleBulkRedeploy}
|
||||||
className="px-5 py-2 rounded-lg font-medium transition bg-purple-500 hover:bg-purple-600"
|
disabled={bulkActionDisabled}
|
||||||
|
className={`px-5 py-2 rounded-lg font-medium transition ${bulkActionDisabled ? 'bg-purple-900 cursor-not-allowed text-gray-400' : 'bg-purple-500 hover:bg-purple-600'}`}
|
||||||
>
|
>
|
||||||
Redeploy All
|
{bulkButtonLabel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
{stacks.map(stack => {
|
{stacks.map(stack => {
|
||||||
const isRedeploying = stack.redeploying;
|
const isRedeploying = stack.redeploying;
|
||||||
|
const isSelected = selectedStackIds.includes(stack.Id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={stack.Id}
|
key={stack.Id}
|
||||||
className={`flex justify-between items-center p-5 rounded-xl shadow-lg transition
|
className={`flex justify-between items-center p-5 rounded-xl shadow-lg transition border
|
||||||
|
${isSelected ? 'border-purple-500 ring-1 ring-purple-500/40' : 'border-transparent'}
|
||||||
${isRedeploying ? "bg-gray-700 cursor-not-allowed" : "bg-gray-800 hover:bg-gray-700"}`}
|
${isRedeploying ? "bg-gray-700 cursor-not-allowed" : "bg-gray-800 hover:bg-gray-700"}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={() => toggleStackSelection(stack.Id, isRedeploying)}
|
||||||
|
className="h-5 w-5 text-purple-500 focus:ring-purple-400 border-gray-600 bg-gray-900 rounded"
|
||||||
|
disabled={isRedeploying}
|
||||||
|
/>
|
||||||
<div className={`w-12 h-12 flex items-center justify-center rounded-full
|
<div className={`w-12 h-12 flex items-center justify-center rounded-full
|
||||||
${stack.updateStatus === "✅" ? "bg-green-500" :
|
${stack.updateStatus === "✅" ? "bg-green-500" :
|
||||||
stack.updateStatus === "⚠️" ? "bg-yellow-500" :
|
stack.updateStatus === "⚠️" ? "bg-yellow-500" :
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Reference in New Issue
Block a user