cloudflare-next-intl 0.9.6 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/check_dynamic_pages.mjs +24 -0
- package/dist/src/client/components/client_provider.js +1 -1
- package/dist/src/client/components/client_provider_static.js +1 -1
- package/dist/src/client/components/locale_link.d.ts +1 -1
- package/dist/src/client/components/locale_link_client.js +1 -1
- package/dist/src/client/hooks/use_path_name.js +1 -1
- package/dist/src/cloudflare_email/escape_html.d.ts +1 -0
- package/dist/src/cloudflare_email/escape_html.js +7 -0
- package/dist/src/cloudflare_email/index.d.ts +3 -0
- package/dist/src/cloudflare_email/index.js +3 -0
- package/dist/src/cloudflare_email/resolve_email_binding.d.ts +11 -0
- package/dist/src/cloudflare_email/resolve_email_binding.js +8 -0
- package/dist/src/cloudflare_email/send_transactional_email.d.ts +18 -0
- package/dist/src/cloudflare_email/send_transactional_email.js +36 -0
- package/dist/src/cloudflare_fetch/fetch_text.d.ts +5 -0
- package/dist/src/cloudflare_fetch/fetch_text.js +17 -0
- package/dist/src/cloudflare_fetch/fetch_with_fallback.d.ts +2 -0
- package/dist/src/cloudflare_fetch/fetch_with_fallback.js +7 -0
- package/dist/src/cloudflare_fetch/index.d.ts +3 -0
- package/dist/src/cloudflare_fetch/index.js +3 -0
- package/dist/src/cloudflare_fetch/resolve_assets_binding.d.ts +5 -0
- package/dist/src/cloudflare_fetch/resolve_assets_binding.js +9 -0
- package/dist/src/config/middleware.d.ts +2 -2
- package/dist/src/config/middleware.js +1 -1
- package/dist/src/cookie_consent/client/components/auto_analytics_events.js +1 -1
- package/dist/src/cookie_consent/client/components/cookie_consent_analytics.js +1 -1
- package/dist/src/cookie_consent/client/components/default_privacy_policy_link.js +1 -1
- package/dist/src/cookie_consent/client/cookie_consent_provider.js +1 -1
- package/dist/src/db/connection.js +15 -5
- package/dist/src/db/context.js +2 -2
- package/dist/src/db/resolve_hyperdrive_connection_string.d.ts +5 -0
- package/dist/src/db/resolve_hyperdrive_connection_string.js +10 -0
- package/dist/src/db/resolve_mode.d.ts +2 -2
- package/dist/src/db/resolve_mode.js +7 -1
- package/dist/src/dynamic_pages_check/check_dynamic_pages.d.ts +16 -0
- package/dist/src/dynamic_pages_check/check_dynamic_pages.js +38 -0
- package/dist/src/dynamic_pages_check/detect_dynamic_usage.d.ts +5 -0
- package/dist/src/dynamic_pages_check/detect_dynamic_usage.js +16 -0
- package/dist/src/dynamic_pages_check/find_page_files.d.ts +1 -0
- package/dist/src/dynamic_pages_check/find_page_files.js +24 -0
- package/dist/src/dynamic_pages_check/index.d.ts +3 -0
- package/dist/src/dynamic_pages_check/index.js +3 -0
- package/dist/src/dynamic_pages_check/insert_dynamic_export.d.ts +1 -0
- package/dist/src/dynamic_pages_check/insert_dynamic_export.js +43 -0
- package/dist/src/error_handling/create_server_error_action.js +1 -1
- package/dist/src/errors_board/client/error_detail_view.d.ts +7 -0
- package/dist/src/errors_board/client/error_detail_view.js +35 -0
- package/dist/src/errors_board/client/error_row.d.ts +7 -0
- package/dist/src/errors_board/client/error_row.js +9 -0
- package/dist/src/errors_board/client/error_ui_client.d.ts +14 -0
- package/dist/src/errors_board/client/error_ui_client.js +26 -0
- package/dist/src/errors_board/client/errors_filter_form.d.ts +8 -0
- package/dist/src/errors_board/client/errors_filter_form.js +6 -0
- package/dist/src/errors_board/client/errors_list_client.d.ts +15 -0
- package/dist/src/errors_board/client/errors_list_client.js +92 -0
- package/dist/src/errors_board/client/errors_login_form.d.ts +5 -0
- package/dist/src/errors_board/client/errors_login_form.js +21 -0
- package/dist/src/errors_board/client/errors_stat_strip.d.ts +6 -0
- package/dist/src/errors_board/client/errors_stat_strip.js +19 -0
- package/dist/src/errors_board/server/actions_factory.d.ts +24 -0
- package/dist/src/errors_board/server/actions_factory.js +39 -0
- package/dist/src/errors_board/server/errors_repository.d.ts +83 -0
- package/dist/src/errors_board/server/errors_repository.js +199 -0
- package/dist/src/errors_board/server/gate.d.ts +19 -0
- package/dist/src/errors_board/server/gate.js +68 -0
- package/dist/src/errors_board/server/index.d.ts +3 -0
- package/dist/src/errors_board/server/index.js +3 -0
- package/dist/src/errors_board/shared/error_ui_helpers.d.ts +13 -0
- package/dist/src/errors_board/shared/error_ui_helpers.js +60 -0
- package/dist/src/firebase_auth/client/auth_user_provider.js +1 -1
- package/dist/src/firebase_auth/middleware/update_session.d.ts +1 -1
- package/dist/src/firebase_auth/middleware/update_session.js +1 -1
- package/dist/src/firebase_auth/server/auth_user_server_provider.js +3 -3
- package/dist/src/firebase_auth/server/clear_session_action.js +1 -1
- package/dist/src/firebase_auth/server/firebase_server.js +1 -1
- package/dist/src/image_optimizer/next_image_shim.d.ts +2 -2
- package/dist/src/image_optimizer/next_image_shim.js +1 -1
- package/dist/src/server/components/link.d.ts +1 -1
- package/dist/src/server/components/link.js +1 -1
- package/dist/src/server/components/server_provider.js +2 -2
- package/dist/src/server/components/server_provider_static.js +2 -2
- package/dist/src/server/functions/geo.js +2 -2
- package/dist/src/server/functions/server.js +2 -2
- package/dist/src/types/types.d.ts +2 -1
- package/package.json +47 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { STATUS_BADGE_CLASS, STATUS_LABELS, STATUS_HINTS, formatRelativeTime, formatLocalTimestamp, parseRequestContext, } from '../shared/error_ui_helpers.js';
|
|
5
|
+
import { DetailBlock, CopyButton, LocalTime } from './error_ui_client.js';
|
|
6
|
+
export default function ErrorDetailView({ row, actions, onDeleted, }) {
|
|
7
|
+
const [isPending, setIsPending] = useState(false);
|
|
8
|
+
async function handleStatusChange(status) {
|
|
9
|
+
setIsPending(true);
|
|
10
|
+
try {
|
|
11
|
+
await actions.setErrorStatus([row.id], status);
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
setIsPending(false);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async function handleDelete() {
|
|
18
|
+
if (!window.confirm("Delete this error? This can't be undone."))
|
|
19
|
+
return;
|
|
20
|
+
setIsPending(true);
|
|
21
|
+
try {
|
|
22
|
+
await actions.deleteErrors([row.id]);
|
|
23
|
+
onDeleted();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
setIsPending(false);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const requestContext = parseRequestContext(row.params);
|
|
30
|
+
return (_jsxs("div", { className: "flex flex-col gap-5", style: { opacity: isPending ? 0.5 : 1 }, children: [_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: `rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide ${STATUS_BADGE_CLASS[row.status]}`, children: row.status }), _jsx("span", { className: "rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600 dark:bg-gray-800 dark:text-gray-300", children: row.flavour }), _jsx("span", { className: "rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600 dark:bg-gray-800 dark:text-gray-300", children: row.is_client === 1 ? 'Client' : 'Server' }), row.count > 1 && (_jsxs("span", { className: "rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-semibold text-gray-500 dark:bg-gray-800 dark:text-gray-400", children: ["Seen \u00D7", row.count] })), _jsx(CopyButton, { text: typeof window !== 'undefined' ? window.location.href : String(row.id), label: "Copy link", copiedLabel: "Link copied" })] }), _jsx("h1", { className: "font-mono text-lg font-semibold wrap-break-word text-gray-900 dark:text-white", children: row.caller }), _jsx("p", { className: "text-sm text-gray-600 dark:text-gray-300", children: row.message })] }), _jsxs("div", { className: "flex w-full shrink-0 flex-col gap-2 sm:w-auto sm:items-end", children: [_jsxs("div", { className: "flex w-full flex-col items-stretch gap-2 sm:w-auto sm:flex-row sm:items-center", children: [_jsx("div", { className: "flex overflow-hidden rounded-lg border border-gray-300 dark:border-gray-700", children: Object.keys(STATUS_LABELS).map((status, index) => (_jsx("button", { disabled: isPending || row.status === status, onClick: () => handleStatusChange(status), className: `flex-1 px-3 py-1.5 text-xs font-medium transition-colors disabled:cursor-default sm:flex-none ${index > 0 ? 'border-l border-gray-300 dark:border-gray-700' : ''} ${row.status === status
|
|
31
|
+
? 'bg-gray-900 text-white dark:bg-white dark:text-gray-900'
|
|
32
|
+
: 'bg-white text-gray-700 hover:bg-gray-100 dark:bg-gray-900 dark:text-gray-200 dark:hover:bg-gray-800'}`, children: STATUS_LABELS[status] }, status))) }), _jsx("button", { disabled: isPending, onClick: handleDelete, className: "rounded-lg border border-red-300 px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 disabled:opacity-40 dark:border-red-900 dark:text-red-400 dark:hover:bg-red-950", children: "Delete error" })] }), _jsx("p", { className: "max-w-72 text-[11px] leading-snug text-gray-400 sm:text-right dark:text-gray-500", children: STATUS_HINTS[row.status] })] })] }), _jsxs("dl", { className: "grid grid-cols-2 gap-x-4 gap-y-3 rounded-xl border border-gray-200 bg-gray-50 p-4 text-xs sm:grid-cols-3 dark:border-gray-800 dark:bg-gray-900/60", children: [_jsxs("div", { children: [_jsx("dt", { className: "text-gray-400 dark:text-gray-500", children: "First seen" }), _jsx("dd", { className: "text-gray-700 dark:text-gray-300", children: _jsx(LocalTime, { format: formatLocalTimestamp, timestampMs: row.created_at }) })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-gray-400 dark:text-gray-500", children: "Last seen" }), _jsxs("dd", { className: "text-gray-700 dark:text-gray-300", children: [_jsx(LocalTime, { format: formatLocalTimestamp, timestampMs: row.updated_at }), ' ', _jsxs("span", { className: "text-gray-400 dark:text-gray-500", children: ["(", _jsx(LocalTime, { format: formatRelativeTime, timestampMs: row.updated_at }), ")"] })] })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-gray-400 dark:text-gray-500", children: "User" }), _jsx("dd", { className: "text-gray-700 dark:text-gray-300", children: row.user_email ?? 'Unknown / not signed in' })] }), _jsxs("div", { children: [_jsx("dt", { className: "text-gray-400 dark:text-gray-500", children: "Regressions" }), _jsx("dd", { className: "text-gray-700 dark:text-gray-300", children: row.reopen_count > 0
|
|
33
|
+
? `Came back ${row.reopen_count} time${row.reopen_count === 1 ? '' : 's'} after being resolved`
|
|
34
|
+
: 'Never came back after a fix' })] }), row.resolved_at !== null && (_jsxs("div", { children: [_jsx("dt", { className: "text-gray-400 dark:text-gray-500", children: "Resolved" }), _jsx("dd", { className: "text-gray-700 dark:text-gray-300", children: _jsx(LocalTime, { format: formatRelativeTime, timestampMs: row.resolved_at }) })] })), requestContext?.path && (_jsxs("div", { children: [_jsx("dt", { className: "text-gray-400 dark:text-gray-500", children: "Page" }), _jsx("dd", { className: "break-all text-gray-700 dark:text-gray-300", children: requestContext.path })] })), requestContext?.referer && (_jsxs("div", { children: [_jsx("dt", { className: "text-gray-400 dark:text-gray-500", children: "Referrer" }), _jsx("dd", { className: "break-all text-gray-700 dark:text-gray-300", children: requestContext.referer })] })), requestContext?.userAgent && (_jsxs("div", { className: "col-span-2 sm:col-span-3", children: [_jsx("dt", { className: "text-gray-400 dark:text-gray-500", children: "User agent" }), _jsx("dd", { className: "break-all text-gray-700 dark:text-gray-300", children: requestContext.userAgent })] }))] }), _jsx(DetailBlock, { label: "Message", text: row.message }), row.stack && _jsx(DetailBlock, { label: "Stack trace", text: row.stack }), row.params && _jsx(DetailBlock, { label: "Params", text: row.params })] }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ErrorRow } from '../server/errors_repository.js';
|
|
2
|
+
export default function ErrorRowItem({ row, selected, onToggleSelect, hrefFor, }: {
|
|
3
|
+
row: ErrorRow;
|
|
4
|
+
selected: boolean;
|
|
5
|
+
onToggleSelect: (id: number, checked: boolean) => void;
|
|
6
|
+
hrefFor: (id: number) => string;
|
|
7
|
+
}): Component;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { STATUS_BADGE_CLASS, STATUS_DOT_CLASS, formatRelativeTime, formatLocalTimestamp } from '../shared/error_ui_helpers.js';
|
|
4
|
+
import { LocalTime, useMounted } from './error_ui_client.js';
|
|
5
|
+
export default function ErrorRowItem({ row, selected, onToggleSelect, hrefFor, }) {
|
|
6
|
+
const mounted = useMounted();
|
|
7
|
+
const absoluteTime = mounted ? formatLocalTimestamp(row.updated_at) : undefined;
|
|
8
|
+
return (_jsxs("a", { href: hrefFor(row.id), className: "flex flex-col gap-1.5 rounded-xl border border-gray-200 bg-white px-4 py-3 shadow-sm transition-colors hover:bg-gray-50 dark:border-gray-800 dark:bg-gray-900 dark:hover:bg-gray-800/50", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: `size-2 shrink-0 rounded-full ${STATUS_DOT_CLASS[row.status]}`, "aria-hidden": true }), _jsx("input", { type: "checkbox", checked: selected, onClick: (event) => event.stopPropagation(), onChange: (event) => onToggleSelect(row.id, event.target.checked), className: "size-4 shrink-0 accent-blue-600" }), _jsx("span", { className: `shrink-0 rounded-full px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide ${STATUS_BADGE_CLASS[row.status]}`, children: row.status }), _jsx("span", { className: "shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-medium text-gray-600 dark:bg-gray-800 dark:text-gray-300", children: row.flavour }), row.is_client === 1 && (_jsx("span", { className: "shrink-0 rounded-full bg-blue-50 px-2 py-0.5 text-[11px] font-medium text-blue-600 dark:bg-blue-500/10 dark:text-blue-300", children: "client" })), row.count > 1 && (_jsxs("span", { className: "shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-[11px] font-semibold text-gray-500 dark:bg-gray-800 dark:text-gray-400", title: `Seen ${row.count} times`, children: ["\u00D7", row.count] })), row.reopen_count > 0 && (_jsxs("span", { className: "shrink-0 rounded-full bg-orange-50 px-2 py-0.5 text-[11px] font-semibold text-orange-700 dark:bg-orange-500/10 dark:text-orange-300", title: `Came back ${row.reopen_count} time${row.reopen_count === 1 ? '' : 's'} after being resolved`, children: ["\u21A9 ", row.reopen_count] })), _jsx("span", { className: "ml-auto shrink-0 text-xs text-gray-400 dark:text-gray-500", title: absoluteTime, children: _jsx(LocalTime, { format: formatRelativeTime, timestampMs: row.updated_at }) })] }), _jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-0.5 pl-4 sm:flex-nowrap", children: [_jsx("span", { className: "shrink-0 font-mono text-xs font-semibold text-gray-800 dark:text-gray-100", children: row.caller }), _jsx("span", { className: "min-w-0 flex-1 truncate text-sm text-gray-500 dark:text-gray-400", children: row.message }), row.user_email && (_jsx("span", { className: "shrink-0 truncate text-xs text-gray-400 sm:ml-auto sm:max-w-[40%] dark:text-gray-500", children: row.user_email }))] })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function useMounted(): boolean;
|
|
2
|
+
export declare function LocalTime({ format, timestampMs }: {
|
|
3
|
+
format: (timestampMs: number) => string;
|
|
4
|
+
timestampMs: number;
|
|
5
|
+
}): Component;
|
|
6
|
+
export declare function CopyButton({ text, label, copiedLabel, }: {
|
|
7
|
+
text: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
copiedLabel?: string;
|
|
10
|
+
}): Component;
|
|
11
|
+
export declare function DetailBlock({ label, text }: {
|
|
12
|
+
label: string;
|
|
13
|
+
text: string;
|
|
14
|
+
}): Component;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
export function useMounted() {
|
|
5
|
+
const [mounted, setMounted] = useState(false);
|
|
6
|
+
useEffect(() => setMounted(true), []);
|
|
7
|
+
return mounted;
|
|
8
|
+
}
|
|
9
|
+
export function LocalTime({ format, timestampMs }) {
|
|
10
|
+
const mounted = useMounted();
|
|
11
|
+
return _jsx("span", { suppressHydrationWarning: true, children: mounted ? format(timestampMs) : '' });
|
|
12
|
+
}
|
|
13
|
+
export function CopyButton({ text, label = 'Copy', copiedLabel = 'Copied', }) {
|
|
14
|
+
const [copied, setCopied] = useState(false);
|
|
15
|
+
function handleCopy(event) {
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
void navigator.clipboard.writeText(text).then(() => {
|
|
18
|
+
setCopied(true);
|
|
19
|
+
setTimeout(() => setCopied(false), 1500);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return (_jsx("button", { type: "button", onClick: handleCopy, className: "rounded-md border border-gray-300 px-2 py-1 text-[11px] font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200", children: copied ? copiedLabel : label }));
|
|
23
|
+
}
|
|
24
|
+
export function DetailBlock({ label, text }) {
|
|
25
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-lg border border-gray-200 dark:border-gray-800", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-200 bg-gray-50 px-3 py-1.5 dark:border-gray-800 dark:bg-gray-950/60", children: [_jsx("span", { className: "text-[11px] font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500", children: label }), _jsx(CopyButton, { text: text })] }), _jsx("pre", { className: "max-h-72 overflow-auto whitespace-pre-wrap break-words p-3 font-mono text-xs leading-relaxed text-gray-700 dark:bg-gray-950 dark:text-gray-300", children: text })] }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
const FIELD_CLASS = 'rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 transition-colors focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white';
|
|
4
|
+
export default function ErrorsFilterForm({ flavours, filters, }) {
|
|
5
|
+
return (_jsxs("form", { className: "flex flex-wrap items-end gap-3 rounded-xl border border-gray-200 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-900/60", children: [_jsx("input", { type: "hidden", name: "status", value: filters.status }), _jsxs("label", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "text-[11px] font-medium uppercase tracking-wide text-gray-400 dark:text-gray-500", children: "Flavour" }), _jsxs("select", { name: "flavour", defaultValue: filters.flavour, className: FIELD_CLASS, children: [_jsx("option", { value: "all", children: "All flavours" }), flavours.map((flavour) => (_jsx("option", { value: flavour, children: flavour }, flavour)))] }, filters.flavour)] }), _jsxs("label", { className: "flex min-w-48 flex-1 flex-col gap-1", children: [_jsx("span", { className: "text-[11px] font-medium uppercase tracking-wide text-gray-400 dark:text-gray-500", children: "Search" }), _jsx("input", { type: "text", name: "q", placeholder: "Message, caller, or user email", defaultValue: filters.q, className: FIELD_CLASS }, filters.q)] }), _jsx("button", { type: "submit", className: "rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700", children: "Apply" })] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ErrorRow } from '../server/errors_repository.js';
|
|
2
|
+
import type { ErrorsActions } from '../server/actions_factory.js';
|
|
3
|
+
interface Filters {
|
|
4
|
+
flavour: string;
|
|
5
|
+
status: string;
|
|
6
|
+
q: string;
|
|
7
|
+
}
|
|
8
|
+
export default function ErrorsListClient({ initialRows, initialNextCursor, filters, actions, hrefFor, }: {
|
|
9
|
+
initialRows: ErrorRow[];
|
|
10
|
+
initialNextCursor: string | null;
|
|
11
|
+
filters: Filters;
|
|
12
|
+
actions: ErrorsActions;
|
|
13
|
+
hrefFor: (id: number) => string;
|
|
14
|
+
}): Component;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import ErrorRowItem from './error_row.js';
|
|
5
|
+
export default function ErrorsListClient({ initialRows, initialNextCursor, filters, actions, hrefFor, }) {
|
|
6
|
+
const [rows, setRows] = useState(initialRows);
|
|
7
|
+
const [nextCursor, setNextCursor] = useState(initialNextCursor);
|
|
8
|
+
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
9
|
+
const [selectedIds, setSelectedIds] = useState(new Set());
|
|
10
|
+
const [isPending, setIsPending] = useState(false);
|
|
11
|
+
const sentinelRef = useRef(null);
|
|
12
|
+
const latestRef = useRef({ isLoadingMore, nextCursor, filters, actions });
|
|
13
|
+
latestRef.current = { isLoadingMore, nextCursor, filters, actions };
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
setRows(initialRows);
|
|
16
|
+
setNextCursor(initialNextCursor);
|
|
17
|
+
}, [initialRows, initialNextCursor]);
|
|
18
|
+
const loadMore = useCallback(() => {
|
|
19
|
+
const { isLoadingMore, nextCursor, filters, actions } = latestRef.current;
|
|
20
|
+
if (isLoadingMore || nextCursor === null)
|
|
21
|
+
return;
|
|
22
|
+
setIsLoadingMore(true);
|
|
23
|
+
void actions
|
|
24
|
+
.loadErrors({ ...filters, cursor: nextCursor })
|
|
25
|
+
.then((result) => {
|
|
26
|
+
setRows((previous) => [...previous, ...result.rows]);
|
|
27
|
+
setNextCursor(result.nextCursor);
|
|
28
|
+
})
|
|
29
|
+
.finally(() => setIsLoadingMore(false));
|
|
30
|
+
}, []);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const sentinel = sentinelRef.current;
|
|
33
|
+
if (!sentinel)
|
|
34
|
+
return;
|
|
35
|
+
const observer = new IntersectionObserver((entries) => {
|
|
36
|
+
if (entries[0]?.isIntersecting)
|
|
37
|
+
loadMore();
|
|
38
|
+
}, { rootMargin: '400px' });
|
|
39
|
+
observer.observe(sentinel);
|
|
40
|
+
return () => observer.disconnect();
|
|
41
|
+
}, [loadMore]);
|
|
42
|
+
function toggleSelect(id, checked) {
|
|
43
|
+
setSelectedIds((previous) => {
|
|
44
|
+
const next = new Set(previous);
|
|
45
|
+
if (checked)
|
|
46
|
+
next.add(id);
|
|
47
|
+
else
|
|
48
|
+
next.delete(id);
|
|
49
|
+
return next;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function toggleSelectAll(checked) {
|
|
53
|
+
setSelectedIds(checked ? new Set(rows.map((row) => row.id)) : new Set());
|
|
54
|
+
}
|
|
55
|
+
async function handleBulkStatus(status) {
|
|
56
|
+
const ids = Array.from(selectedIds);
|
|
57
|
+
setIsPending(true);
|
|
58
|
+
try {
|
|
59
|
+
await actions.setErrorStatus(ids, status);
|
|
60
|
+
setSelectedIds(new Set());
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
setIsPending(false);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function handleBulkDelete() {
|
|
67
|
+
const ids = Array.from(selectedIds);
|
|
68
|
+
if (!window.confirm(`Delete ${ids.length} error${ids.length === 1 ? '' : 's'}? This can't be undone.`))
|
|
69
|
+
return;
|
|
70
|
+
setIsPending(true);
|
|
71
|
+
try {
|
|
72
|
+
await actions.deleteErrors(ids);
|
|
73
|
+
setSelectedIds(new Set());
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
setIsPending(false);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function handleDeleteAllResolved() {
|
|
80
|
+
if (!window.confirm("Delete every resolved error (including ones not currently loaded)? This can't be undone."))
|
|
81
|
+
return;
|
|
82
|
+
setIsPending(true);
|
|
83
|
+
try {
|
|
84
|
+
await actions.deleteAllResolved();
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
setIsPending(false);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const hasSelection = selectedIds.size > 0;
|
|
91
|
+
return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "sticky top-2 z-10 flex flex-wrap items-center gap-2 rounded-xl border border-gray-200 bg-white/90 px-3 py-2 shadow-sm backdrop-blur dark:border-gray-800 dark:bg-gray-900/90", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300", children: [_jsx("input", { type: "checkbox", checked: rows.length > 0 && selectedIds.size === rows.length, onChange: (event) => toggleSelectAll(event.target.checked), className: "size-4 accent-blue-600" }), hasSelection ? (_jsxs("span", { className: "font-medium text-gray-900 dark:text-white", children: [selectedIds.size, " selected"] })) : (_jsxs("span", { children: [rows.length, " error", rows.length === 1 ? '' : 's'] }))] }), _jsxs("div", { className: "ml-auto flex flex-wrap gap-2", children: [_jsx("button", { disabled: isPending || !hasSelection, onClick: () => handleBulkStatus('investigating'), className: "rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-40 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800", children: "Mark investigating" }), _jsx("button", { disabled: isPending || !hasSelection, onClick: () => handleBulkStatus('resolved'), className: "rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-40 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800", children: "Mark resolved" }), filters.status === 'muted' ? (_jsx("button", { disabled: isPending || !hasSelection, onClick: () => handleBulkStatus('new'), title: "Bring these back onto the board as New.", className: "rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-40 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800", children: "Unmute" })) : (_jsx("button", { disabled: isPending || !hasSelection, onClick: () => handleBulkStatus('muted'), title: "Hide for good. Repeats stay hidden and never reopen.", className: "rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-40 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800", children: "Mute" })), _jsx("button", { disabled: isPending || !hasSelection, onClick: handleBulkDelete, className: "rounded-lg border border-red-300 px-3 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 disabled:opacity-40 dark:border-red-900 dark:text-red-400 dark:hover:bg-red-950", children: "Delete selected" }), _jsx("button", { disabled: isPending, onClick: handleDeleteAllResolved, className: "rounded-lg border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-40 dark:border-gray-700 dark:text-gray-200 dark:hover:bg-gray-800", children: "Delete all resolved" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [rows.map((row) => (_jsx(ErrorRowItem, { row: row, selected: selectedIds.has(row.id), onToggleSelect: toggleSelect, hrefFor: hrefFor }, row.id))), rows.length === 0 && (_jsxs("div", { className: "flex flex-col items-center gap-1 rounded-xl border border-dashed border-gray-300 py-14 text-center dark:border-gray-700", children: [_jsx("p", { className: "text-sm font-medium text-gray-600 dark:text-gray-300", children: "No errors here" }), _jsx("p", { className: "text-xs text-gray-400 dark:text-gray-500", children: "Nothing matches the current filters." })] }))] }), nextCursor !== null && (_jsx("div", { ref: sentinelRef, className: "flex justify-center py-4", children: isLoadingMore && (_jsxs("span", { className: "flex items-center gap-2 text-sm text-gray-400 dark:text-gray-500", children: [_jsx("span", { className: "size-3 animate-spin rounded-full border-2 border-gray-300 border-t-transparent dark:border-gray-600" }), "Loading more\u2026"] })) })), nextCursor === null && rows.length > 0 && (_jsx("p", { className: "py-4 text-center text-xs text-gray-400 dark:text-gray-500", children: "You've reached the end." }))] }));
|
|
92
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useTransition } from 'react';
|
|
4
|
+
export default function ErrorsLoginForm({ login, onSuccess, title = 'Error log', }) {
|
|
5
|
+
const [password, setPassword] = useState('');
|
|
6
|
+
const [error, setError] = useState(null);
|
|
7
|
+
const [isPending, startTransition] = useTransition();
|
|
8
|
+
function handleSubmit(event) {
|
|
9
|
+
event.preventDefault();
|
|
10
|
+
setError(null);
|
|
11
|
+
startTransition(async () => {
|
|
12
|
+
const ok = await login(password);
|
|
13
|
+
if (!ok) {
|
|
14
|
+
setError('Wrong password');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
onSuccess();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return (_jsxs("main", { className: "mx-auto flex min-h-screen max-w-sm flex-col justify-center gap-4 px-4", children: [_jsx("h1", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: title }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-2", children: [_jsx("input", { type: "password", autoFocus: true, value: password, onChange: (event) => setPassword(event.target.value), placeholder: "Password", className: "rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white" }), error && _jsx("p", { className: "text-xs text-red-600 dark:text-red-400", children: error }), _jsx("button", { type: "submit", disabled: isPending, className: "rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700 disabled:opacity-50", children: "Enter" })] })] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import Link from 'next/link.js';
|
|
3
|
+
const STAT_CONFIG = [
|
|
4
|
+
{ status: 'all', label: 'Total', dotClass: 'bg-blue-500' },
|
|
5
|
+
{ status: 'new', label: 'New', dotClass: 'bg-red-500' },
|
|
6
|
+
{ status: 'investigating', label: 'Investigating', dotClass: 'bg-amber-500' },
|
|
7
|
+
{ status: 'resolved', label: 'Resolved', dotClass: 'bg-emerald-500' },
|
|
8
|
+
{ status: 'muted', label: 'Muted', dotClass: 'bg-gray-400 dark:bg-gray-500' },
|
|
9
|
+
];
|
|
10
|
+
export default function ErrorsStatStrip({ counts, activeStatus, linkFor, }) {
|
|
11
|
+
const total = counts.new + counts.investigating + counts.resolved;
|
|
12
|
+
return (_jsx("div", { className: "grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-5", children: STAT_CONFIG.map(({ status, label, dotClass }) => {
|
|
13
|
+
const value = status === 'all' ? total : counts[status];
|
|
14
|
+
const isActive = activeStatus === status;
|
|
15
|
+
return (_jsxs(Link, { prefetch: false, href: linkFor(status), className: `flex items-center justify-between rounded-xl border px-3 py-2.5 transition-colors ${isActive
|
|
16
|
+
? 'border-blue-400 bg-blue-50 dark:border-blue-500/50 dark:bg-blue-500/10'
|
|
17
|
+
: 'border-gray-200 bg-white hover:bg-gray-50 dark:border-gray-800 dark:bg-gray-900 dark:hover:bg-gray-800/50'}`, children: [_jsxs("span", { className: "flex items-center gap-2 text-xs font-medium text-gray-500 dark:text-gray-400", children: [_jsx("span", { className: `size-2 rounded-full ${dotClass}`, "aria-hidden": true }), label] }), _jsx("span", { className: "text-lg font-semibold tabular-nums text-gray-900 dark:text-white", children: value })] }, status));
|
|
18
|
+
}) }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type D1DatabaseLike, type ErrorsListResult, listErrors as listErrorsImpl, setErrorsStatus as setErrorsStatusImpl, deleteErrorsByIds as deleteErrorsByIdsImpl, deleteAllResolvedErrors as deleteAllResolvedErrorsImpl } from './errors_repository.js';
|
|
2
|
+
export interface ErrorsActions {
|
|
3
|
+
loadErrors(rawParams: {
|
|
4
|
+
flavour?: string;
|
|
5
|
+
status?: string;
|
|
6
|
+
q?: string;
|
|
7
|
+
cursor?: string | null;
|
|
8
|
+
}): Promise<ErrorsListResult>;
|
|
9
|
+
setErrorStatus(ids: number[], status: string): Promise<void>;
|
|
10
|
+
deleteErrors(ids: number[]): Promise<void>;
|
|
11
|
+
deleteAllResolved(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export interface ErrorsActionsOptions {
|
|
14
|
+
getDb: () => Promise<D1DatabaseLike> | D1DatabaseLike;
|
|
15
|
+
requireAccess: () => Promise<void>;
|
|
16
|
+
listPath?: string;
|
|
17
|
+
repository?: {
|
|
18
|
+
listErrors?: typeof listErrorsImpl;
|
|
19
|
+
setErrorsStatus?: typeof setErrorsStatusImpl;
|
|
20
|
+
deleteErrorsByIds?: typeof deleteErrorsByIdsImpl;
|
|
21
|
+
deleteAllResolvedErrors?: typeof deleteAllResolvedErrorsImpl;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare function createErrorsActions(options: ErrorsActionsOptions): ErrorsActions;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { revalidatePath } from 'next/cache.js';
|
|
2
|
+
import { parseErrorsListFilters, boundErrorIds, isErrorStatus, listErrors as listErrorsImpl, setErrorsStatus as setErrorsStatusImpl, deleteErrorsByIds as deleteErrorsByIdsImpl, deleteAllResolvedErrors as deleteAllResolvedErrorsImpl, } from './errors_repository.js';
|
|
3
|
+
export function createErrorsActions(options) {
|
|
4
|
+
const listErrors = options.repository?.listErrors ?? listErrorsImpl;
|
|
5
|
+
const setErrorsStatus = options.repository?.setErrorsStatus ?? setErrorsStatusImpl;
|
|
6
|
+
const deleteErrorsByIds = options.repository?.deleteErrorsByIds ?? deleteErrorsByIdsImpl;
|
|
7
|
+
const deleteAllResolvedErrors = options.repository?.deleteAllResolvedErrors ?? deleteAllResolvedErrorsImpl;
|
|
8
|
+
const listPath = options.listPath ?? '/errors';
|
|
9
|
+
return {
|
|
10
|
+
async loadErrors(rawParams) {
|
|
11
|
+
await options.requireAccess();
|
|
12
|
+
const filters = parseErrorsListFilters(rawParams);
|
|
13
|
+
const db = await options.getDb();
|
|
14
|
+
return listErrors(db, filters);
|
|
15
|
+
},
|
|
16
|
+
async setErrorStatus(ids, status) {
|
|
17
|
+
await options.requireAccess();
|
|
18
|
+
if (!isErrorStatus(status))
|
|
19
|
+
throw new Error(`errors_board: unknown status "${status}"`);
|
|
20
|
+
const boundedIds = boundErrorIds(ids);
|
|
21
|
+
const db = await options.getDb();
|
|
22
|
+
await setErrorsStatus(db, boundedIds, status);
|
|
23
|
+
revalidatePath(listPath);
|
|
24
|
+
},
|
|
25
|
+
async deleteErrors(ids) {
|
|
26
|
+
await options.requireAccess();
|
|
27
|
+
const boundedIds = boundErrorIds(ids);
|
|
28
|
+
const db = await options.getDb();
|
|
29
|
+
await deleteErrorsByIds(db, boundedIds);
|
|
30
|
+
revalidatePath(listPath);
|
|
31
|
+
},
|
|
32
|
+
async deleteAllResolved() {
|
|
33
|
+
await options.requireAccess();
|
|
34
|
+
const db = await options.getDb();
|
|
35
|
+
await deleteAllResolvedErrors(db);
|
|
36
|
+
revalidatePath(listPath);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export declare const ERROR_STATUSES: readonly ["new", "investigating", "resolved", "muted"];
|
|
2
|
+
export type ErrorStatus = typeof ERROR_STATUSES[number];
|
|
3
|
+
export declare const BOARD_STATUSES: readonly ["new", "investigating", "resolved"];
|
|
4
|
+
declare const MAX_MESSAGE_LENGTH = 2000;
|
|
5
|
+
declare const MAX_STACK_LENGTH = 8000;
|
|
6
|
+
declare const MAX_PARAMS_LENGTH = 4000;
|
|
7
|
+
export declare const ERRORS_PAGE_SIZE = 50;
|
|
8
|
+
export declare const MAX_IDS_PER_ACTION = 200;
|
|
9
|
+
export interface ErrorRow {
|
|
10
|
+
id: number;
|
|
11
|
+
fingerprint: string;
|
|
12
|
+
created_at: number;
|
|
13
|
+
updated_at: number;
|
|
14
|
+
flavour: string;
|
|
15
|
+
caller: string;
|
|
16
|
+
message: string;
|
|
17
|
+
stack: string | null;
|
|
18
|
+
params: string | null;
|
|
19
|
+
is_client: number;
|
|
20
|
+
status: ErrorStatus;
|
|
21
|
+
count: number;
|
|
22
|
+
user_email: string | null;
|
|
23
|
+
reopen_count: number;
|
|
24
|
+
resolved_at: number | null;
|
|
25
|
+
}
|
|
26
|
+
export interface D1PreparedStatementLike {
|
|
27
|
+
bind(...values: unknown[]): D1PreparedStatementLike;
|
|
28
|
+
run(): Promise<unknown>;
|
|
29
|
+
all<T = unknown>(): Promise<{
|
|
30
|
+
results?: T[];
|
|
31
|
+
}>;
|
|
32
|
+
first<T = unknown>(): Promise<T | null>;
|
|
33
|
+
}
|
|
34
|
+
export interface D1DatabaseLike {
|
|
35
|
+
prepare(sql: string): D1PreparedStatementLike;
|
|
36
|
+
batch<T = unknown>(statements: D1PreparedStatementLike[]): Promise<{
|
|
37
|
+
results?: T[];
|
|
38
|
+
}[]>;
|
|
39
|
+
}
|
|
40
|
+
export interface RecordErrorInput {
|
|
41
|
+
flavour: string;
|
|
42
|
+
caller: string;
|
|
43
|
+
message: string;
|
|
44
|
+
stack: string | null;
|
|
45
|
+
params: string | null;
|
|
46
|
+
isClient: boolean;
|
|
47
|
+
userEmail: string | null;
|
|
48
|
+
}
|
|
49
|
+
export interface ErrorsListFilters {
|
|
50
|
+
flavour: string;
|
|
51
|
+
status: ErrorStatus | 'all';
|
|
52
|
+
q: string;
|
|
53
|
+
cursor: string | null;
|
|
54
|
+
}
|
|
55
|
+
export interface ErrorsListResult {
|
|
56
|
+
rows: ErrorRow[];
|
|
57
|
+
nextCursor: string | null;
|
|
58
|
+
}
|
|
59
|
+
export interface ErrorsBoardResult extends ErrorsListResult {
|
|
60
|
+
flavours: string[];
|
|
61
|
+
counts: Record<ErrorStatus, number>;
|
|
62
|
+
}
|
|
63
|
+
export declare function isErrorStatus(value: string): value is ErrorStatus;
|
|
64
|
+
export declare function encodeCursor(updatedAt: number, id: number): string;
|
|
65
|
+
export declare function parseErrorsListFilters(raw: {
|
|
66
|
+
flavour?: string;
|
|
67
|
+
status?: string;
|
|
68
|
+
q?: string;
|
|
69
|
+
cursor?: string | null;
|
|
70
|
+
}): ErrorsListFilters;
|
|
71
|
+
export declare function boundErrorIds(ids: number[]): number[];
|
|
72
|
+
declare function truncate(value: string, maxLength: number): string;
|
|
73
|
+
export { MAX_MESSAGE_LENGTH, MAX_STACK_LENGTH, MAX_PARAMS_LENGTH, truncate };
|
|
74
|
+
export declare function ensureSchema(db: D1DatabaseLike): Promise<void>;
|
|
75
|
+
export declare function computeFingerprint(flavour: string, caller: string, message: string): Promise<string>;
|
|
76
|
+
export declare function recordError(db: D1DatabaseLike, input: RecordErrorInput): Promise<void>;
|
|
77
|
+
export declare function listErrors(db: D1DatabaseLike, filters: ErrorsListFilters): Promise<ErrorsListResult>;
|
|
78
|
+
export declare function getErrorById(db: D1DatabaseLike, id: number): Promise<ErrorRow | null>;
|
|
79
|
+
export declare function distinctFlavours(db: D1DatabaseLike): Promise<string[]>;
|
|
80
|
+
export declare function loadErrorsBoard(db: D1DatabaseLike, filters: ErrorsListFilters): Promise<ErrorsBoardResult>;
|
|
81
|
+
export declare function setErrorsStatus(db: D1DatabaseLike, ids: number[], status: ErrorStatus): Promise<void>;
|
|
82
|
+
export declare function deleteErrorsByIds(db: D1DatabaseLike, ids: number[]): Promise<void>;
|
|
83
|
+
export declare function deleteAllResolvedErrors(db: D1DatabaseLike): Promise<void>;
|