This commit is contained in:
root
2025-10-21 06:30:47 +00:00
parent 2c3379026b
commit 98a6df5081
8 changed files with 255 additions and 116 deletions
+8 -8
View File
@@ -4,20 +4,20 @@ const ToastContext = createContext(null);
const VARIANT_STYLES = {
success: {
container: "border-green-500/80 bg-green-950/90 text-green-100",
accent: "bg-green-400"
container: "border-mossGreen-500/80 bg-mossGreen-900/90 text-white",
accent: "bg-mossGreen-400"
},
warning: {
container: "border-yellow-500/80 bg-yellow-950/90 text-yellow-100",
accent: "bg-yellow-400"
container: "border-warmAmberGlow-500/80 bg-warmAmberGlow-900/90 text-white",
accent: "bg-warmAmberGlow-400"
},
error: {
container: "border-red-500/80 bg-red-950/90 text-red-100",
accent: "bg-red-400"
container: "border-sunsetCoral-500/80 bg-sunsetCoral-900/90 text-white",
accent: "bg-sunsetCoral-400"
},
info: {
container: "border-sky-500/60 bg-sky-950/90 text-sky-100",
accent: "bg-sky-400"
container: "border-arcticBlue-500 bg-arcticBlue-900/90 text-white",
accent: "bg-arcticBlue-400"
}
};
+65 -44
View File
@@ -18,7 +18,7 @@ import {
useSelect
} from "@material-tailwind/react";
const StickyOption = React.forwardRef(({ value, onValueSelect, onClick, onKeyDown, ...props }, ref) => {
const StickyOption = React.forwardRef(({ value, onClick, onKeyDown, ...props }, ref) => {
const { setOpen } = useSelect();
const reopen = useCallback(() => {
@@ -31,20 +31,13 @@ const StickyOption = React.forwardRef(({ value, onValueSelect, onClick, onKeyDow
});
}, [setOpen]);
const commitSelection = useCallback(() => {
if (typeof onValueSelect === "function") {
onValueSelect(value);
}
}, [onValueSelect, value]);
const handleClick = useCallback((event) => {
if (typeof onClick === "function") {
onClick(event);
}
commitSelection();
reopen();
}, [onClick, commitSelection, reopen]);
}, [onClick, reopen]);
const handleKeyDown = useCallback((event) => {
if (typeof onKeyDown === "function") {
@@ -52,10 +45,9 @@ const StickyOption = React.forwardRef(({ value, onValueSelect, onClick, onKeyDow
}
if (event.key === "Enter" || event.key === " ") {
commitSelection();
reopen();
}
}, [onKeyDown, commitSelection, reopen]);
}, [onKeyDown, reopen]);
return (
<Option
@@ -71,10 +63,10 @@ const StickyOption = React.forwardRef(({ value, onValueSelect, onClick, onKeyDow
StickyOption.displayName = "StickyOption";
const STATUS_COLORS = {
success: "text-green-400",
warning: "text-yellow-400",
error: "text-red-400",
started: "text-blue-300"
success: "text-mossGreen-500",
warning: "text-warmAmberGlow-500",
error: "text-sunsetCoral-500",
started: "text-arcticBlue-500"
};
const formatTimestamp = (value) => {
@@ -419,7 +411,7 @@ export function Logs() {
}
}, [filtersReady, currentFilters, perPage, page]);
const handleMultiSelectChange = (setter) => (valueOrEvent) => {
const handleMultiSelectChange = useCallback((setter) => (valueOrEvent) => {
if (typeof valueOrEvent === "string") {
const value = valueOrEvent;
if (value === "") {
@@ -453,7 +445,12 @@ export function Logs() {
setter(values);
}
setPage(1);
};
}, [setPage]);
const selectStacks = useMemo(() => handleMultiSelectChange(setSelectedStacks), [handleMultiSelectChange, setSelectedStacks]);
const selectStatuses = useMemo(() => handleMultiSelectChange(setSelectedStatuses), [handleMultiSelectChange, setSelectedStatuses]);
const selectRedeployTypes = useMemo(() => handleMultiSelectChange(setSelectedRedeployTypes), [handleMultiSelectChange, setSelectedRedeployTypes]);
const selectEndpoints = useMemo(() => handleMultiSelectChange(setSelectedEndpoints), [handleMultiSelectChange, setSelectedEndpoints]);
const removeFilterValue = (setter, value) => {
setter((prev) => prev.filter((entry) => entry !== value));
@@ -631,7 +628,7 @@ export function Logs() {
return (
<div className="mt-12 mb-8 flex flex-col gap-12">
<Card>
<CardHeader variant="gradient" color="gray" className="p-6 pt-2 pb-2">
<CardHeader variant="gradient" color="gray" className="p-4 pt-2 pb-2">
<Typography variant="h6" color="white">
<button
onClick={handleToggleFilters}
@@ -679,7 +676,7 @@ export function Logs() {
<Select
multiple
onChange={noop}
className="text-gray-500"
className="text-stormGrey-500"
variant="static"
dismiss={{ itemPress: false }}
label="Stacks"
@@ -688,16 +685,22 @@ export function Logs() {
<StickyOption
key={value}
value={value}
onValueSelect={handleMultiSelectChange(setSelectedStacks)}
onClick={() => selectStacks(value)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
selectStacks(value);
}
}}
className={`text-black-600 ${value === ALL_OPTION_VALUE ? 'font-semibold text-black-800' : ''}`}
>
{label}
</StickyOption>
))}
</Select>
<div className="mt-2 mb-2 min-h-[1.5rem] text-xs text-gray-400">
<div className="mt-2 mb-2 min-h-[1.5rem] text-xs text-stormGrey-400">
{selectedStacks.length === 0 ? (
<span className="rounded-full bg-gray-700/60 px-2 py-0.5 text-gray-300 ">
<span className="rounded-full bg-stormGrey-700/60 px-2 py-0.5 text-white ">
Alle Stacks
</span>
) : (
@@ -707,7 +710,7 @@ export function Logs() {
key={stackId}
type="button"
onClick={() => removeFilterValue(setSelectedStacks, stackId)}
className="rounded-full bg-purple-500/80 px-2 py-0.5 text-white transition hover:bg-purple-500/90 focus:outline-none focus:ring-2 focus:ring-purple-300 cursor-pointer"
className="rounded-full bg-lavenderSmoke-600/80 px-2 py-0.5 text-white transition hover:bg-lavenderSmoke-600/90 focus:outline-none focus:ring-2 focus:ring-lavenderSmoke-400 cursor-pointer"
title="Filter entfernen"
>
{stackLabelMap.get(stackId) ?? `Stack ${stackId}`}
@@ -721,7 +724,7 @@ export function Logs() {
<Select
multiple
onChange={noop}
className="text-gray-500"
className="text-stormGrey-500"
variant="static"
dismiss={{ itemPress: false }}
label="Status"
@@ -730,16 +733,22 @@ export function Logs() {
<StickyOption
key={value}
value={value}
onValueSelect={handleMultiSelectChange(setSelectedStatuses)}
onClick={() => selectStatuses(value)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
selectStatuses(value);
}
}}
className={`text-black-600 ${value === ALL_OPTION_VALUE ? 'font-semibold text-black-800' : ''}`}
>
{label}
</StickyOption>
))}
</Select>
<div className="mt-2 mb-2 min-h-[1.5rem] text-xs text-gray-400">
<div className="mt-2 mb-2 min-h-[1.5rem] text-xs text-stormGrey-400">
{selectedStatuses.length === 0 ? (
<span className="rounded-full bg-gray-700/60 px-2 py-0.5 text-gray-300">
<span className="rounded-full bg-stormGrey-700/60 px-2 py-0.5 text-white">
Alle Status
</span>
) : (
@@ -749,7 +758,7 @@ export function Logs() {
key={status}
type="button"
onClick={() => removeFilterValue(setSelectedStatuses, status)}
className="rounded-full bg-brown-500/80 px-2 py-0.5 text-white transition hover:bg-brown-500/90 focus:outline-none focus:ring-2 focus:ring-brown-300 cursor-pointer"
className="rounded-full bg-copperRust-600/80 px-2 py-0.5 text-white transition hover:bg-copperRust-600/90 focus:outline-none focus:ring-2 focus:ring-copperRust-400 cursor-pointer"
title="Filter entfernen"
>
{status}
@@ -763,7 +772,7 @@ export function Logs() {
<Select
multiple
onChange={noop}
className="text-gray-500"
className="text-stormGrey-500"
variant="static"
dismiss={{ itemPress: false }}
label="Redeploy-Typ"
@@ -772,7 +781,13 @@ export function Logs() {
<StickyOption
key={value}
value={value}
onValueSelect={handleMultiSelectChange(setSelectedRedeployTypes)}
onClick={() => selectRedeployTypes(value)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
selectRedeployTypes(value);
}
}}
className={`text-black-600 ${value === ALL_OPTION_VALUE ? 'font-semibold text-black-800' : ''}`}
>
{label}
@@ -780,9 +795,9 @@ export function Logs() {
))}
</Select>
<div className="mt-2 mb-2 min-h-[1.5rem] text-xs text-gray-400">
<div className="mt-2 mb-2 min-h-[1.5rem] text-xs text-stormGrey-400">
{selectedRedeployTypes.length === 0 ? (
<span className="rounded-full bg-gray-700/60 px-2 py-0.5 text-gray-300">
<span className="rounded-full bg-stormGrey-700/60 px-2 py-0.5 text-white">
Alle Typen
</span>
) : (
@@ -792,7 +807,7 @@ export function Logs() {
key={type}
type="button"
onClick={() => removeFilterValue(setSelectedRedeployTypes, type)}
className="rounded-full bg-brown-500/80 px-2 py-0.5 text-white transition hover:bg-brown-500/90 focus:outline-none focus:ring-2 focus:ring-brown-300 cursor-pointer"
className="rounded-full bg-citrusPunch-600/80 px-2 py-0.5 text-white transition hover:bg-citrusPunch-600/90 focus:outline-none focus:ring-2 focus:ring-citrusPunch-400 cursor-pointer"
title="Filter entfernen"
>
{REDEPLOY_TYPE_LABELS[type] ?? type}
@@ -806,7 +821,7 @@ export function Logs() {
<Select
multiple
onChange={noop}
className="text-gray-500"
className="text-stormGrey-500"
variant="static"
dismiss={{ itemPress: false }}
label="Endpoints"
@@ -815,16 +830,22 @@ export function Logs() {
<StickyOption
key={value}
value={value}
onValueSelect={handleMultiSelectChange(setSelectedEndpoints)}
onClick={() => selectEndpoints(value)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
selectEndpoints(value);
}
}}
className={`text-black-600 ${value === ALL_OPTION_VALUE ? 'font-semibold text-black-800' : ''}`}
>
{label}
</StickyOption>
))}
</Select>
<div className="mt-2 mb-2 min-h-[1.5rem] text-xs text-gray-400">
<div className="mt-2 mb-2 min-h-[1.5rem] text-xs text-stormGrey-400">
{selectedEndpoints.length === 0 ? (
<span className="rounded-full bg-gray-700/60 px-2 py-0.5 text-gray-300">
<span className="rounded-full bg-stormGrey-700/60 px-2 py-0.5 text-white">
Alle Endpoints
</span>
) : (
@@ -834,7 +855,7 @@ export function Logs() {
key={endpoint}
type="button"
onClick={() => removeFilterValue(setSelectedEndpoints, endpoint)}
className="rounded-full bg-teal-500/80 px-2 py-0.5 text-white transition hover:bg-teal-500/90 focus:outline-none focus:ring-2 focus:ring-brown-300 cursor-pointer"
className="rounded-full bg-emeraldMist-500/80 px-2 py-0.5 text-white transition hover:bg-emeraldMist-500/90 focus:outline-none focus:ring-2 focus:ring-emeraldMist-400 cursor-pointer"
title="Filter entfernen"
>
Endpoint {endpoint}
@@ -903,7 +924,7 @@ export function Logs() {
</CardBody>
</Card>
<Card>
<CardHeader variant="gradient" color="gray" className="mb-8 p-6">
<CardHeader variant="gradient" color="gray" className="mb-5 p-4">
<Typography
variant="h6"
color="white"
@@ -918,7 +939,7 @@ export function Logs() {
<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"
className="rounded-md border border-gray-700 bg-gray-900 px-2 py-1 text-sm text-gray-100 focus:border-lavenderSmoke-500 focus:outline-none focus:ring-1 focus:ring-lavenderSmoke-500"
>
{PER_PAGE_OPTIONS.map(({ value, label }) => (
<option key={value} value={value}>
@@ -936,11 +957,11 @@ export function Logs() {
{["Zeitpunkt", "Stack", "Art", "Status", "Nachricht", "Endpoint", "Aktionen"].map((el) => (
<th
key={el}
className="border-b border-blue-gray-50 py-3 px-5 text-left"
className="border-b border-stormGrey-50 py-3 px-5 text-left"
>
<Typography
variant="small"
className="text-[11px] font-bold uppercase text-blue-gray-400"
className="text-[11px] font-bold uppercase text-stormGrey-400"
>
{el}
</Typography>
@@ -962,7 +983,7 @@ export function Logs() {
<td className={className}>
<Typography
variant="small"
className="mb-1 block text-xs font-medium text-blue-gray-600">
className="mb-1 block text-xs font-medium text-stormGrey-600">
{formatTimestamp(log.timestamp)}
</Typography>
</td>
@@ -973,7 +994,7 @@ export function Logs() {
variant="small"
>
{showStackId && (
<span className="text-xs text-gray-400">ID: {log.stackId}</span>
<span className="text-xs text-stormGrey-400">ID: {log.stackId}</span>
)}
</Typography>
</div>
+177 -59
View File
@@ -20,10 +20,10 @@ const UPDATE_STATUS_LABELS = {
};
const UPDATE_STATUS_STYLES = {
idle: "bg-gray-700 text-gray-200",
running: "bg-sky-600 text-white",
success: "bg-green-600 text-white",
error: "bg-red-600 text-white"
idle: "bg-stormGrey-700 text-gray-200",
running: "bg-arcticBlue-600 text-white",
success: "bg-mossGreen-600 text-white",
error: "bg-sunsetCoral-600 text-white"
};
const UPDATE_STAGE_LABELS = {
@@ -36,13 +36,13 @@ const UPDATE_STAGE_LABELS = {
};
const LOG_LEVEL_STYLES = {
info: "text-blue-700",
success: "text-green-700",
warning: "text-amber-700",
error: "text-red-700",
stdout: "text-gray-700",
stderr: "text-orange-700",
debug: "text-cyan-700"
info: "text-arcticBlue-700",
success: "text-mossGreen-700",
warning: "text-warmAmberGlow-700",
error: "text-sunsetCoral-700",
stdout: "text-stormGrey-700",
stderr: "text-warmAmberGlow-700",
debug: "text-lavenderSmoke-700"
};
const formatLogTimestamp = (value) => {
@@ -128,6 +128,7 @@ export function Maintenance() {
const maintenanceActive = Boolean(maintenanceMeta?.active);
const maintenanceMessage = maintenanceMeta?.message;
const maintenanceExtraType = maintenanceMeta?.extra?.type;
const updateRunning = Boolean(updateState?.running);
const [scriptDraft, setScriptDraft] = useState("");
@@ -432,10 +433,16 @@ export function Maintenance() {
setMaintenanceToggleLoading(true);
try {
await setMaintenanceMode({
active: nextActive,
message: nextActive ? (maintenanceMessage ?? undefined) : null
});
const shouldPreserveMessage = nextActive && Boolean(maintenanceExtraType);
const payload = { active: nextActive };
if (shouldPreserveMessage && maintenanceMessage) {
payload.message = maintenanceMessage;
} else {
payload.message = null;
}
await setMaintenanceMode(payload);
showToast({
variant: nextActive ? "info" : "success",
title: nextActive ? "Wartungsmodus aktiviert" : "Wartungsmodus deaktiviert",
@@ -449,7 +456,7 @@ export function Maintenance() {
} finally {
setMaintenanceToggleLoading(false);
}
}, [maintenanceLoading, maintenanceToggleLoading, maintenanceActive, updateRunning, setMaintenanceMode, maintenanceMessage, showToast]);
}, [maintenanceLoading, maintenanceToggleLoading, maintenanceActive, updateRunning, setMaintenanceMode, maintenanceMessage, maintenanceExtraType, showToast]);
const handleTriggerUpdate = useCallback(async () => {
if (maintenanceActive || updateRunning) {
@@ -589,8 +596,41 @@ export function Maintenance() {
</div>
</div>
)}
<Card>
<CardHeader variant="gradient" color="gray" className="mb-8 p-6">
<CardHeader variant="gradient" color="gray" className="mb-5 p-4">
<Typography
variant="h6"
color="white"
className="flex items-center justify-between"
>
<span>Wartungsmodus</span>
</Typography>
</CardHeader>
<CardBody className="flex flex-col gap-4 p-4">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<p className="text-sm antialiased font-sans text-sm font-light leading-normal text-inherit">
Schaltet StackPulse für Benutzer in den Wartungsmodus.
</p>
</div>
<Switch
checked={maintenanceActive}
disabled={maintenanceToggleLoading || maintenanceLoading || updateRunning}
onChange={(event) => handleMaintenanceToggle(event.target.checked)}
ripple={false}
color="amber"
/>
</div>
{maintenanceToggleLoading && (
<p className="text-xs text-stormGrey-500">Wartungsmodus wird aktualisiert</p>
)}
</CardBody>
</Card>
<Card>
<CardHeader variant="gradient" color="gray" className="mb-5 p-4">
<Typography
variant="h6"
color="white"
@@ -616,7 +656,7 @@ export function Maintenance() {
: "Portainer ist auf dem aktuellen Stand.")}
</p>
{portainerUpdatedAt && !portainerLoading && (
<p className="mt-1 text-xs text-gray-500 block antialiased font-sans">
<p className="mt-1 text-xs text-stormGrey-500 block antialiased font-sans">
Stand: {portainerUpdatedAt.toLocaleString("de-DE", {
hour: "2-digit",
minute: "2-digit",
@@ -665,23 +705,23 @@ export function Maintenance() {
</div>
{portainerStatus.edition && (
<div className="flex items-center justify-between">
<span className="text-gray-400">Edition</span>
<span className="text-stormGrey-400">Edition</span>
<span className="font-medium">{portainerStatus.edition}</span>
</div>
)}
{portainerStatus.build && (
<div className="flex items-center justify-between">
<span className="text-gray-400">Build</span>
<span className="text-stormGrey-400">Build</span>
<span className="font-medium">{portainerStatus.build}</span>
</div>
)}
{portainerStatus.errors?.latestVersion && (
<div className="rounded-md mt-8 border border-red-500/40 bg-red-900/40 px-3 py-2 text-xs">
<div className="rounded-md mt-8 border bg-sunsetCoral-600 text-white px-3 py-2 text-xs">
Neueste Version konnte nicht ermittelt werden: {portainerStatus.errors.latestVersion}
</div>
)}
{portainerStatus.errors?.container && (
<div className="rounded-md mt-8 border border-red-500/40 bg-red-900/40 px-3 py-2 text-xs">
<div className="rounded-md mt-8 border bg-sunsetCoral-600 text-white px-3 py-2 text-xs">
Container-Details konnten nicht ermittelt werden: {portainerStatus.errors.container}
</div>
)}
@@ -870,7 +910,7 @@ export function Maintenance() {
</Button>
</div>
{sshTestResult && (
<p className={`mt-3 text-xs ${sshTestResult.success ? 'text-green-800' : 'text-red-800'}`}>
<p className={`mt-3 text-xs ${sshTestResult.success ? 'text-mossGreen-600' : 'text-sunsetCoral-500'}`}>
{sshTestResult.success ? 'SSH-Verbindung erfolgreich.' : `SSH-Verbindung fehlgeschlagen: ${sshTestResult.error}`}
</p>
)}
@@ -905,7 +945,7 @@ export function Maintenance() {
</div>
{scriptConfig?.customUpdatedAt && (
<p className="mt-2 text-xs text-gray-500">
<p className="mt-2 text-xs text-stormGrey-500">
Zuletzt geändert: {formatCreatedAt(scriptConfig.customUpdatedAt)}
</p>
)}
@@ -940,29 +980,29 @@ export function Maintenance() {
<span>{updateFinishedAt}</span>
</div>
{updateStatusMessage && (
<div className="rounded-md border border-green-700 bg-green-800 px-3 py-2 text-xs text-white">
<div className="rounded-md border border-mossGreen-700 bg-mossGreen-800 px-3 py-2 text-xs text-white">
{updateStatusMessage}
</div>
)}
{updateState?.error && (
<div className="rounded-md border border-red-500/60 bg-red-900/40 px-3 py-2 text-xs text-red-200">
<div className="rounded-md border border-sunsetCoral-500/60 bg-sunsetCoral-900/40 px-3 py-2 text-xs text-white">
{updateState.error}
</div>
)}
{updateActionError && (
<div className="rounded-md border border-red-500/60 bg-red-900/40 px-3 py-2 text-xs text-red-200">
<div className="rounded-md border border-sunsetCoral-500/60 bg-sunsetCoral-900/40 px-3 py-2 text-xs text-white">
{updateActionError}
</div>
)}
</div>
<div className="mt-3 h-40 overflow-y-auto rounded-md border border-gray-700/20 bg-gray-950/70 p-3 font-mono text-[11px] leading-relaxed">
{updateLogs.length === 0 ? (
<p className="text-gray-500">Noch keine Protokolle vorhanden.</p>
<p className="text-stormGrey-500">Noch keine Protokolle vorhanden.</p>
) : (
updateLogs.map((entry, index) => (
<div key={`${entry.timestamp}-${index}`} className="mb-2 last:mb-0">
<span className="text-gray-500">[{formatLogTimestamp(entry.timestamp)}]</span>{" "}
<span className={(LOG_LEVEL_STYLES[entry.level] ?? "text-gray-300") + " whitespace-normal break-words"} style={{ overflowWrap: "anywhere", wordBreak: "normal" }}>
<span className={(LOG_LEVEL_STYLES[entry.level] ?? "text-stormGrey-300") + " whitespace-normal break-words"} style={{ overflowWrap: "anywhere", wordBreak: "normal" }}>
{entry.message}
</span>
</div>
@@ -984,46 +1024,124 @@ export function Maintenance() {
</CardBody>
</Card>
<Card>
<CardHeader
color="transparent"
floated={false}
shadow={false}
className="m-0 p-4 block antialiased font-sans"
>
<Typography variant="h5" color="blue-gray" className="block antialiased font-sans">
Wartungsmodus
<CardHeader variant="gradient" color="gray" className="mb-5 p-4">
<Typography
variant="h6"
color="white"
className="flex items-center justify-between"
>
<span>Doppelte Stacks</span>
</Typography>
</CardHeader>
<CardBody className="flex flex-col gap-4 p-4">
<div className="rounded-lg border border-warmAmberGlow-600 bg-warmAmberGlow-700 px-4 py-3 text-sm text-white">
Das Entfernen von deppelten Stacks ist noch nicht getestet. Bitte nicht in Produktivumgebungen einsetzen.
</div>
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<p className="text-sm text-blue-gray-200">
Schaltet StackPulse für Benutzer in den Wartungsmodus.
<p className="text-sm antialiased font-sans text-sm font-light leading-normal text-inherit">
{isDuplicatesDisabled
? "Wartungsmodus aktiv Duplikat-Verwaltung ist vorübergehend deaktiviert."
: duplicatesLoading
? "Analyse läuft…"
: totals.groups === 0
? "Keine Duplikate gefunden"
: `${totals.groups} Stack-Namen mit insgesamt ${totals.duplicateCount} Duplikaten gefunden`}
</p>
{maintenanceMessage ? (<p className="text-xs text-gray-500">Hinweis: {maintenanceMessage}</p>) : null}
<p className="text-xs text-gray-500">
{maintenanceActive
? `Aktiv seit ${maintenanceActivatedAt ?? "-"}.`
: `Zuletzt geändert: ${maintenanceUpdatedAt ?? "-"}.`}
</p>
{updateRunning && (
<p className="text-xs text-amber-600">Portainer-Update läuft Umschalten vorübergehend gesperrt.</p>
{duplicatesUpdatedAt && !duplicatesLoading && !isDuplicatesDisabled && (
<p className="mt-1 text-xs text-gray-500">
Stand: {duplicatesUpdatedAt.toLocaleString("de-DE", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
})}
</p>
)}
<p className="text-xs text-gray-500">
Status: {maintenanceActive ? "Aktiv" : "Inaktiv"}
</p>
</div>
<Switch
checked={maintenanceActive}
disabled={maintenanceToggleLoading || maintenanceLoading || updateRunning}
onChange={(event) => handleMaintenanceToggle(event.target.checked)}
ripple={false}
color="amber"
/>
<Button
onClick={() => fetchDuplicates({ silent: false })}
disabled={isDuplicatesDisabled || duplicatesLoading || duplicatesRefreshing || activeCleanupId !== null}
className=""
>
Aktualisieren
</Button>
{duplicatesError && !isDuplicatesDisabled && (
<div className="rounded-lg border border-sunsetCoral-500/60 bg-sunsetCoral-900/40 px-4 py-3 text-sm text-white">
{duplicatesError}
</div>
)}
</div>
{maintenanceToggleLoading && (
<p className="text-xs text-gray-500">Wartungsmodus wird aktualisiert</p>
{duplicatesLoading ? (
<div className="flex flex-col gap-4 p-4">
Daten werden geladen
</div>
) : totals.groups === 0 || isDuplicatesDisabled ? (
<div className="rounded-xl border-mossGreen-500/80 bg-mossGreen-900/90 text-mossGreen-100 p-8 text-center text-sm text-white">
{isDuplicatesDisabled
? "Wartungsmodus aktiv Duplikat-Verwaltung ist vorübergehend deaktiviert."
: "Es wurden keine doppelten Stacks gefunden."}
</div>
) : (
<div className="space-y-5">
{duplicates.map((entry) => {
const canonicalId = entry?.canonical?.Id;
const duplicatesForEntry = entry?.duplicates || [];
const isProcessing = activeCleanupId === String(canonicalId);
return (
<div
key={canonicalId || entry.name}
className="rounded-xl border border-gray-700 bg-gray-800/70 p-6 shadow"
>
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-white">{entry.name}</h3>
<span className="rounded-full bg-amber-500/20 px-3 py-0.5 text-xs font-medium text-amber-200">
{duplicatesForEntry.length} Duplikat{duplicatesForEntry.length === 1 ? "" : "e"}
</span>
</div>
<p className="text-sm text-gray-300">
Behaltener Stack: ID {canonicalId} (Endpoint {entry?.canonical?.EndpointId ?? "-"})
</p>
<p className="text-xs text-gray-500">
Typ: {resolveStackType(entry?.canonical?.Type)} Erstellt: {formatCreatedAt(entry?.canonical?.Created)}
</p>
</div>
<button
type="button"
onClick={() => handleCleanup(entry)}
disabled={isDuplicatesDisabled || isProcessing || duplicatesRefreshing || duplicatesLoading}
className="self-start rounded-lg bg-sunsetCoral-500 px-4 py-2 text-sm font-semibold text-white transition hover:bg-sunsetCoral-600 disabled:opacity-50"
>
{isProcessing ? "Bereinigung läuft…" : `Bereinigen (${duplicatesForEntry.length})`}
</button>
</div>
<div className="mt-5 grid gap-3">
{duplicatesForEntry.map((duplicate) => (
<div
key={duplicate.Id}
className="rounded-lg border border-sunsetCoral-500/40 bg-sunsetCoral-900/20 p-4 text-sm text-white"
>
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="font-semibold text-white">ID: {duplicate.Id}</span>
<span>Endpoint: {duplicate.EndpointId ?? "-"}</span>
<span>Typ: {resolveStackType(duplicate.Type)}</span>
<span>Erstellt: {formatCreatedAt(duplicate.Created)}</span>
</div>
</div>
))}
</div>
</div>
);
})}
</div>
)}
</CardBody>
</Card>
+1 -1
View File
@@ -10,7 +10,7 @@ module.exports = withMT({
green: colors.green,
slate: colors.slate,
orange: colors.orange, // falls du z.B. text-orange-300 brauchst
auroraTeal: {50:'#f0fafb',100:'#e0f5f7',200:'#c3ecef',300:'#9de0e4',400:'#6ad3d8',500:'#38c6cc',600:'#2aa9b0',700:'#1f8d94',800:'#166e76',900:'#0c4f58'},
auroraTeal: {50:'#f0fafb',100:'#e0f5f7',200:'#c3ecef',300:'#9de0e4',400:'#6ad3d8',500:'#38c6cc',600:'#2aa9b0',700:'#1f8d94',800:'#166e76',900:'#0c4f58'},
sunsetCoral: {50:'#fff6f5',100:'#ffece9',200:'#ffd7d3',300:'#ffb8b5',400:'#ff9189',500:'#ff6a5d',600:'#e65048',700:'#b93d38',800:'#8a2c28',900:'#5c1c1a'},
lavenderSmoke: {50:'#faf8fc',100:'#f5f1f9',200:'#ece2f3',300:'#dac4e6',400:'#b88acf',500:'#9670b8',600:'#7a5894',700:'#5d4373',800:'#412f52',900:'#291e38'},
mossGreen: {50:'#f5faf2',100:'#ebf5e5',200:'#d9ebca',300:'#b8d99e',400:'#8cc56f',500:'#60b240',600:'#4d8f33',700:'#3b6b27',800:'#2a491d',900:'#1c3012'},