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 Analytics (nepcha.com) -->
|
||||||
<!-- Nepcha is a easy-to-use web analytics. No cookies and fully compliant with GDPR, CCPA and PECR. -->
|
<!-- 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 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>
|
<script type="module" crossorigin src="/assets/index-3cdb6846.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/index-c9347383.css">
|
<link rel="stylesheet" href="/assets/index-422138e0.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -4,20 +4,20 @@ const ToastContext = createContext(null);
|
|||||||
|
|
||||||
const VARIANT_STYLES = {
|
const VARIANT_STYLES = {
|
||||||
success: {
|
success: {
|
||||||
container: "border-green-500/80 bg-green-950/90 text-green-100",
|
container: "border-mossGreen-500/80 bg-mossGreen-900/90 text-white",
|
||||||
accent: "bg-green-400"
|
accent: "bg-mossGreen-400"
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
container: "border-yellow-500/80 bg-yellow-950/90 text-yellow-100",
|
container: "border-warmAmberGlow-500/80 bg-warmAmberGlow-900/90 text-white",
|
||||||
accent: "bg-yellow-400"
|
accent: "bg-warmAmberGlow-400"
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
container: "border-red-500/80 bg-red-950/90 text-red-100",
|
container: "border-sunsetCoral-500/80 bg-sunsetCoral-900/90 text-white",
|
||||||
accent: "bg-red-400"
|
accent: "bg-sunsetCoral-400"
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
container: "border-sky-500/60 bg-sky-950/90 text-sky-100",
|
container: "border-arcticBlue-500 bg-arcticBlue-900/90 text-white",
|
||||||
accent: "bg-sky-400"
|
accent: "bg-arcticBlue-400"
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
useSelect
|
useSelect
|
||||||
} from "@material-tailwind/react";
|
} 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 { setOpen } = useSelect();
|
||||||
|
|
||||||
const reopen = useCallback(() => {
|
const reopen = useCallback(() => {
|
||||||
@@ -31,20 +31,13 @@ const StickyOption = React.forwardRef(({ value, onValueSelect, onClick, onKeyDow
|
|||||||
});
|
});
|
||||||
}, [setOpen]);
|
}, [setOpen]);
|
||||||
|
|
||||||
const commitSelection = useCallback(() => {
|
|
||||||
if (typeof onValueSelect === "function") {
|
|
||||||
onValueSelect(value);
|
|
||||||
}
|
|
||||||
}, [onValueSelect, value]);
|
|
||||||
|
|
||||||
const handleClick = useCallback((event) => {
|
const handleClick = useCallback((event) => {
|
||||||
if (typeof onClick === "function") {
|
if (typeof onClick === "function") {
|
||||||
onClick(event);
|
onClick(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
commitSelection();
|
|
||||||
reopen();
|
reopen();
|
||||||
}, [onClick, commitSelection, reopen]);
|
}, [onClick, reopen]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((event) => {
|
const handleKeyDown = useCallback((event) => {
|
||||||
if (typeof onKeyDown === "function") {
|
if (typeof onKeyDown === "function") {
|
||||||
@@ -52,10 +45,9 @@ const StickyOption = React.forwardRef(({ value, onValueSelect, onClick, onKeyDow
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
commitSelection();
|
|
||||||
reopen();
|
reopen();
|
||||||
}
|
}
|
||||||
}, [onKeyDown, commitSelection, reopen]);
|
}, [onKeyDown, reopen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Option
|
<Option
|
||||||
@@ -71,10 +63,10 @@ const StickyOption = React.forwardRef(({ value, onValueSelect, onClick, onKeyDow
|
|||||||
StickyOption.displayName = "StickyOption";
|
StickyOption.displayName = "StickyOption";
|
||||||
|
|
||||||
const STATUS_COLORS = {
|
const STATUS_COLORS = {
|
||||||
success: "text-green-400",
|
success: "text-mossGreen-500",
|
||||||
warning: "text-yellow-400",
|
warning: "text-warmAmberGlow-500",
|
||||||
error: "text-red-400",
|
error: "text-sunsetCoral-500",
|
||||||
started: "text-blue-300"
|
started: "text-arcticBlue-500"
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTimestamp = (value) => {
|
const formatTimestamp = (value) => {
|
||||||
@@ -419,7 +411,7 @@ export function Logs() {
|
|||||||
}
|
}
|
||||||
}, [filtersReady, currentFilters, perPage, page]);
|
}, [filtersReady, currentFilters, perPage, page]);
|
||||||
|
|
||||||
const handleMultiSelectChange = (setter) => (valueOrEvent) => {
|
const handleMultiSelectChange = useCallback((setter) => (valueOrEvent) => {
|
||||||
if (typeof valueOrEvent === "string") {
|
if (typeof valueOrEvent === "string") {
|
||||||
const value = valueOrEvent;
|
const value = valueOrEvent;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
@@ -453,7 +445,12 @@ export function Logs() {
|
|||||||
setter(values);
|
setter(values);
|
||||||
}
|
}
|
||||||
setPage(1);
|
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) => {
|
const removeFilterValue = (setter, value) => {
|
||||||
setter((prev) => prev.filter((entry) => entry !== value));
|
setter((prev) => prev.filter((entry) => entry !== value));
|
||||||
@@ -631,7 +628,7 @@ export function Logs() {
|
|||||||
return (
|
return (
|
||||||
<div className="mt-12 mb-8 flex flex-col gap-12">
|
<div className="mt-12 mb-8 flex flex-col gap-12">
|
||||||
<Card>
|
<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">
|
<Typography variant="h6" color="white">
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleFilters}
|
onClick={handleToggleFilters}
|
||||||
@@ -679,7 +676,7 @@ export function Logs() {
|
|||||||
<Select
|
<Select
|
||||||
multiple
|
multiple
|
||||||
onChange={noop}
|
onChange={noop}
|
||||||
className="text-gray-500"
|
className="text-stormGrey-500"
|
||||||
variant="static"
|
variant="static"
|
||||||
dismiss={{ itemPress: false }}
|
dismiss={{ itemPress: false }}
|
||||||
label="Stacks"
|
label="Stacks"
|
||||||
@@ -688,16 +685,22 @@ export function Logs() {
|
|||||||
<StickyOption
|
<StickyOption
|
||||||
key={value}
|
key={value}
|
||||||
value={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' : ''}`}
|
className={`text-black-600 ${value === ALL_OPTION_VALUE ? 'font-semibold text-black-800' : ''}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</StickyOption>
|
</StickyOption>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</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 ? (
|
{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
|
Alle Stacks
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -707,7 +710,7 @@ export function Logs() {
|
|||||||
key={stackId}
|
key={stackId}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeFilterValue(setSelectedStacks, stackId)}
|
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"
|
title="Filter entfernen"
|
||||||
>
|
>
|
||||||
{stackLabelMap.get(stackId) ?? `Stack ${stackId}`}
|
{stackLabelMap.get(stackId) ?? `Stack ${stackId}`}
|
||||||
@@ -721,7 +724,7 @@ export function Logs() {
|
|||||||
<Select
|
<Select
|
||||||
multiple
|
multiple
|
||||||
onChange={noop}
|
onChange={noop}
|
||||||
className="text-gray-500"
|
className="text-stormGrey-500"
|
||||||
variant="static"
|
variant="static"
|
||||||
dismiss={{ itemPress: false }}
|
dismiss={{ itemPress: false }}
|
||||||
label="Status"
|
label="Status"
|
||||||
@@ -730,16 +733,22 @@ export function Logs() {
|
|||||||
<StickyOption
|
<StickyOption
|
||||||
key={value}
|
key={value}
|
||||||
value={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' : ''}`}
|
className={`text-black-600 ${value === ALL_OPTION_VALUE ? 'font-semibold text-black-800' : ''}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</StickyOption>
|
</StickyOption>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</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 ? (
|
{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
|
Alle Status
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -749,7 +758,7 @@ export function Logs() {
|
|||||||
key={status}
|
key={status}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeFilterValue(setSelectedStatuses, status)}
|
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"
|
title="Filter entfernen"
|
||||||
>
|
>
|
||||||
{status}
|
{status}
|
||||||
@@ -763,7 +772,7 @@ export function Logs() {
|
|||||||
<Select
|
<Select
|
||||||
multiple
|
multiple
|
||||||
onChange={noop}
|
onChange={noop}
|
||||||
className="text-gray-500"
|
className="text-stormGrey-500"
|
||||||
variant="static"
|
variant="static"
|
||||||
dismiss={{ itemPress: false }}
|
dismiss={{ itemPress: false }}
|
||||||
label="Redeploy-Typ"
|
label="Redeploy-Typ"
|
||||||
@@ -772,7 +781,13 @@ export function Logs() {
|
|||||||
<StickyOption
|
<StickyOption
|
||||||
key={value}
|
key={value}
|
||||||
value={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' : ''}`}
|
className={`text-black-600 ${value === ALL_OPTION_VALUE ? 'font-semibold text-black-800' : ''}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@@ -780,9 +795,9 @@ export function Logs() {
|
|||||||
|
|
||||||
))}
|
))}
|
||||||
</Select>
|
</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 ? (
|
{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
|
Alle Typen
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -792,7 +807,7 @@ export function Logs() {
|
|||||||
key={type}
|
key={type}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeFilterValue(setSelectedRedeployTypes, type)}
|
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"
|
title="Filter entfernen"
|
||||||
>
|
>
|
||||||
{REDEPLOY_TYPE_LABELS[type] ?? type}
|
{REDEPLOY_TYPE_LABELS[type] ?? type}
|
||||||
@@ -806,7 +821,7 @@ export function Logs() {
|
|||||||
<Select
|
<Select
|
||||||
multiple
|
multiple
|
||||||
onChange={noop}
|
onChange={noop}
|
||||||
className="text-gray-500"
|
className="text-stormGrey-500"
|
||||||
variant="static"
|
variant="static"
|
||||||
dismiss={{ itemPress: false }}
|
dismiss={{ itemPress: false }}
|
||||||
label="Endpoints"
|
label="Endpoints"
|
||||||
@@ -815,16 +830,22 @@ export function Logs() {
|
|||||||
<StickyOption
|
<StickyOption
|
||||||
key={value}
|
key={value}
|
||||||
value={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' : ''}`}
|
className={`text-black-600 ${value === ALL_OPTION_VALUE ? 'font-semibold text-black-800' : ''}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</StickyOption>
|
</StickyOption>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</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 ? (
|
{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
|
Alle Endpoints
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -834,7 +855,7 @@ export function Logs() {
|
|||||||
key={endpoint}
|
key={endpoint}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeFilterValue(setSelectedEndpoints, endpoint)}
|
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"
|
title="Filter entfernen"
|
||||||
>
|
>
|
||||||
Endpoint {endpoint}
|
Endpoint {endpoint}
|
||||||
@@ -903,7 +924,7 @@ export function Logs() {
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader variant="gradient" color="gray" className="mb-8 p-6">
|
<CardHeader variant="gradient" color="gray" className="mb-5 p-4">
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
color="white"
|
color="white"
|
||||||
@@ -918,7 +939,7 @@ export function Logs() {
|
|||||||
<select
|
<select
|
||||||
value={perPage}
|
value={perPage}
|
||||||
onChange={handlePerPageChange}
|
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 }) => (
|
{PER_PAGE_OPTIONS.map(({ value, label }) => (
|
||||||
<option key={value} value={value}>
|
<option key={value} value={value}>
|
||||||
@@ -936,11 +957,11 @@ export function Logs() {
|
|||||||
{["Zeitpunkt", "Stack", "Art", "Status", "Nachricht", "Endpoint", "Aktionen"].map((el) => (
|
{["Zeitpunkt", "Stack", "Art", "Status", "Nachricht", "Endpoint", "Aktionen"].map((el) => (
|
||||||
<th
|
<th
|
||||||
key={el}
|
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
|
<Typography
|
||||||
variant="small"
|
variant="small"
|
||||||
className="text-[11px] font-bold uppercase text-blue-gray-400"
|
className="text-[11px] font-bold uppercase text-stormGrey-400"
|
||||||
>
|
>
|
||||||
{el}
|
{el}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -962,7 +983,7 @@ export function Logs() {
|
|||||||
<td className={className}>
|
<td className={className}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="small"
|
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)}
|
{formatTimestamp(log.timestamp)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</td>
|
</td>
|
||||||
@@ -973,7 +994,7 @@ export function Logs() {
|
|||||||
variant="small"
|
variant="small"
|
||||||
>
|
>
|
||||||
{showStackId && (
|
{showStackId && (
|
||||||
<span className="text-xs text-gray-400">ID: {log.stackId}</span>
|
<span className="text-xs text-stormGrey-400">ID: {log.stackId}</span>
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ const UPDATE_STATUS_LABELS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UPDATE_STATUS_STYLES = {
|
const UPDATE_STATUS_STYLES = {
|
||||||
idle: "bg-gray-700 text-gray-200",
|
idle: "bg-stormGrey-700 text-gray-200",
|
||||||
running: "bg-sky-600 text-white",
|
running: "bg-arcticBlue-600 text-white",
|
||||||
success: "bg-green-600 text-white",
|
success: "bg-mossGreen-600 text-white",
|
||||||
error: "bg-red-600 text-white"
|
error: "bg-sunsetCoral-600 text-white"
|
||||||
};
|
};
|
||||||
|
|
||||||
const UPDATE_STAGE_LABELS = {
|
const UPDATE_STAGE_LABELS = {
|
||||||
@@ -36,13 +36,13 @@ const UPDATE_STAGE_LABELS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const LOG_LEVEL_STYLES = {
|
const LOG_LEVEL_STYLES = {
|
||||||
info: "text-blue-700",
|
info: "text-arcticBlue-700",
|
||||||
success: "text-green-700",
|
success: "text-mossGreen-700",
|
||||||
warning: "text-amber-700",
|
warning: "text-warmAmberGlow-700",
|
||||||
error: "text-red-700",
|
error: "text-sunsetCoral-700",
|
||||||
stdout: "text-gray-700",
|
stdout: "text-stormGrey-700",
|
||||||
stderr: "text-orange-700",
|
stderr: "text-warmAmberGlow-700",
|
||||||
debug: "text-cyan-700"
|
debug: "text-lavenderSmoke-700"
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatLogTimestamp = (value) => {
|
const formatLogTimestamp = (value) => {
|
||||||
@@ -128,6 +128,7 @@ export function Maintenance() {
|
|||||||
|
|
||||||
const maintenanceActive = Boolean(maintenanceMeta?.active);
|
const maintenanceActive = Boolean(maintenanceMeta?.active);
|
||||||
const maintenanceMessage = maintenanceMeta?.message;
|
const maintenanceMessage = maintenanceMeta?.message;
|
||||||
|
const maintenanceExtraType = maintenanceMeta?.extra?.type;
|
||||||
const updateRunning = Boolean(updateState?.running);
|
const updateRunning = Boolean(updateState?.running);
|
||||||
|
|
||||||
const [scriptDraft, setScriptDraft] = useState("");
|
const [scriptDraft, setScriptDraft] = useState("");
|
||||||
@@ -432,10 +433,16 @@ export function Maintenance() {
|
|||||||
|
|
||||||
setMaintenanceToggleLoading(true);
|
setMaintenanceToggleLoading(true);
|
||||||
try {
|
try {
|
||||||
await setMaintenanceMode({
|
const shouldPreserveMessage = nextActive && Boolean(maintenanceExtraType);
|
||||||
active: nextActive,
|
const payload = { active: nextActive };
|
||||||
message: nextActive ? (maintenanceMessage ?? undefined) : null
|
|
||||||
});
|
if (shouldPreserveMessage && maintenanceMessage) {
|
||||||
|
payload.message = maintenanceMessage;
|
||||||
|
} else {
|
||||||
|
payload.message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await setMaintenanceMode(payload);
|
||||||
showToast({
|
showToast({
|
||||||
variant: nextActive ? "info" : "success",
|
variant: nextActive ? "info" : "success",
|
||||||
title: nextActive ? "Wartungsmodus aktiviert" : "Wartungsmodus deaktiviert",
|
title: nextActive ? "Wartungsmodus aktiviert" : "Wartungsmodus deaktiviert",
|
||||||
@@ -449,7 +456,7 @@ export function Maintenance() {
|
|||||||
} finally {
|
} finally {
|
||||||
setMaintenanceToggleLoading(false);
|
setMaintenanceToggleLoading(false);
|
||||||
}
|
}
|
||||||
}, [maintenanceLoading, maintenanceToggleLoading, maintenanceActive, updateRunning, setMaintenanceMode, maintenanceMessage, showToast]);
|
}, [maintenanceLoading, maintenanceToggleLoading, maintenanceActive, updateRunning, setMaintenanceMode, maintenanceMessage, maintenanceExtraType, showToast]);
|
||||||
|
|
||||||
const handleTriggerUpdate = useCallback(async () => {
|
const handleTriggerUpdate = useCallback(async () => {
|
||||||
if (maintenanceActive || updateRunning) {
|
if (maintenanceActive || updateRunning) {
|
||||||
@@ -589,8 +596,41 @@ export function Maintenance() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<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"
|
||||||
|
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
|
<Typography
|
||||||
variant="h6"
|
variant="h6"
|
||||||
color="white"
|
color="white"
|
||||||
@@ -616,7 +656,7 @@ export function Maintenance() {
|
|||||||
: "Portainer ist auf dem aktuellen Stand.")}
|
: "Portainer ist auf dem aktuellen Stand.")}
|
||||||
</p>
|
</p>
|
||||||
{portainerUpdatedAt && !portainerLoading && (
|
{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", {
|
Stand: {portainerUpdatedAt.toLocaleString("de-DE", {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
@@ -665,23 +705,23 @@ export function Maintenance() {
|
|||||||
</div>
|
</div>
|
||||||
{portainerStatus.edition && (
|
{portainerStatus.edition && (
|
||||||
<div className="flex items-center justify-between">
|
<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>
|
<span className="font-medium">{portainerStatus.edition}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{portainerStatus.build && (
|
{portainerStatus.build && (
|
||||||
<div className="flex items-center justify-between">
|
<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>
|
<span className="font-medium">{portainerStatus.build}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{portainerStatus.errors?.latestVersion && (
|
{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}
|
Neueste Version konnte nicht ermittelt werden: {portainerStatus.errors.latestVersion}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{portainerStatus.errors?.container && (
|
{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}
|
Container-Details konnten nicht ermittelt werden: {portainerStatus.errors.container}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -870,7 +910,7 @@ export function Maintenance() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{sshTestResult && (
|
{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}`}
|
{sshTestResult.success ? 'SSH-Verbindung erfolgreich.' : `SSH-Verbindung fehlgeschlagen: ${sshTestResult.error}`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -905,7 +945,7 @@ export function Maintenance() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
{scriptConfig?.customUpdatedAt && (
|
{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)}
|
Zuletzt geändert: {formatCreatedAt(scriptConfig.customUpdatedAt)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -940,29 +980,29 @@ export function Maintenance() {
|
|||||||
<span>{updateFinishedAt}</span>
|
<span>{updateFinishedAt}</span>
|
||||||
</div>
|
</div>
|
||||||
{updateStatusMessage && (
|
{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}
|
{updateStatusMessage}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{updateState?.error && (
|
{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}
|
{updateState.error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{updateActionError && (
|
{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}
|
{updateActionError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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">
|
<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 ? (
|
{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) => (
|
updateLogs.map((entry, index) => (
|
||||||
<div key={`${entry.timestamp}-${index}`} className="mb-2 last:mb-0">
|
<div key={`${entry.timestamp}-${index}`} className="mb-2 last:mb-0">
|
||||||
<span className="text-gray-500">[{formatLogTimestamp(entry.timestamp)}]</span>{" "}
|
<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}
|
{entry.message}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -984,46 +1024,124 @@ export function Maintenance() {
|
|||||||
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader
|
<CardHeader variant="gradient" color="gray" className="mb-5 p-4">
|
||||||
color="transparent"
|
<Typography
|
||||||
floated={false}
|
variant="h6"
|
||||||
shadow={false}
|
color="white"
|
||||||
className="m-0 p-4 block antialiased font-sans"
|
className="flex items-center justify-between"
|
||||||
>
|
>
|
||||||
<Typography variant="h5" color="blue-gray" className="block antialiased font-sans">
|
<span>Doppelte Stacks</span>
|
||||||
Wartungsmodus
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody className="flex flex-col gap-4 p-4">
|
<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="flex items-start justify-between gap-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm text-blue-gray-200">
|
<p className="text-sm antialiased font-sans text-sm font-light leading-normal text-inherit">
|
||||||
Schaltet StackPulse für Benutzer in den Wartungsmodus.
|
{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>
|
</p>
|
||||||
{maintenanceMessage ? (<p className="text-xs text-gray-500">Hinweis: {maintenanceMessage}</p>) : null}
|
{duplicatesUpdatedAt && !duplicatesLoading && !isDuplicatesDisabled && (
|
||||||
<p className="text-xs text-gray-500">
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
{maintenanceActive
|
Stand: {duplicatesUpdatedAt.toLocaleString("de-DE", {
|
||||||
? `Aktiv seit ${maintenanceActivatedAt ?? "-"}.`
|
hour: "2-digit",
|
||||||
: `Zuletzt geändert: ${maintenanceUpdatedAt ?? "-"}.`}
|
minute: "2-digit",
|
||||||
</p>
|
second: "2-digit"
|
||||||
{updateRunning && (
|
})}
|
||||||
<p className="text-xs text-amber-600">Portainer-Update läuft – Umschalten vorübergehend gesperrt.</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-gray-500">
|
|
||||||
Status: {maintenanceActive ? "Aktiv" : "Inaktiv"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
|
||||||
checked={maintenanceActive}
|
<Button
|
||||||
disabled={maintenanceToggleLoading || maintenanceLoading || updateRunning}
|
onClick={() => fetchDuplicates({ silent: false })}
|
||||||
onChange={(event) => handleMaintenanceToggle(event.target.checked)}
|
disabled={isDuplicatesDisabled || duplicatesLoading || duplicatesRefreshing || activeCleanupId !== null}
|
||||||
ripple={false}
|
className=""
|
||||||
color="amber"
|
>
|
||||||
/>
|
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>
|
</div>
|
||||||
{maintenanceToggleLoading && (
|
{duplicatesLoading ? (
|
||||||
<p className="text-xs text-gray-500">Wartungsmodus wird aktualisiert…</p>
|
<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>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ module.exports = withMT({
|
|||||||
green: colors.green,
|
green: colors.green,
|
||||||
slate: colors.slate,
|
slate: colors.slate,
|
||||||
orange: colors.orange, // falls du z. B. text-orange-300 brauchst
|
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'},
|
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'},
|
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'},
|
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