Update
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -32,8 +32,8 @@
|
||||
<!-- Nepcha Analytics (nepcha.com) -->
|
||||
<!-- Nepcha is a easy-to-use web analytics. No cookies and fully compliant with GDPR, CCPA and PECR. -->
|
||||
<script defer data-site="YOUR_DOMAIN_HERE" src="https://api.nepcha.com/js/nepcha-analytics.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-55cb988b.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-c9347383.css">
|
||||
<script type="module" crossorigin src="/assets/index-3cdb6846.js"></script>
|
||||
<link rel="stylesheet" href="/assets/index-422138e0.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'},
|
||||
|
||||
Reference in New Issue
Block a user