Update
This commit is contained in:
+36
-6
@@ -62,6 +62,8 @@ const REDEPLOY_TYPES = {
|
|||||||
SELECTION: 'Auswahl'
|
SELECTION: 'Auswahl'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SELF_STACK_ID = process.env.SELF_STACK_ID ? String(process.env.SELF_STACK_ID) : null;
|
||||||
|
|
||||||
const isStackOutdated = async (stackId) => {
|
const isStackOutdated = async (stackId) => {
|
||||||
try {
|
try {
|
||||||
const statusRes = await axiosInstance.get(`/api/stacks/${stackId}/images_status?refresh=true`);
|
const statusRes = await axiosInstance.get(`/api/stacks/${stackId}/images_status?refresh=true`);
|
||||||
@@ -76,7 +78,9 @@ const filterOutdatedStacks = async (stacks = []) => {
|
|||||||
const results = await Promise.all(
|
const results = await Promise.all(
|
||||||
stacks.map(async (stack) => ({
|
stacks.map(async (stack) => ({
|
||||||
stack,
|
stack,
|
||||||
outdated: await isStackOutdated(stack.Id)
|
outdated: SELF_STACK_ID && String(stack.Id) === SELF_STACK_ID
|
||||||
|
? false
|
||||||
|
: await isStackOutdated(stack.Id)
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -95,11 +99,31 @@ app.get('/api/stacks', async (req, res) => {
|
|||||||
const stacksRes = await axiosInstance.get('/api/stacks');
|
const stacksRes = await axiosInstance.get('/api/stacks');
|
||||||
const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID);
|
const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID);
|
||||||
|
|
||||||
const uniqueStacksMap = {};
|
const stacksByName = new Map();
|
||||||
|
const duplicateNames = new Set();
|
||||||
|
|
||||||
filteredStacks.forEach(stack => {
|
filteredStacks.forEach(stack => {
|
||||||
if (!uniqueStacksMap[stack.Name]) uniqueStacksMap[stack.Name] = stack;
|
const name = stack.Name;
|
||||||
|
const isSelf = SELF_STACK_ID && String(stack.Id) === SELF_STACK_ID;
|
||||||
|
const existingEntry = stacksByName.get(name);
|
||||||
|
|
||||||
|
if (!existingEntry) {
|
||||||
|
stacksByName.set(name, { stack, isSelf });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicateNames.add(name);
|
||||||
|
|
||||||
|
if (!existingEntry.isSelf && isSelf) {
|
||||||
|
stacksByName.set(name, { stack, isSelf });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const uniqueStacks = Object.values(uniqueStacksMap);
|
|
||||||
|
const uniqueStacks = Array.from(stacksByName.values()).map(entry => entry.stack);
|
||||||
|
|
||||||
|
if (duplicateNames.size) {
|
||||||
|
console.warn(`⚠️ [API] GET /api/stacks: Doppelte Stack-Namen erkannt: ${Array.from(duplicateNames).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
const stacksWithStatus = await Promise.all(
|
const stacksWithStatus = await Promise.all(
|
||||||
uniqueStacks.map(async (stack) => {
|
uniqueStacks.map(async (stack) => {
|
||||||
@@ -111,11 +135,17 @@ app.get('/api/stacks', async (req, res) => {
|
|||||||
return {
|
return {
|
||||||
...stack,
|
...stack,
|
||||||
updateStatus: statusEmoji,
|
updateStatus: statusEmoji,
|
||||||
redeploying: redeployingStacks[stack.Id] || false
|
redeploying: redeployingStacks[stack.Id] || false,
|
||||||
|
redeployDisabled: SELF_STACK_ID ? String(stack.Id) === SELF_STACK_ID : false
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`❌ Fehler beim Abrufen des Status für Stack ${stack.Id}:`, err.message);
|
console.error(`❌ Fehler beim Abrufen des Status für Stack ${stack.Id}:`, err.message);
|
||||||
return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false };
|
return {
|
||||||
|
...stack,
|
||||||
|
updateStatus: '❌',
|
||||||
|
redeploying: redeployingStacks[stack.Id] || false,
|
||||||
|
redeployDisabled: SELF_STACK_ID ? String(stack.Id) === SELF_STACK_ID : false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ services:
|
|||||||
PORTAINER_URL: "Your_Portainer_Server_Address"
|
PORTAINER_URL: "Your_Portainer_Server_Address"
|
||||||
PORTAINER_API_KEY: "Your_Portainer_API_Key"
|
PORTAINER_API_KEY: "Your_Portainer_API_Key"
|
||||||
PORTAINER_ENDPOINT_ID: "Your_Portainer_Endpoint_ID"
|
PORTAINER_ENDPOINT_ID: "Your_Portainer_Endpoint_ID"
|
||||||
|
SELF_STACK_ID: "Stackpulse ID"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+45
-18
@@ -8,6 +8,20 @@ export default function Stacks() {
|
|||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [selectedStackIds, setSelectedStackIds] = useState([]);
|
const [selectedStackIds, setSelectedStackIds] = useState([]);
|
||||||
|
|
||||||
|
const mergeStackState = (previousStacks, incomingStacks) => {
|
||||||
|
const prevMap = new Map(previousStacks.map((stack) => [stack.Id, stack]));
|
||||||
|
const sortedIncoming = [...incomingStacks].sort((a, b) => a.Name.localeCompare(b.Name));
|
||||||
|
|
||||||
|
return sortedIncoming.map((stack) => {
|
||||||
|
const previous = prevMap.get(stack.Id);
|
||||||
|
return {
|
||||||
|
...stack,
|
||||||
|
redeploying: previous?.redeploying || stack.redeploying || false,
|
||||||
|
redeployDisabled: stack.redeployDisabled ?? previous?.redeployDisabled ?? false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const socket = io("/", {
|
const socket = io("/", {
|
||||||
path: "/socket.io",
|
path: "/socket.io",
|
||||||
@@ -18,22 +32,25 @@ export default function Stacks() {
|
|||||||
socket.on("redeployStatus", async ({ stackId, status }) => {
|
socket.on("redeployStatus", async ({ stackId, status }) => {
|
||||||
console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`);
|
console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`);
|
||||||
|
|
||||||
|
setStacks(prev =>
|
||||||
|
prev.map(stack => {
|
||||||
|
if (stack.Id !== stackId) return stack;
|
||||||
|
return {
|
||||||
|
...stack,
|
||||||
|
redeploying: status,
|
||||||
|
updateStatus: status ? stack.updateStatus : '✅'
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (!status) {
|
if (!status) {
|
||||||
// Status nach Redeploy neu vom Server holen
|
// Status nach Redeploy neu vom Server holen
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("/api/stacks");
|
const res = await axios.get("/api/stacks");
|
||||||
const sortedStacks = [...res.data].sort((a, b) => a.Name.localeCompare(b.Name));
|
setStacks(prev => mergeStackState(prev, res.data));
|
||||||
setStacks(sortedStacks.map(stack => ({ ...stack, redeploying: stack.redeploying || false })));
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Fehler beim Aktualisieren nach Redeploy:", err);
|
console.error("Fehler beim Aktualisieren nach Redeploy:", err);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// UI direkt auf redeploying setzen
|
|
||||||
setStacks(prev =>
|
|
||||||
prev.map(stack =>
|
|
||||||
stack.Id === stackId ? { ...stack, redeploying: true } : stack
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -44,8 +61,7 @@ export default function Stacks() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await axios.get("/api/stacks");
|
const res = await axios.get("/api/stacks");
|
||||||
const sortedStacks = [...res.data].sort((a, b) => a.Name.localeCompare(b.Name));
|
setStacks(prev => mergeStackState(prev, res.data));
|
||||||
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");
|
||||||
@@ -62,7 +78,7 @@ export default function Stacks() {
|
|||||||
setSelectedStackIds(prev => {
|
setSelectedStackIds(prev => {
|
||||||
const filtered = prev.filter(id => {
|
const filtered = prev.filter(id => {
|
||||||
const match = stacks.find(stack => stack.Id === id);
|
const match = stacks.find(stack => stack.Id === id);
|
||||||
return match && match.updateStatus !== '✅';
|
return match && match.updateStatus !== '✅' && !match.redeployDisabled;
|
||||||
});
|
});
|
||||||
return filtered.length === prev.length ? prev : filtered;
|
return filtered.length === prev.length ? prev : filtered;
|
||||||
});
|
});
|
||||||
@@ -78,6 +94,9 @@ export default function Stacks() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRedeploy = async (stackId) => {
|
const handleRedeploy = async (stackId) => {
|
||||||
|
const targetStack = stacks.find((stack) => stack.Id === stackId);
|
||||||
|
if (targetStack?.redeployDisabled) return;
|
||||||
|
|
||||||
setSelectedStackIds((prev) => prev.filter((id) => id !== stackId));
|
setSelectedStackIds((prev) => prev.filter((id) => id !== stackId));
|
||||||
setStacks((prev) =>
|
setStacks((prev) =>
|
||||||
prev.map((stack) =>
|
prev.map((stack) =>
|
||||||
@@ -99,7 +118,7 @@ export default function Stacks() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRedeployAll = async () => {
|
const handleRedeployAll = async () => {
|
||||||
const outdatedStacks = stacks.filter((stack) => stack.updateStatus !== '✅');
|
const outdatedStacks = stacks.filter((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled);
|
||||||
if (!outdatedStacks.length) return;
|
if (!outdatedStacks.length) return;
|
||||||
|
|
||||||
const outdatedIds = new Set(outdatedStacks.map((stack) => stack.Id));
|
const outdatedIds = new Set(outdatedStacks.map((stack) => stack.Id));
|
||||||
@@ -133,7 +152,7 @@ export default function Stacks() {
|
|||||||
|
|
||||||
const eligibleIds = selectedStackIds.filter((id) => {
|
const eligibleIds = selectedStackIds.filter((id) => {
|
||||||
const stack = stacks.find((entry) => entry.Id === id);
|
const stack = stacks.find((entry) => entry.Id === id);
|
||||||
return stack && stack.updateStatus !== '✅';
|
return stack && stack.updateStatus !== '✅' && !stack.redeployDisabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!eligibleIds.length) {
|
if (!eligibleIds.length) {
|
||||||
@@ -168,7 +187,7 @@ export default function Stacks() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasSelection = selectedStackIds.length > 0;
|
const hasSelection = selectedStackIds.length > 0;
|
||||||
const hasOutdatedStacks = stacks.some((stack) => stack.updateStatus !== '✅');
|
const hasOutdatedStacks = stacks.some((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled);
|
||||||
const bulkButtonLabel = hasSelection
|
const bulkButtonLabel = hasSelection
|
||||||
? `Redeploy Auswahl (${selectedStackIds.length})`
|
? `Redeploy Auswahl (${selectedStackIds.length})`
|
||||||
: 'Redeploy All';
|
: 'Redeploy All';
|
||||||
@@ -176,9 +195,9 @@ export default function Stacks() {
|
|||||||
const bulkActionDisabled = hasSelection
|
const bulkActionDisabled = hasSelection
|
||||||
? selectedStackIds.length === 0 || selectedStackIds.every(id => {
|
? selectedStackIds.length === 0 || selectedStackIds.every(id => {
|
||||||
const targetStack = stacks.find(stack => stack.Id === id);
|
const targetStack = stacks.find(stack => stack.Id === id);
|
||||||
return !targetStack || targetStack.redeploying || targetStack.updateStatus === '✅';
|
return !targetStack || targetStack.redeploying || targetStack.updateStatus === '✅' || targetStack.redeployDisabled;
|
||||||
})
|
})
|
||||||
: !hasOutdatedStacks || stacks.every(stack => stack.updateStatus !== '✅' || stack.redeploying);
|
: !hasOutdatedStacks || stacks.every(stack => stack.updateStatus !== '✅' || stack.redeploying || stack.redeployDisabled);
|
||||||
|
|
||||||
const handleBulkRedeploy = () => {
|
const handleBulkRedeploy = () => {
|
||||||
if (hasSelection) {
|
if (hasSelection) {
|
||||||
@@ -208,7 +227,8 @@ export default function Stacks() {
|
|||||||
const isRedeploying = stack.redeploying;
|
const isRedeploying = stack.redeploying;
|
||||||
const isSelected = selectedStackIds.includes(stack.Id);
|
const isSelected = selectedStackIds.includes(stack.Id);
|
||||||
const isCurrent = stack.updateStatus === '✅';
|
const isCurrent = stack.updateStatus === '✅';
|
||||||
const isSelectable = !isRedeploying && !isCurrent;
|
const isSelfStack = Boolean(stack.redeployDisabled);
|
||||||
|
const isSelectable = !isRedeploying && !isCurrent && !isSelfStack;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -243,6 +263,11 @@ export default function Stacks() {
|
|||||||
<span className="text-xs uppercase tracking-wide text-orange-300">Redeploy</span>
|
<span className="text-xs uppercase tracking-wide text-orange-300">Redeploy</span>
|
||||||
<span className="text-orange-200">läuft…</span>
|
<span className="text-orange-200">läuft…</span>
|
||||||
</>
|
</>
|
||||||
|
) : isSelfStack ? (
|
||||||
|
<>
|
||||||
|
<span className="text-xs uppercase tracking-wide text-gray-400">System</span>
|
||||||
|
<span className="text-gray-300">Redeploy deaktiviert</span>
|
||||||
|
</>
|
||||||
) : isCurrent ? (
|
) : isCurrent ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-xs uppercase tracking-wide text-gray-400">Status</span>
|
<span className="text-xs uppercase tracking-wide text-gray-400">Status</span>
|
||||||
@@ -250,6 +275,8 @@ export default function Stacks() {
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<span className="text-xs uppercase tracking-wide text-gray-400">Status</span>
|
||||||
|
<span className="text-amber-300">Veraltet</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRedeploy(stack.Id)}
|
onClick={() => handleRedeploy(stack.Id)}
|
||||||
disabled={isRedeploying}
|
disabled={isRedeploying}
|
||||||
|
|||||||
Reference in New Issue
Block a user