Stack Filter Begin
This commit is contained in:
+447
-22
@@ -1,12 +1,32 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const SELECTION_PROMPT_STORAGE_KEY = "stackSelectionPreference";
|
||||
|
||||
const PER_PAGE_DEFAULT = "50";
|
||||
const PER_PAGE_OPTIONS = [
|
||||
{ value: "10", label: "10" },
|
||||
{ value: "25", label: "25" },
|
||||
{ value: "50", label: "50" },
|
||||
{ value: "100", label: "100" },
|
||||
{ value: "all", label: "Alle" }
|
||||
];
|
||||
const VALID_PER_PAGE_VALUES = new Set(PER_PAGE_OPTIONS.map((option) => option.value));
|
||||
|
||||
export default function Stacks() {
|
||||
const [stacks, setStacks] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [selectedStackIds, setSelectedStackIds] = useState([]);
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectionPromptVisible, setSelectionPromptVisible] = useState(false);
|
||||
const [rememberSelectionChoice, setRememberSelectionChoice] = useState(false);
|
||||
const [selectionPreferenceStored, setSelectionPreferenceStored] = useState(false);
|
||||
const [showRedeployingOnly, setShowRedeployingOnly] = useState(false);
|
||||
const [perPage, setPerPage] = useState(PER_PAGE_DEFAULT);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const mergeStackState = (previousStacks, incomingStacks) => {
|
||||
const prevMap = new Map(previousStacks.map((stack) => [stack.Id, stack]));
|
||||
@@ -85,6 +105,149 @@ export default function Stacks() {
|
||||
});
|
||||
}, [stacks]);
|
||||
|
||||
const normalizedSearch = searchQuery.trim().toLowerCase();
|
||||
|
||||
const filteredStacks = useMemo(() => {
|
||||
return stacks.filter((stack) => {
|
||||
if (statusFilter === "current" && stack.updateStatus !== '✅') return false;
|
||||
if (statusFilter === "outdated" && stack.updateStatus === '✅') return false;
|
||||
if (showRedeployingOnly && !stack.redeploying) return false;
|
||||
|
||||
if (normalizedSearch) {
|
||||
const identifier = `${stack.Name ?? ""} ${stack.Id ?? ""}`.toLowerCase();
|
||||
if (!identifier.includes(normalizedSearch)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [stacks, statusFilter, normalizedSearch, showRedeployingOnly]);
|
||||
|
||||
const eligibleFilteredStacks = useMemo(
|
||||
() => filteredStacks.filter((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled),
|
||||
[filteredStacks]
|
||||
);
|
||||
|
||||
const stacksById = useMemo(() => {
|
||||
const map = new Map();
|
||||
stacks.forEach((stack) => {
|
||||
map.set(stack.Id, stack);
|
||||
});
|
||||
return map;
|
||||
}, [stacks]);
|
||||
|
||||
const filteredStackIdSet = useMemo(() => new Set(filteredStacks.map((stack) => stack.Id)), [filteredStacks]);
|
||||
|
||||
const paginatedStacks = useMemo(() => {
|
||||
if (perPage === 'all') {
|
||||
return filteredStacks;
|
||||
}
|
||||
const perPageNumber = Number(perPage);
|
||||
const start = (page - 1) * perPageNumber;
|
||||
return filteredStacks.slice(start, start + perPageNumber);
|
||||
}, [filteredStacks, perPage, page]);
|
||||
|
||||
const visiblePageStackIds = useMemo(() => new Set(paginatedStacks.map((stack) => stack.Id)), [paginatedStacks]);
|
||||
|
||||
const eligiblePageStacks = useMemo(
|
||||
() => paginatedStacks.filter((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled),
|
||||
[paginatedStacks]
|
||||
);
|
||||
|
||||
const selectionPreferenceRef = useRef({ action: 'keep', remember: false });
|
||||
const previousFiltersRef = useRef({ status: statusFilter, search: normalizedSearch, redeploying: showRedeployingOnly });
|
||||
const didMountRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
const storedValue = window.sessionStorage.getItem(SELECTION_PROMPT_STORAGE_KEY);
|
||||
if (!storedValue) return;
|
||||
const parsed = JSON.parse(storedValue);
|
||||
if (parsed && (parsed.action === 'keep' || parsed.action === 'clear')) {
|
||||
selectionPreferenceRef.current = { action: parsed.action, remember: true };
|
||||
setSelectionPreferenceStored(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('⚠️ Konnte gespeicherte Auswahl-Einstellung nicht laden:', err);
|
||||
selectionPreferenceRef.current = { action: 'keep', remember: false };
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const normalized = normalizedSearch;
|
||||
if (!didMountRef.current) {
|
||||
didMountRef.current = true;
|
||||
previousFiltersRef.current = { status: statusFilter, search: normalized, redeploying: showRedeployingOnly };
|
||||
return;
|
||||
}
|
||||
|
||||
const prev = previousFiltersRef.current;
|
||||
const filtersChanged = prev.status !== statusFilter || prev.search !== normalized || prev.redeploying !== showRedeployingOnly;
|
||||
|
||||
if (!filtersChanged) {
|
||||
if (statusFilter === 'all' && normalized.length === 0 && !showRedeployingOnly && selectionPromptVisible) {
|
||||
setSelectionPromptVisible(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
previousFiltersRef.current = { status: statusFilter, search: normalized, redeploying: showRedeployingOnly };
|
||||
|
||||
if (selectedStackIds.length === 0) {
|
||||
setSelectionPromptVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasFilters = statusFilter !== 'all' || normalized.length > 0 || showRedeployingOnly;
|
||||
if (!hasFilters) {
|
||||
setSelectionPromptVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const storedPreference = selectionPreferenceRef.current;
|
||||
if (storedPreference?.remember) {
|
||||
if (storedPreference.action === 'clear') {
|
||||
setSelectedStackIds([]);
|
||||
}
|
||||
setSelectionPromptVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setRememberSelectionChoice(Boolean(storedPreference?.remember));
|
||||
setSelectionPromptVisible(true);
|
||||
}, [statusFilter, normalizedSearch, selectedStackIds.length, selectionPromptVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedStackIds.length === 0 && selectionPromptVisible) {
|
||||
setSelectionPromptVisible(false);
|
||||
}
|
||||
}, [selectedStackIds.length, selectionPromptVisible]);
|
||||
|
||||
const hasActiveFilters = statusFilter !== "all" || normalizedSearch.length > 0 || showRedeployingOnly;
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [statusFilter, normalizedSearch, showRedeployingOnly]);
|
||||
|
||||
useEffect(() => {
|
||||
if (perPage === 'all') {
|
||||
if (page !== 1) setPage(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const perPageNumber = Number(perPage);
|
||||
const totalPagesCalc = Math.max(1, Math.ceil(filteredStacks.length / perPageNumber));
|
||||
if (page > totalPagesCalc) {
|
||||
setPage(totalPagesCalc);
|
||||
}
|
||||
}, [filteredStacks.length, perPage, page]);
|
||||
|
||||
const totalItems = filteredStacks.length;
|
||||
const perPageNumber = perPage === 'all' ? (totalItems || 1) : Number(perPage);
|
||||
const totalPages = perPage === 'all' ? 1 : Math.max(1, Math.ceil(totalItems / perPageNumber));
|
||||
const pageStart = totalItems === 0 ? 0 : (perPage === 'all' ? 1 : ((page - 1) * perPageNumber + 1));
|
||||
const pageEnd = perPage === 'all' ? totalItems : Math.min(totalItems, page * perPageNumber);
|
||||
|
||||
const toggleStackSelection = (stackId, disabled) => {
|
||||
if (disabled) return;
|
||||
setSelectedStackIds(prev =>
|
||||
@@ -94,6 +257,83 @@ export default function Stacks() {
|
||||
);
|
||||
};
|
||||
|
||||
const handlePerPageChange = (event) => {
|
||||
const value = event.target.value;
|
||||
if (!VALID_PER_PAGE_VALUES.has(value)) return;
|
||||
setPerPage(value);
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
const handlePrevPage = () => {
|
||||
setPage((prev) => Math.max(1, prev - 1));
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (perPage === 'all') return;
|
||||
setPage((prev) => {
|
||||
const perPageNumberLocal = Number(perPage);
|
||||
const totalPagesCalc = Math.max(1, Math.ceil(filteredStacks.length / perPageNumberLocal));
|
||||
return Math.min(totalPagesCalc, prev + 1);
|
||||
});
|
||||
};
|
||||
|
||||
const applySelectionPreference = (action) => {
|
||||
const remember = rememberSelectionChoice;
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
if (remember) {
|
||||
const payload = { action, remember: true };
|
||||
window.sessionStorage.setItem(SELECTION_PROMPT_STORAGE_KEY, JSON.stringify(payload));
|
||||
selectionPreferenceRef.current = payload;
|
||||
} else {
|
||||
window.sessionStorage.removeItem(SELECTION_PROMPT_STORAGE_KEY);
|
||||
selectionPreferenceRef.current = { action, remember: false };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('⚠️ Konnte Auswahl-Einstellung nicht speichern:', err);
|
||||
selectionPreferenceRef.current = { action, remember: false };
|
||||
}
|
||||
} else {
|
||||
selectionPreferenceRef.current = { action, remember: false };
|
||||
}
|
||||
|
||||
setSelectionPreferenceStored(remember);
|
||||
setRememberSelectionChoice(remember);
|
||||
|
||||
if (action === 'clear') {
|
||||
setSelectedStackIds([]);
|
||||
}
|
||||
|
||||
setSelectionPromptVisible(false);
|
||||
};
|
||||
|
||||
const clearStoredSelectionPreference = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
window.sessionStorage.removeItem(SELECTION_PROMPT_STORAGE_KEY);
|
||||
} catch (err) {
|
||||
console.error('⚠️ Konnte gespeicherte Auswahl-Einstellung nicht löschen:', err);
|
||||
}
|
||||
}
|
||||
|
||||
selectionPreferenceRef.current = { action: 'keep', remember: false };
|
||||
setSelectionPreferenceStored(false);
|
||||
setRememberSelectionChoice(false);
|
||||
|
||||
if (hasActiveFilters && selectedStackIds.length > 0) {
|
||||
setSelectionPromptVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedStackIds([]);
|
||||
setSelectionPromptVisible(false);
|
||||
};
|
||||
|
||||
const handleChipRemove = (stackId) => {
|
||||
setSelectedStackIds((prev) => prev.filter((id) => id !== stackId));
|
||||
};
|
||||
|
||||
const handleRedeploy = async (stackId) => {
|
||||
setSelectedStackIds((prev) => prev.filter((id) => id !== stackId));
|
||||
setStacks((prev) =>
|
||||
@@ -116,28 +356,31 @@ export default function Stacks() {
|
||||
};
|
||||
|
||||
const handleRedeployAll = async () => {
|
||||
const outdatedStacks = stacks.filter((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled);
|
||||
if (!outdatedStacks.length) return;
|
||||
if (!eligiblePageStacks.length) return;
|
||||
|
||||
const outdatedIds = new Set(outdatedStacks.map((stack) => stack.Id));
|
||||
const targetIds = new Set(eligiblePageStacks.map((stack) => stack.Id));
|
||||
|
||||
setStacks(prev =>
|
||||
prev.map(stack =>
|
||||
outdatedIds.has(stack.Id)
|
||||
targetIds.has(stack.Id)
|
||||
? { ...stack, redeploying: true }
|
||||
: stack
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
await axios.put("/api/stacks/redeploy-all");
|
||||
setSelectedStackIds((prev) => prev.filter((id) => !outdatedIds.has(id)));
|
||||
if (targetIds.size === eligibleFilteredStacks.length) {
|
||||
await axios.put("/api/stacks/redeploy-all");
|
||||
} else {
|
||||
await axios.put("/api/stacks/redeploy-selection", { stackIds: Array.from(targetIds) });
|
||||
}
|
||||
setSelectedStackIds((prev) => prev.filter((id) => !targetIds.has(id)));
|
||||
// Statusupdates kommen über Socket.IO
|
||||
} catch (err) {
|
||||
console.error("❌ Fehler beim Redeploy ALL:", err);
|
||||
setStacks(prev =>
|
||||
prev.map(stack =>
|
||||
outdatedIds.has(stack.Id)
|
||||
targetIds.has(stack.Id)
|
||||
? { ...stack, redeploying: false }
|
||||
: stack
|
||||
)
|
||||
@@ -185,17 +428,17 @@ export default function Stacks() {
|
||||
};
|
||||
|
||||
const hasSelection = selectedStackIds.length > 0;
|
||||
const hasOutdatedStacks = stacks.some((stack) => stack.updateStatus !== '✅' && !stack.redeployDisabled);
|
||||
const hasOutdatedStacks = eligiblePageStacks.length > 0;
|
||||
const bulkButtonLabel = hasSelection
|
||||
? `Redeploy Auswahl (${selectedStackIds.length})`
|
||||
: 'Redeploy All';
|
||||
|
||||
const bulkActionDisabled = hasSelection
|
||||
? selectedStackIds.length === 0 || selectedStackIds.every(id => {
|
||||
? selectionPromptVisible || selectedStackIds.length === 0 || selectedStackIds.every(id => {
|
||||
const targetStack = stacks.find(stack => stack.Id === id);
|
||||
return !targetStack || targetStack.redeploying || targetStack.updateStatus === '✅' || targetStack.redeployDisabled;
|
||||
})
|
||||
: !hasOutdatedStacks || stacks.every(stack => stack.updateStatus !== '✅' || stack.redeploying || stack.redeployDisabled);
|
||||
: !hasOutdatedStacks || eligiblePageStacks.every(stack => stack.redeploying);
|
||||
|
||||
const handleBulkRedeploy = () => {
|
||||
if (hasSelection) {
|
||||
@@ -210,18 +453,196 @@ export default function Stacks() {
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex justify-end mb-4">
|
||||
<button
|
||||
onClick={handleBulkRedeploy}
|
||||
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'}`}
|
||||
>
|
||||
{bulkButtonLabel}
|
||||
</button>
|
||||
<div className="mb-6 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-end md:gap-6">
|
||||
<div>
|
||||
<span className="mb-2 block text-sm font-medium text-gray-300">Status</span>
|
||||
<div className="inline-flex overflow-hidden rounded-lg border border-gray-700 bg-gray-900">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStatusFilter('all')}
|
||||
className={`px-4 py-2 text-sm font-medium transition ${statusFilter === 'all' ? 'bg-purple-600 text-white' : 'text-gray-300 hover:text-white'} border-r border-gray-700 last:border-r-0`}
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStatusFilter('current')}
|
||||
className={`px-4 py-2 text-sm font-medium transition ${statusFilter === 'current' ? 'bg-purple-600 text-white' : 'text-gray-300 hover:text-white'} border-r border-gray-700 last:border-r-0`}
|
||||
>
|
||||
Aktuell
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStatusFilter('outdated')}
|
||||
className={`px-4 py-2 text-sm font-medium transition ${statusFilter === 'outdated' ? 'bg-purple-600 text-white' : 'text-gray-300 hover:text-white'}`}
|
||||
>
|
||||
Veraltet
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:w-64">
|
||||
<label htmlFor="stack-search" className="mb-2 block text-sm font-medium text-gray-300">Suche</label>
|
||||
<input
|
||||
id="stack-search"
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
placeholder="Name oder ID"
|
||||
className="w-full rounded-lg border border-gray-700 bg-gray-900 px-3 py-2 text-sm text-gray-100 placeholder-gray-500 focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500"
|
||||
/>
|
||||
<label className="mt-2 flex items-center gap-2 text-sm text-gray-300">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showRedeployingOnly}
|
||||
onChange={(event) => setShowRedeployingOnly(event.target.checked)}
|
||||
className="h-4 w-4 rounded border border-purple-400 bg-gray-900 text-purple-400 focus:ring-purple-400"
|
||||
/>
|
||||
<span>Nur Redeploy läuft</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-2 md:items-end">
|
||||
{selectionPreferenceStored && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearStoredSelectionPreference}
|
||||
className="text-xs font-medium text-amber-300 underline underline-offset-2 transition hover:text-amber-100"
|
||||
>
|
||||
Gespeicherte Entscheidung löschen
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleBulkRedeploy}
|
||||
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'}`}
|
||||
>
|
||||
{bulkButtonLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{stacks.map(stack => {
|
||||
<div className="mb-6 flex flex-col gap-3 text-sm text-gray-300 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
{totalItems === 0
|
||||
? 'Keine Stacks verfügbar'
|
||||
: `Zeige ${pageStart}-${pageEnd} von ${totalItems} Stacks`}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<label className="flex items-center gap-2">
|
||||
<span>Einträge pro Seite:</span>
|
||||
<select
|
||||
value={perPage}
|
||||
onChange={handlePerPageChange}
|
||||
className="rounded-md border border-gray-700 bg-gray-900 px-2 py-1 text-sm text-gray-100 focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500"
|
||||
>
|
||||
{PER_PAGE_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePrevPage}
|
||||
disabled={page <= 1 || totalItems === 0 || perPage === 'all'}
|
||||
className="rounded-md border border-gray-700 px-2 py-1 text-xs font-medium uppercase tracking-wide text-gray-200 transition hover:bg-gray-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
<span>{`Seite ${Math.min(page, totalPages)} / ${totalPages}`}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNextPage}
|
||||
disabled={perPage === 'all' || totalItems === 0 || page >= totalPages}
|
||||
className="rounded-md border border-gray-700 px-2 py-1 text-xs font-medium uppercase tracking-wide text-gray-200 transition hover:bg-gray-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Weiter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedStackIds.length > 0 && (
|
||||
<div className="mb-4 flex flex-col gap-2 text-sm text-gray-300 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium text-gray-200">Auswahl:</span>
|
||||
{selectedStackIds.map((id) => {
|
||||
const stack = stacksById.get(id);
|
||||
const name = stack?.Name || `Stack ${id}`;
|
||||
const isFiltered = filteredStackIdSet.has(id);
|
||||
const isVisibleOnPage = visiblePageStackIds.has(id);
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
onClick={() => handleChipRemove(id)}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-purple-500/60 bg-purple-500/10 px-3 py-1 text-purple-100 transition hover:border-purple-400 hover:bg-purple-500/20"
|
||||
>
|
||||
<span>{name}</span>
|
||||
{!isFiltered && (
|
||||
<span className="text-xs uppercase tracking-wide text-amber-300">Ausgefiltert</span>
|
||||
)}
|
||||
{isFiltered && !isVisibleOnPage && (
|
||||
<span className="text-xs uppercase tracking-wide text-blue-300">Andere Seite</span>
|
||||
)}
|
||||
<span className="text-xs font-semibold text-purple-200">x</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearSelection}
|
||||
className="self-start text-xs font-medium text-gray-400 underline underline-offset-2 transition hover:text-gray-200 md:self-auto"
|
||||
>
|
||||
Auswahl aufheben
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectionPromptVisible && (
|
||||
<div className="mb-6 flex flex-col gap-3 rounded-lg border border-amber-500/50 bg-amber-900/40 px-4 py-3 text-amber-100 md:flex-row md:items-center md:justify-between">
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
{selectedStackIds.length === 1
|
||||
? '1 Stack ist weiterhin ausgewählt.'
|
||||
: `${selectedStackIds.length} Stacks sind weiterhin ausgewählt.`}
|
||||
{' '}Soll die Auswahl entfernt werden?
|
||||
</p>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={rememberSelectionChoice}
|
||||
onChange={(event) => setRememberSelectionChoice(event.target.checked)}
|
||||
className="h-4 w-4 rounded border border-amber-400 bg-amber-950 text-amber-400 focus:ring-amber-300"
|
||||
/>
|
||||
<span>Einstellung merken (bis Browser geschlossen wird)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applySelectionPreference('clear')}
|
||||
className="rounded-md bg-amber-500 px-4 py-2 text-sm font-medium text-amber-950 transition hover:bg-amber-400"
|
||||
>
|
||||
Auswahl entfernen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => applySelectionPreference('keep')}
|
||||
className="rounded-md border border-amber-400 px-4 py-2 text-sm font-medium transition hover:bg-amber-400/20"
|
||||
>
|
||||
Auswahl behalten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
{paginatedStacks.map(stack => {
|
||||
const isRedeploying = stack.redeploying;
|
||||
const isSelected = selectedStackIds.includes(stack.Id);
|
||||
const isCurrent = stack.updateStatus === '✅';
|
||||
@@ -289,7 +710,11 @@ export default function Stacks() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{stacks.length === 0 && <p className="text-gray-400">Keine Stacks gefunden.</p>}
|
||||
{filteredStacks.length === 0 && (
|
||||
<p className="text-gray-400">
|
||||
{hasActiveFilters ? 'Keine Stacks für die gesetzten Filter.' : 'Keine Stacks gefunden.'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user