This commit is contained in:
root
2025-10-20 07:51:36 +00:00
parent 974462e983
commit 476b09499a
8 changed files with 247 additions and 112 deletions
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
+2 -2
View File
@@ -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-ad90a034.js"></script> <script type="module" crossorigin src="/assets/index-5c7d29a2.js"></script>
<link rel="stylesheet" href="/assets/index-71775912.css"> <link rel="stylesheet" href="/assets/index-770f67cf.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+1 -1
View File
@@ -33,7 +33,7 @@ export function reducer(state, action) {
export function MaterialTailwindControllerProvider({ children }) { export function MaterialTailwindControllerProvider({ children }) {
const initialState = { const initialState = {
openSidenav: false, openSidenav: false,
sidenavColor: "dark", sidenavColor: "gray",
sidenavType: "white", sidenavType: "white",
transparentNavbar: true, transparentNavbar: true,
fixedNavbar: false, fixedNavbar: false,
+1 -1
View File
@@ -15,7 +15,7 @@ import App from "./App";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { ThemeProvider } from "@material-tailwind/react"; import { ThemeProvider } from "@material-tailwind/react";
import { MaterialTailwindControllerProvider } from "@/context"; import { MaterialTailwindControllerProvider } from "@/context";
import "../public/css/tailwind.css"; import "../dist/css/tailwind.css";
ReactDOM.createRoot(document.getElementById("root")).render( ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode> <React.StrictMode>
+206 -71
View File
@@ -14,9 +14,62 @@ import {
ButtonGroup, ButtonGroup,
Select, Select,
Option, Option,
Input Input,
useSelect
} from "@material-tailwind/react"; } from "@material-tailwind/react";
const StickyOption = React.forwardRef(({ value, onValueSelect, onClick, onKeyDown, ...props }, ref) => {
const { setOpen } = useSelect();
const reopen = useCallback(() => {
const schedule = (typeof window !== "undefined" && typeof window.requestAnimationFrame === "function")
? window.requestAnimationFrame
: (callback) => setTimeout(callback, 0);
schedule(() => {
setOpen(true);
});
}, [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]);
const handleKeyDown = useCallback((event) => {
if (typeof onKeyDown === "function") {
onKeyDown(event);
}
if (event.key === "Enter" || event.key === " ") {
commitSelection();
reopen();
}
}, [onKeyDown, commitSelection, reopen]);
return (
<Option
{...props}
value={value}
ref={ref}
onClick={handleClick}
onKeyDown={handleKeyDown}
/>
);
});
StickyOption.displayName = "StickyOption";
const STATUS_COLORS = { const STATUS_COLORS = {
success: "text-green-400", success: "text-green-400",
warning: "text-yellow-400", warning: "text-yellow-400",
@@ -105,6 +158,8 @@ export function Logs() {
const [perPage, setPerPage] = useState(PER_PAGE_DEFAULT); const [perPage, setPerPage] = useState(PER_PAGE_DEFAULT);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const noop = useCallback(() => { }, []);
const updateFilterOptions = useCallback((payload) => { const updateFilterOptions = useCallback((payload) => {
const logsPayload = Array.isArray(payload) ? payload : payload?.items ?? []; const logsPayload = Array.isArray(payload) ? payload : payload?.items ?? [];
@@ -364,8 +419,34 @@ export function Logs() {
} }
}, [filtersReady, currentFilters, perPage, page]); }, [filtersReady, currentFilters, perPage, page]);
const handleMultiSelectChange = (setter) => (event) => { const handleMultiSelectChange = (setter) => (valueOrEvent) => {
const values = Array.from(event.target.selectedOptions).map((option) => option.value); if (typeof valueOrEvent === "string") {
const value = valueOrEvent;
if (value === "") {
return;
}
setter((prev) => {
if (value === ALL_OPTION_VALUE) {
return [];
}
if (prev.includes(value)) {
return prev.filter((entry) => entry !== value);
}
return [...prev, value];
});
setPage(1);
return;
}
const selectedOptions = valueOrEvent?.target?.selectedOptions;
if (!selectedOptions) {
return;
}
const values = Array.from(selectedOptions).map((option) => option.value);
if (values.includes(ALL_OPTION_VALUE)) { if (values.includes(ALL_OPTION_VALUE)) {
setter([]); setter([]);
} else { } else {
@@ -374,24 +455,8 @@ export function Logs() {
setPage(1); setPage(1);
}; };
const handleOptionMouseDown = (event, currentValues, setter) => { const removeFilterValue = (setter, value) => {
event.preventDefault(); setter((prev) => prev.filter((entry) => entry !== value));
event.stopPropagation();
const { value } = event.target;
if (value === ALL_OPTION_VALUE) {
if (currentValues.length) {
setter([]);
setPage(1);
}
return;
}
const nextValues = currentValues.includes(value)
? currentValues.filter((entry) => entry !== value)
: [...currentValues, value];
setter(nextValues);
setPage(1); setPage(1);
}; };
@@ -614,161 +679,174 @@ export function Logs() {
<div className="grid gap-4 flex-1"> <div className="grid gap-4 flex-1">
<Select <Select
multiple multiple
value={selectedStacks} onChange={noop}
onChange={handleMultiSelectChange(setSelectedStacks)}
className="text-gray-500" className="text-gray-500"
variant="static" variant="static"
dismiss={{ itemPress: false }}
label="Stacks" label="Stacks"
> >
{stackSelectOptions.map(({ value, label }) => ( {stackSelectOptions.map(({ value, label }) => (
<option <StickyOption
key={value} key={value}
value={value} value={value}
onMouseDown={(event) => handleOptionMouseDown(event, selectedStacks, setSelectedStacks)} onValueSelect={handleMultiSelectChange(setSelectedStacks)}
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}
</option> </StickyOption>
))} ))}
</Select> </Select>
<div className="mt-2 min-h-[1.5rem] text-xs text-gray-400"> <div className="mt-2 mb-2 min-h-[1.5rem] text-xs text-gray-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-gray-700/60 px-2 py-0.5 text-gray-300 ">
Alle Stacks Alle Stacks
</span> </span>
) : ( ) : (
<div className="flex flex-wrap gap-2"> <span>
{selectedStacks.map((stackId) => ( {selectedStacks.map((stackId) => (
<span <button
key={stackId} key={stackId}
className="rounded-full bg-purple-500/80 px-2 py-0.5 text-white" 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"
title="Filter entfernen"
> >
{stackLabelMap.get(stackId) ?? `Stack ${stackId}`} {stackLabelMap.get(stackId) ?? `Stack ${stackId}`}
</span> </button>
))} ))}
</div> </span>
)} )}
</div> </div>
</div> </div>
<div className="grid gap-4 flex-1"> <div className="grid gap-4 flex-1">
<Select <Select
multiple multiple
value={selectedStatuses} onChange={noop}
onChange={handleMultiSelectChange(setSelectedStatuses)}
className="text-gray-500" className="text-gray-500"
variant="static" variant="static"
dismiss={{ itemPress: false }}
label="Status" label="Status"
> >
{statusSelectOptions.map(({ value, label }) => ( {statusSelectOptions.map(({ value, label }) => (
<option <StickyOption
key={value} key={value}
value={value} value={value}
onMouseDown={(event) => handleOptionMouseDown(event, selectedStatuses, setSelectedStatuses)} onValueSelect={handleMultiSelectChange(setSelectedStatuses)}
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}
</option> </StickyOption>
))} ))}
</Select> </Select>
<div className="mt-2 min-h-[1.5rem] text-xs text-gray-400"> <div className="mt-2 mb-2 min-h-[1.5rem] text-xs text-gray-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-gray-700/60 px-2 py-0.5 text-gray-300">
Alle Status Alle Status
</span> </span>
) : ( ) : (
<div className="flex flex-wrap gap-2"> <span>
{selectedStatuses.map((status) => ( {selectedStatuses.map((status) => (
<span <button
key={status} key={status}
className="rounded-full bg-brown-500/80 px-2 py-0.5 text-white" 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"
title="Filter entfernen"
> >
{status} {status}
</span> </button>
))} ))}
</div> </span>
)} )}
</div> </div>
</div> </div>
<div className="grid gap-4 flex-1"> <div className="grid gap-4 flex-1">
<Select <Select
multiple multiple
value={selectedRedeployTypes} onChange={noop}
onChange={handleMultiSelectChange(setSelectedRedeployTypes)}
className="text-gray-500" className="text-gray-500"
variant="static" variant="static"
dismiss={{ itemPress: false }}
label="Redeploy-Typ" label="Redeploy-Typ"
> >
{redeployTypeSelectOptions.map(({ value, label }) => ( {redeployTypeSelectOptions.map(({ value, label }) => (
<option <StickyOption
key={value} key={value}
value={value} value={value}
onMouseDown={(event) => handleOptionMouseDown(event, selectedRedeployTypes, setSelectedRedeployTypes)} onValueSelect={handleMultiSelectChange(setSelectedRedeployTypes)}
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}
</option> </StickyOption>
))} ))}
</Select> </Select>
<div className="mt-2 min-h-[1.5rem] text-xs text-gray-400"> <div className="mt-2 mb-2 min-h-[1.5rem] text-xs text-gray-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-gray-700/60 px-2 py-0.5 text-gray-300">
Alle Typen Alle Typen
</span> </span>
) : ( ) : (
<div className="flex flex-wrap gap-2"> <span>
{selectedRedeployTypes.map((type) => ( {selectedRedeployTypes.map((type) => (
<span <button
key={type} key={type}
className="rounded-full bg-teal-500/80 px-2 py-0.5 text-white" 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"
title="Filter entfernen"
> >
{REDEPLOY_TYPE_LABELS[type] ?? type} {REDEPLOY_TYPE_LABELS[type] ?? type}
</span> </button>
))} ))}
</div> </span>
)} )}
</div> </div>
</div> </div>
<div className="grid gap-4 flex-1"> <div className="grid gap-4 flex-1">
<Select <Select
multiple multiple
value={selectedEndpoints} onChange={noop}
onChange={handleMultiSelectChange(setSelectedRedeployTypes)}
className="text-gray-500" className="text-gray-500"
variant="static" variant="static"
dismiss={{ itemPress: false }}
label="Endpoints" label="Endpoints"
> >
{endpointSelectOptions.map(({ value, label }) => ( {endpointSelectOptions.map(({ value, label }) => (
<option <StickyOption
key={value} key={value}
value={value} value={value}
onMouseDown={(event) => handleOptionMouseDown(event, selectedEndpoints, setSelectedEndpoints)} onValueSelect={handleMultiSelectChange(setSelectedEndpoints)}
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}
</option> </StickyOption>
))} ))}
</Select> </Select>
<div className="mt-2 min-h-[1.5rem] text-xs text-gray-400"> <div className="mt-2 mb-2 min-h-[1.5rem] text-xs text-gray-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-gray-700/60 px-2 py-0.5 text-gray-300">
Alle Endpoints Alle Endpoints
</span> </span>
) : ( ) : (
<div className="flex flex-wrap gap-2"> <span>
{selectedEndpoints.map((endpoint) => ( {selectedEndpoints.map((endpoint) => (
<span <button
key={endpoint} key={endpoint}
className="rounded-full bg-blue-500/80 px-2 py-0.5 text-white" 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"
title="Filter entfernen"
> >
{endpoint} Endpoint {endpoint}
</span> </button>
))} ))}
</div> </span>
)} )}
</div> </div>
</div> </div>
</div> </div>
<div className="flex flex-wrap gap-2 mt-10"> <div className="flex flex-wrap gap-2 mt-5">
<div className="grid gap-4 flex-1"> <div className="grid gap-4 flex-1">
<Input <Input
value={messageQuery} value={messageQuery}
@@ -781,18 +859,75 @@ export function Logs() {
placeholder="Suche" /> placeholder="Suche" />
</div> </div>
</div> </div>
<div className="flex flex-col md:flex-row flex-wrap gap-4 mt-8">
<div className="flex-1">
<Input
type="datetime-local"
variant="static"
label="Von"
value={fromDate}
onChange={(event) => {
setFromDate(event.target.value);
setPage(1);
}}
className="w-full"
/>
</div>
<div className="flex-1">
<Input
type="datetime-local"
variant="static"
label="Bis"
value={toDate}
onChange={(event) => {
setToDate(event.target.value);
setPage(1);
}}
className="w-full"
/>
</div>
<div className="flex-1">
<Button
onClick={handleResetFilters}
disabled={actionLoading || loading}
className="w-full"
>
Zurücksetzen
</Button>
</div>
</div>
</div> </div>
)} )}
</CardBody> </CardBody>
</Card> </Card>
<Card> <Card>
<CardHeader variant="gradient" color="gray" className="mb-8 p-6"> <CardHeader variant="gradient" color="gray" className="mb-8 p-6">
<Typography variant="h6" color="white"> <Typography
Logs variant="h6"
color="white"
className="flex items-center justify-between"
>
<span>Logs</span>
<div className="flex items-center gap-2">
<span className="text-xs uppercase tracking-wide text-gray-400">
Einträge pro Seite:
</span>
<select
value={perPage}
onChange={handlePerPageChange}
className="rounded-md border border-gray-700 bg-gray-900 px-2 py-1 text-sm text-gray-100 focus:border-purple-500 focus:outline-none focus:ring-1 focus:ring-purple-500"
>
{PER_PAGE_OPTIONS.map(({ value, label }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
</Typography> </Typography>
</CardHeader> </CardHeader>
<CardBody className="overflow-x-scroll px-0 pt-0 pb-2"> <CardBody className="overflow-x-scroll px-0 pt-0 pb-2">
+2 -2
View File
@@ -807,12 +807,12 @@ export default function Logs() {
</div> </div>
<div className="flex flex-wrap items-center justify-end gap-3"> <div className="flex flex-wrap items-center justify-end gap-3">
<button <Button
onClick={handleResetFilters} onClick={handleResetFilters}
className="rounded-md border border-gray-600 px-4 py-2 text-gray-200 transition hover:bg-gray-700" className="rounded-md border border-gray-600 px-4 py-2 text-gray-200 transition hover:bg-gray-700"
> >
Zurücksetzen Zurücksetzen
</button> </Button>
</div> </div>
</div> </div>
)} )}