Make table page size a configurable setting, not a hardcoded 20
Pagination just landed hardcoded to 20 rows everywhere; add a way to change that instead of leaving it fixed for every table in the app. pageSize joins dateFormat/timeFormat on the existing display settings object (server-side default 20, 5-500 range enforced by the PUT schema) rather than becoming its own settings section, since it's the same kind of thing -- an admin-configured, globally-applied display preference read by every signed-in role via the already-public GET /api/settings/display endpoint, same as the date/time format already works. Renamed the "Date & Time" settings tab/page/route to "Display" (still just one component, now covering both date/time format and table pagination) since its scope no longer matches the old name -- kept Settings.tsx's usual pattern of one page per concern rather than adding a second, oddly-scoped tab just for one number field. New web/src/utils/pageSize.ts mirrors utils/date.ts's existing module-level "set once at startup, read anywhere without prop- drilling" pattern; usePagination()'s pageSize parameter now defaults to getPageSize() instead of a literal 20, evaluated fresh on every call so it picks up a saved change without touching any of the ten pages already using the hook. Verified server-side against a temp SQLite DB: pageSize defaults to 20, a partial update sets it without disturbing dateFormat/timeFormat and vice versa, and it persists across a fresh settings read. Also checked the default-parameter mechanics directly (re-evaluates the global value on every call rather than capturing it once, and an explicit override still wins). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7bf9b03839
commit
b40234a557
10 files changed
+180
-123
No files matched your search
@@ -1,110 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api, type AppSettings, type DateFormat, type TimeFormat } from "../../api/client";
|
||||
import { setDateTimeSettings } from "../../utils/date";
|
||||
|
||||
const DATE_FORMAT_OPTIONS: { value: DateFormat; label: string; example: string }[] = [
|
||||
{ value: "ymd", label: "Year-Month-Day", example: "2026-09-15" },
|
||||
{ value: "dmy", label: "Day/Month/Year", example: "15/09/2026" },
|
||||
{ value: "mdy", label: "Month/Day/Year", example: "09/15/2026" },
|
||||
];
|
||||
|
||||
function previewTime(timeFormat: TimeFormat): string {
|
||||
return timeFormat === "12h" ? "2:30:00 PM" : "14:30:00";
|
||||
}
|
||||
|
||||
export default function DateTimeSettings() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [dateFormat, setDateFormatState] = useState<DateFormat>("ymd");
|
||||
const [timeFormat, setTimeFormatState] = useState<TimeFormat>("24h");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api.settings
|
||||
.get()
|
||||
.then((res: { settings: AppSettings }) => {
|
||||
setDateFormatState(res.settings.display.dateFormat);
|
||||
setTimeFormatState(res.settings.display.timeFormat);
|
||||
})
|
||||
.catch((err) => setLoadError(err instanceof Error ? err.message : String(err)))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
async function handleSave() {
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
setSaved(false);
|
||||
try {
|
||||
const res = await api.settings.update({ display: { dateFormat, timeFormat } });
|
||||
setDateTimeSettings(res.settings.display);
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 3000);
|
||||
} catch (err) {
|
||||
setSaveError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="text-secondary">Loading…</div>;
|
||||
|
||||
const dateExample = DATE_FORMAT_OPTIONS.find((o) => o.value === dateFormat)?.example ?? "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 className="mb-0">Date & Time</h3>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
{saveError && <span className="text-danger small">{saveError}</span>}
|
||||
{saved && <span className="text-success small">✓ Settings saved</span>}
|
||||
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
|
||||
{saving ? "Saving…" : "Save Settings"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loadError && <div className="alert alert-danger">{loadError}</div>}
|
||||
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Display format</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p className="text-secondary">
|
||||
Controls how dates and times are shown throughout the app — tables previously mixed formats depending on
|
||||
the page; this makes them consistent everywhere.
|
||||
</p>
|
||||
<div className="row g-3">
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Date format</label>
|
||||
<select className="form-select" value={dateFormat} onChange={(e) => setDateFormatState(e.target.value as DateFormat)}>
|
||||
{DATE_FORMAT_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label} ({opt.example})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Clock</label>
|
||||
<select className="form-select" value={timeFormat} onChange={(e) => setTimeFormatState(e.target.value as TimeFormat)}>
|
||||
<option value="24h">24-hour (14:30:00)</option>
|
||||
<option value="12h">12-hour (2:30:00 PM)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<div className="text-secondary small">Preview</div>
|
||||
<div>
|
||||
<code>
|
||||
{dateExample} {previewTime(timeFormat)}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api, type AppSettings, type DateFormat, type TimeFormat } from "../../api/client";
|
||||
import { setDateTimeSettings } from "../../utils/date";
|
||||
import { setPageSize } from "../../utils/pageSize";
|
||||
|
||||
const DATE_FORMAT_OPTIONS: { value: DateFormat; label: string; example: string }[] = [
|
||||
{ value: "ymd", label: "Year-Month-Day", example: "2026-09-15" },
|
||||
{ value: "dmy", label: "Day/Month/Year", example: "15/09/2026" },
|
||||
{ value: "mdy", label: "Month/Day/Year", example: "09/15/2026" },
|
||||
];
|
||||
|
||||
function previewTime(timeFormat: TimeFormat): string {
|
||||
return timeFormat === "12h" ? "2:30:00 PM" : "14:30:00";
|
||||
}
|
||||
|
||||
export default function DisplaySettings() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
const [dateFormat, setDateFormatState] = useState<DateFormat>("ymd");
|
||||
const [timeFormat, setTimeFormatState] = useState<TimeFormat>("24h");
|
||||
const [pageSize, setPageSizeState] = useState(20);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api.settings
|
||||
.get()
|
||||
.then((res: { settings: AppSettings }) => {
|
||||
setDateFormatState(res.settings.display.dateFormat);
|
||||
setTimeFormatState(res.settings.display.timeFormat);
|
||||
setPageSizeState(res.settings.display.pageSize);
|
||||
})
|
||||
.catch((err) => setLoadError(err instanceof Error ? err.message : String(err)))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
async function handleSave() {
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
setSaved(false);
|
||||
try {
|
||||
const res = await api.settings.update({ display: { dateFormat, timeFormat, pageSize } });
|
||||
setDateTimeSettings(res.settings.display);
|
||||
setPageSize(res.settings.display.pageSize);
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 3000);
|
||||
} catch (err) {
|
||||
setSaveError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) return <div className="text-secondary">Loading…</div>;
|
||||
|
||||
const dateExample = DATE_FORMAT_OPTIONS.find((o) => o.value === dateFormat)?.example ?? "";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex align-items-center justify-content-between mb-3">
|
||||
<h3 className="mb-0">Display</h3>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
{saveError && <span className="text-danger small">{saveError}</span>}
|
||||
{saved && <span className="text-success small">✓ Settings saved</span>}
|
||||
<button className="btn btn-primary" onClick={handleSave} disabled={saving}>
|
||||
{saving ? "Saving…" : "Save Settings"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loadError && <div className="alert alert-danger">{loadError}</div>}
|
||||
|
||||
<div className="row row-cards">
|
||||
<div className="col-md-7">
|
||||
<div className="card h-100">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Date & time format</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p className="text-secondary">
|
||||
Controls how dates and times are shown throughout the app — tables previously mixed formats
|
||||
depending on the page; this makes them consistent everywhere.
|
||||
</p>
|
||||
<div className="row g-3">
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Date format</label>
|
||||
<select className="form-select" value={dateFormat} onChange={(e) => setDateFormatState(e.target.value as DateFormat)}>
|
||||
{DATE_FORMAT_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label} ({opt.example})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Clock</label>
|
||||
<select className="form-select" value={timeFormat} onChange={(e) => setTimeFormatState(e.target.value as TimeFormat)}>
|
||||
<option value="24h">24-hour (14:30:00)</option>
|
||||
<option value="12h">12-hour (2:30:00 PM)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<div className="text-secondary small">Preview</div>
|
||||
<div>
|
||||
<code>
|
||||
{dateExample} {previewTime(timeFormat)}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-md-5">
|
||||
<div className="card h-100">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">Table pagination</h3>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<p className="text-secondary">
|
||||
How many rows a paginated table (Secrets, IP Addresses, DNS records, Servers, Audit Log, and each
|
||||
integration's device/guest/repo/template list) shows per page.
|
||||
</p>
|
||||
<label className="form-label">Rows per page</label>
|
||||
<input
|
||||
type="number"
|
||||
className="form-control"
|
||||
min={5}
|
||||
max={500}
|
||||
value={pageSize}
|
||||
onChange={(e) => setPageSizeState(Number(e.target.value))}
|
||||
/>
|
||||
<div className="form-hint">Between 5 and 500.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user