Added Debug Log and info pickup older 6month + hygiene crt
This commit is contained in:
30
src/App.js
30
src/App.js
@@ -23,6 +23,7 @@ import ConfirmationDialog from './components/ConfirmationDialog';
|
||||
import StoreSyncOverlay from './components/StoreSyncOverlay';
|
||||
import RangePickerModal from './components/RangePickerModal';
|
||||
import StoreWatchPage from './components/StoreWatchPage';
|
||||
import DebugPage from './components/DebugPage';
|
||||
|
||||
function App() {
|
||||
const [credentials, setCredentials] = useState({ email: '', password: '' });
|
||||
@@ -484,6 +485,15 @@ function App() {
|
||||
);
|
||||
};
|
||||
|
||||
const handleToggleDormantSkip = (entryId) => {
|
||||
setIsDirty(true);
|
||||
setConfig((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === entryId ? { ...item, skipDormantCheck: !item.skipDormantCheck } : item
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleWeekdayChange = (entryId, value) => {
|
||||
setIsDirty(true);
|
||||
setConfig((prev) =>
|
||||
@@ -623,6 +633,7 @@ function App() {
|
||||
active: false,
|
||||
checkProfileId: true,
|
||||
onlyNotify: false,
|
||||
skipDormantCheck: false,
|
||||
hidden: false
|
||||
}
|
||||
];
|
||||
@@ -732,6 +743,7 @@ function App() {
|
||||
onToggleActive={handleToggleActive}
|
||||
onToggleProfileCheck={handleToggleProfileCheck}
|
||||
onToggleOnlyNotify={handleToggleOnlyNotify}
|
||||
onToggleDormantSkip={handleToggleDormantSkip}
|
||||
onWeekdayChange={handleWeekdayChange}
|
||||
weekdays={weekdays}
|
||||
onRangePickerRequest={setActiveRangePicker}
|
||||
@@ -778,12 +790,12 @@ function App() {
|
||||
<Route
|
||||
path="/store-watch"
|
||||
element={
|
||||
<StoreWatchPage
|
||||
authorizedFetch={authorizedFetch}
|
||||
knownStores={stores}
|
||||
userLocation={userLocationWithLabel}
|
||||
locationLoading={preferencesLoading}
|
||||
locationError={preferencesError}
|
||||
<StoreWatchPage
|
||||
authorizedFetch={authorizedFetch}
|
||||
knownStores={stores}
|
||||
userLocation={userLocationWithLabel}
|
||||
locationLoading={preferencesLoading}
|
||||
locationError={preferencesError}
|
||||
notificationPanelOpen={notificationPanelOpen}
|
||||
onToggleNotificationPanel={() => setNotificationPanelOpen((prev) => !prev)}
|
||||
notificationProps={sharedNotificationProps}
|
||||
@@ -791,6 +803,12 @@ function App() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/debug"
|
||||
element={
|
||||
session?.isAdmin ? <DebugPage authorizedFetch={authorizedFetch} /> : <AdminAccessMessage />
|
||||
}
|
||||
/>
|
||||
<Route path="/admin" element={adminPageContent} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
|
||||
@@ -97,6 +97,7 @@ const DashboardView = ({
|
||||
onToggleActive,
|
||||
onToggleProfileCheck,
|
||||
onToggleOnlyNotify,
|
||||
onToggleDormantSkip,
|
||||
onWeekdayChange,
|
||||
weekdays,
|
||||
onRangePickerRequest,
|
||||
@@ -225,7 +226,7 @@ const DashboardView = ({
|
||||
id: 'checkProfileId',
|
||||
header: () => <span>Profil prüfen</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center">
|
||||
<div className="text-center" title={row.original.lastFetchDate ? `Letzte Abholung: ${row.original.lastFetchDate}` : 'Keine Info zur letzten Abholung'}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-5 w-5"
|
||||
@@ -237,6 +238,23 @@ const DashboardView = ({
|
||||
enableSorting: false,
|
||||
enableColumnFilter: false
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'skipDormantCheck',
|
||||
header: () => <span>Ruhe-Prüfung</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center" title={row.original.lastFetchDate ? `Letzte Abholung: ${row.original.lastFetchDate}` : 'Keine Info zur letzten Abholung'}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-5 w-5"
|
||||
checked={!!row.original.skipDormantCheck}
|
||||
onChange={() => onToggleDormantSkip(row.original.id)}
|
||||
title="Warnung bei ausbleibenden Abholungen/Hygiene für diesen Betrieb deaktivieren"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableColumnFilter: false
|
||||
}),
|
||||
columnHelper.accessor((row) => row.onlyNotify, {
|
||||
id: 'onlyNotify',
|
||||
header: ({ column }) => (
|
||||
@@ -545,18 +563,30 @@ const DashboardView = ({
|
||||
</p>
|
||||
<p className={`text-xs mt-2 ${statusClass}`}>{statusLabel}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onStoreSelect(store)}
|
||||
title={blockedByNoPickups ? 'Keine Pickups – automatisch verborgen' : undefined}
|
||||
className="bg-blue-50 hover:bg-blue-100 text-blue-600 border border-blue-200 px-3 py-1.5 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-300"
|
||||
>
|
||||
{isVisible ? 'Zur Liste springen' : 'Hinzufügen'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-1 text-sm text-gray-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="h-4 w-4"
|
||||
checked={!!entry?.skipDormantCheck}
|
||||
onChange={() => onToggleDormantSkip(storeId)}
|
||||
title="Warnung bei ausbleibenden Abholungen/Hygiene für diesen Betrieb deaktivieren"
|
||||
/>
|
||||
<span>Ruhe-Prüfung</span>
|
||||
</label>
|
||||
<button
|
||||
onClick={() => onStoreSelect(store)}
|
||||
title={blockedByNoPickups ? 'Keine Pickups – automatisch verborgen' : undefined}
|
||||
className="bg-blue-50 hover:bg-blue-100 text-blue-600 border border-blue-200 px-3 py-1.5 rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-300"
|
||||
>
|
||||
{isVisible ? 'Zur Liste springen' : 'Hinzufügen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
||||
441
src/components/DebugPage.js
Normal file
441
src/components/DebugPage.js
Normal file
@@ -0,0 +1,441 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
createColumnHelper,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getSortedRowModel,
|
||||
getPaginationRowModel,
|
||||
useReactTable
|
||||
} from '@tanstack/react-table';
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return new Date(value).toLocaleString();
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const DirectionBadge = ({ value }) => {
|
||||
const base = 'px-2 py-1 text-xs rounded-full font-semibold';
|
||||
if (value === 'outgoing') {
|
||||
return <span className={`${base} bg-purple-100 text-purple-700`}>Outgoing</span>;
|
||||
}
|
||||
return <span className={`${base} bg-blue-100 text-blue-700`}>Incoming</span>;
|
||||
};
|
||||
|
||||
const StatusBadge = ({ status }) => {
|
||||
const code = Number(status);
|
||||
let color = 'bg-gray-100 text-gray-700';
|
||||
if (code >= 500) {
|
||||
color = 'bg-red-100 text-red-700';
|
||||
} else if (code >= 400) {
|
||||
color = 'bg-amber-100 text-amber-800';
|
||||
} else if (code > 0) {
|
||||
color = 'bg-green-100 text-green-700';
|
||||
}
|
||||
return <span className={`px-2 py-1 text-xs rounded-full font-semibold ${color}`}>{status ?? '—'}</span>;
|
||||
};
|
||||
|
||||
const buildRequestLink = (path, target) => {
|
||||
if (!path) {
|
||||
return null;
|
||||
}
|
||||
const trimmed = String(path).trim();
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
let base = '';
|
||||
if (target) {
|
||||
const normalizedTarget = String(target).trim().replace(/\/+$/, '');
|
||||
base = /^https?:\/\//i.test(normalizedTarget)
|
||||
? normalizedTarget
|
||||
: `https://${normalizedTarget}`;
|
||||
} else if (typeof window !== 'undefined' && window.location?.origin) {
|
||||
base = window.location.origin;
|
||||
}
|
||||
if (!base) {
|
||||
return null;
|
||||
}
|
||||
const normalizedPath = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
||||
return `${base}${normalizedPath}`;
|
||||
};
|
||||
|
||||
const DebugPage = ({ authorizedFetch }) => {
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const columnHelper = useMemo(() => createColumnHelper(), []);
|
||||
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 100 });
|
||||
const [expandedId, setExpandedId] = useState(null);
|
||||
|
||||
const TABLE_STATE_KEY = 'debugRequestsTableState';
|
||||
|
||||
const readTableState = useCallback(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return { sorting: [], columnFilters: [] };
|
||||
}
|
||||
try {
|
||||
const raw = window.localStorage.getItem(TABLE_STATE_KEY);
|
||||
if (!raw) {
|
||||
return { sorting: [], columnFilters: [] };
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
sorting: Array.isArray(parsed.sorting) ? parsed.sorting : [],
|
||||
columnFilters: Array.isArray(parsed.columnFilters) ? parsed.columnFilters : []
|
||||
};
|
||||
} catch {
|
||||
return { sorting: [], columnFilters: [] };
|
||||
}
|
||||
}, []);
|
||||
|
||||
const persistTableState = useCallback((state) => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window.localStorage.setItem(TABLE_STATE_KEY, JSON.stringify(state));
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fetchLogs = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const response = await authorizedFetch('/api/debug/requests');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
setLogs(Array.isArray(data.logs) ? data.logs : []);
|
||||
} catch (err) {
|
||||
setError(`Logs konnten nicht geladen werden: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [authorizedFetch]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchLogs();
|
||||
}, [fetchLogs]);
|
||||
|
||||
const rows = useMemo(() => logs.map((entry) => ({
|
||||
id: entry.id,
|
||||
direction: entry.direction || 'incoming',
|
||||
method: entry.method || '',
|
||||
path: entry.path || '',
|
||||
status: entry.status ?? null,
|
||||
durationMs: entry.durationMs ?? null,
|
||||
timestamp: entry.timestamp,
|
||||
profileId: entry.profileId || null,
|
||||
sessionId: entry.sessionId || null,
|
||||
target: entry.target || null,
|
||||
error: entry.error || null,
|
||||
responseBody: entry.responseBody || null
|
||||
})), [logs]);
|
||||
|
||||
const ColumnTextFilter = ({ column, placeholder }) => {
|
||||
if (!column.getCanFilter()) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={column.getFilterValue() ?? ''}
|
||||
onChange={(event) => column.setFilterValue(event.target.value || undefined)}
|
||||
placeholder={placeholder}
|
||||
className="mt-1 w-full rounded border px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SortableHeader = ({ column, label, placeholder }) => (
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-between text-left font-semibold whitespace-nowrap"
|
||||
onClick={column.getToggleSortingHandler()}
|
||||
>
|
||||
<span>{label}</span>
|
||||
{column.getIsSorted() ? (
|
||||
<span className="text-xs text-gray-500">{column.getIsSorted() === 'asc' ? '▲' : '▼'}</span>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400">⇅</span>
|
||||
)}
|
||||
</button>
|
||||
<ColumnTextFilter column={column} placeholder={placeholder} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor('timestamp', {
|
||||
header: ({ column }) => <SortableHeader column={column} label="Zeitpunkt" placeholder="z. B. 2024" />,
|
||||
cell: ({ getValue }) => formatDateTime(getValue()),
|
||||
sortingFn: 'datetime',
|
||||
filterFn: 'includesString'
|
||||
}),
|
||||
columnHelper.accessor('direction', {
|
||||
header: ({ column }) => (
|
||||
<SortableHeader column={column} label="Richtung" placeholder="incoming/outgoing" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<DirectionBadge value={row.original.direction} />
|
||||
{row.original.target && <span className="text-xs text-gray-500">{row.original.target}</span>}
|
||||
</div>
|
||||
),
|
||||
sortingFn: 'alphanumeric',
|
||||
filterFn: 'includesString'
|
||||
}),
|
||||
columnHelper.accessor('method', {
|
||||
header: ({ column }) => <SortableHeader column={column} label="Methode" placeholder="GET, POST…" />,
|
||||
cell: ({ getValue }) => <span className="font-mono text-xs uppercase">{getValue()}</span>,
|
||||
sortingFn: 'alphanumeric',
|
||||
filterFn: 'includesString'
|
||||
}),
|
||||
columnHelper.accessor('path', {
|
||||
header: ({ column }) => <SortableHeader column={column} label="Pfad / Ziel" placeholder="/api/…" />,
|
||||
cell: ({ getValue, row }) => {
|
||||
const value = getValue();
|
||||
const href = buildRequestLink(value, row.original.target);
|
||||
if (!href) {
|
||||
return (
|
||||
<span className="max-w-xs truncate block whitespace-nowrap" title={value}>
|
||||
{value || '—'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
className="max-w-xs truncate block whitespace-nowrap text-blue-600 hover:underline"
|
||||
title={href}
|
||||
>
|
||||
{value || href}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
sortingFn: 'alphanumeric',
|
||||
filterFn: 'includesString'
|
||||
}),
|
||||
columnHelper.accessor('status', {
|
||||
header: ({ column }) => <SortableHeader column={column} label="Status" placeholder="200, 500…" />,
|
||||
cell: ({ getValue }) => <StatusBadge status={getValue()} />,
|
||||
sortingFn: 'alphanumeric',
|
||||
filterFn: 'includesString'
|
||||
}),
|
||||
columnHelper.accessor('durationMs', {
|
||||
header: ({ column }) => <SortableHeader column={column} label="Dauer (ms)" placeholder="z. B. >100" />,
|
||||
cell: ({ getValue }) => (getValue() != null ? getValue() : '—'),
|
||||
sortingFn: 'alphanumeric',
|
||||
filterFn: 'includesString'
|
||||
}),
|
||||
columnHelper.accessor('profileId', {
|
||||
header: ({ column }) => <SortableHeader column={column} label="Profil" placeholder="Profil-ID" />,
|
||||
cell: ({ getValue }) => <span className="text-xs text-gray-600">{getValue() || '—'}</span>,
|
||||
sortingFn: 'alphanumeric',
|
||||
filterFn: 'includesString'
|
||||
}),
|
||||
columnHelper.accessor('sessionId', {
|
||||
header: ({ column }) => <SortableHeader column={column} label="Session" placeholder="Session-ID" />,
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return (
|
||||
<span className="text-xs text-gray-600 max-w-xs truncate block whitespace-nowrap" title={value || ''}>
|
||||
{value || '—'}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
sortingFn: 'alphanumeric',
|
||||
filterFn: 'includesString'
|
||||
}),
|
||||
columnHelper.accessor('error', {
|
||||
header: ({ column }) => <SortableHeader column={column} label="Fehler" placeholder="Fehlermeldung" />,
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return (
|
||||
<span className="text-xs text-gray-600 max-w-xs truncate block whitespace-nowrap" title={value || ''}>
|
||||
{value || '—'}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
sortingFn: 'alphanumeric',
|
||||
filterFn: 'includesString'
|
||||
})
|
||||
],
|
||||
[columnHelper]
|
||||
);
|
||||
|
||||
const initialState = useMemo(() => readTableState(), [readTableState]);
|
||||
const [tableSorting, setTableSorting] = useState(initialState.sorting.length ? initialState.sorting : [{ id: 'timestamp', desc: true }]);
|
||||
const [tableFilters, setTableFilters] = useState(initialState.columnFilters);
|
||||
|
||||
useEffect(() => {
|
||||
persistTableState({ sorting: tableSorting, columnFilters: tableFilters });
|
||||
}, [tableFilters, tableSorting, persistTableState]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: rows,
|
||||
columns,
|
||||
state: {
|
||||
sorting: tableSorting,
|
||||
columnFilters: tableFilters,
|
||||
pagination
|
||||
},
|
||||
onSortingChange: setTableSorting,
|
||||
onColumnFiltersChange: setTableFilters,
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel()
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-4 max-w-7xl w-full mx-auto bg-white shadow-lg rounded-lg mt-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-800">Request-Debug</h1>
|
||||
<p className="text-gray-500 text-sm">Alle Requests der letzten 14 Tage</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={fetchLogs}
|
||||
className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Lade…' : 'Neu laden'}
|
||||
</button>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="mb-4 rounded border border-red-200 bg-red-50 text-red-700 px-4 py-2">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-gray-50">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th key={header.id} className="px-4 py-2 text-left text-gray-600 align-top whitespace-nowrap">
|
||||
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={9} className="px-4 py-4 text-center text-gray-500">
|
||||
Keine Einträge vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
table.getRowModel().rows.map((row) => {
|
||||
const isExpanded = expandedId === row.original.id;
|
||||
return (
|
||||
<React.Fragment key={row.id}>
|
||||
<tr
|
||||
className={`border-b hover:bg-gray-50 cursor-pointer ${isExpanded ? 'bg-blue-50/40' : ''}`}
|
||||
onClick={() => setExpandedId((prev) => (prev === row.original.id ? null : row.original.id))}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td key={cell.id} className="px-4 py-2 align-top whitespace-nowrap">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
<tr className="border-b bg-gray-50">
|
||||
<td colSpan={table.getVisibleLeafColumns().length} className="px-4 py-3">
|
||||
<div className="text-sm text-gray-800">
|
||||
<div className="flex flex-wrap gap-4 mb-2">
|
||||
<span className="font-semibold">Details</span>
|
||||
<span>Richtung: {row.original.direction}</span>
|
||||
<span>Methode: {row.original.method}</span>
|
||||
<span>Status: {row.original.status ?? '—'}</span>
|
||||
<span>Dauer: {row.original.durationMs != null ? `${row.original.durationMs} ms` : '—'}</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold mb-1">Response-Body</p>
|
||||
<pre className="bg-white border rounded p-3 text-xs whitespace-pre-wrap max-h-80 overflow-auto">
|
||||
{row.original.responseBody || 'Keine Details erfasst.'}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-4 text-sm text-gray-700 gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Zeilen pro Seite:</span>
|
||||
<select
|
||||
className="border rounded px-2 py-1 text-sm"
|
||||
value={pagination.pageSize}
|
||||
onChange={(event) => table.setPageSize(Number(event.target.value))}
|
||||
>
|
||||
{[25, 50, 100, 200].map((size) => (
|
||||
<option key={size} value={size}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="px-3 py-1 border rounded disabled:opacity-50"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-1 border rounded disabled:opacity-50"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
Zurück
|
||||
</button>
|
||||
<span>
|
||||
Seite {table.getState().pagination.pageIndex + 1} von {table.getPageCount() || 1}
|
||||
</span>
|
||||
<button
|
||||
className="px-3 py-1 border rounded disabled:opacity-50"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
Weiter
|
||||
</button>
|
||||
<button
|
||||
className="px-3 py-1 border rounded disabled:opacity-50"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DebugPage;
|
||||
@@ -9,6 +9,7 @@ const NavigationTabs = ({ isAdmin, onProtectedNavigate }) => {
|
||||
{ to: '/store-watch', label: 'Betriebs-Monitoring' }
|
||||
];
|
||||
if (isAdmin) {
|
||||
tabs.push({ to: '/debug', label: 'Debug' });
|
||||
tabs.push({ to: '/admin', label: 'Admin' });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user