@rapidmx/web-client 0.3.1 → 0.5.0
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/apps/admin/audit-log/index.tsx +35 -8
- package/apps/admin/branding/index.tsx +39 -404
- package/apps/admin/data-requests/index.tsx +480 -485
- package/apps/admin/distribution-lists/[uid].tsx +19 -1
- package/apps/admin/domains/[uid].tsx +92 -259
- package/apps/admin/encryption-policy/index.tsx +19 -0
- package/apps/admin/escrow-scopes/[uid].tsx +348 -237
- package/apps/admin/escrow-scopes/new/index.tsx +8 -2
- package/apps/admin/index.tsx +118 -82
- package/apps/admin/ingest-queue/index.tsx +44 -5
- package/apps/admin/mailbox-policy/index.tsx +19 -0
- package/apps/admin/mailboxes/[uid].tsx +211 -184
- package/apps/admin/mailboxes/new/index.tsx +28 -291
- package/apps/admin/plugins/index.tsx +15 -0
- package/apps/admin/quarantine/index.tsx +46 -14
- package/apps/admin/retention-policy/index.tsx +39 -132
- package/apps/admin/setup/index.tsx +15 -0
- package/apps/admin/transport-rules/[uid].tsx +30 -1
- package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
- package/apps/admin/transport-rules/index.tsx +24 -6
- package/apps/admin/transport-rules/new/index.tsx +30 -1
- package/apps/escrow/_layout.tsx +2 -3
- package/apps/escrow/audit-log/index.tsx +196 -168
- package/apps/escrow/matters/[uid].tsx +621 -521
- package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
- package/apps/shared/components/admin/layout/AdminShell.tsx +251 -210
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
- package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
- package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
- package/apps/shared/components/admin/settings/BrandingForm.tsx +385 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +187 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +112 -0
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +308 -0
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +152 -0
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -0
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -0
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +290 -0
- package/apps/shared/components/admin/setup/SetupWizard.tsx +416 -0
- package/apps/shared/components/admin/signOut.ts +79 -0
- package/apps/shared/components/admin/usePagedList.tsx +129 -0
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
- package/apps/shared/components/calendar/EventModal.tsx +717 -513
- package/apps/shared/components/calendar/MonthView.tsx +173 -139
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
- package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
- package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
- package/apps/shared/components/calendar/allDay.ts +124 -0
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
- package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
- package/apps/shared/components/contacts/ContactForm.tsx +377 -314
- package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
- package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
- package/apps/shared/components/contacts/contactKeys.ts +78 -0
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
- package/apps/shared/components/layout/AppShell.tsx +270 -169
- package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +150 -26
- package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
- package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -0
- package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
- package/apps/shared/components/mail/MessageDetailPane.tsx +799 -76
- package/apps/shared/components/mail/compose/ComposeContext.tsx +26 -16
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1652 -765
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +418 -302
- package/apps/shared/components/mail/pinnedSigners.ts +124 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -0
- package/apps/shared/components/mail/writableMailboxes.ts +101 -0
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
- package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
- package/apps/shared/mail/listAllPages.ts +39 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +98 -0
- package/apps/shared/search/localIndexBlockCipher.ts +83 -0
- package/apps/shared/search/localIndexBuilder.ts +481 -0
- package/apps/shared/search/localIndexKey.ts +27 -0
- package/apps/shared/search/localIndexRpcClient.ts +316 -0
- package/apps/shared/search/localIndexSchema.ts +236 -0
- package/apps/shared/search/localIndexSizePreference.ts +88 -0
- package/apps/shared/search/localIndexStorage.ts +98 -0
- package/apps/shared/search/localIndexVFS.ts +266 -0
- package/apps/shared/search/localIndexWorker.ts +926 -0
- package/apps/shared/search/searchTier2.ts +79 -0
- package/apps/shared/search/wa-sqlite-shims.d.ts +44 -0
- package/apps/www/calendar/index.tsx +438 -407
- package/apps/www/contacts/[uid].tsx +21 -2
- package/apps/www/contacts/index.tsx +148 -17
- package/apps/www/index.tsx +1314 -486
- package/apps/www/messages/[uid].tsx +23 -7
- package/apps/www/settings/auto-reply/index.tsx +134 -129
- package/apps/www/settings/booking-types/[uid].tsx +306 -300
- package/apps/www/settings/encryption/index.tsx +800 -215
- package/apps/www/settings/filters/[uid].tsx +171 -155
- package/apps/www/settings/filters/new/index.tsx +8 -1
- package/apps/www/settings/focused-inbox/index.tsx +150 -148
- package/apps/www/settings/privacy/index.tsx +494 -436
- package/apps/www/settings/read-receipts/index.tsx +148 -125
- package/apps/www/settings/sharing/index.tsx +274 -0
- package/apps/www/tasks/index.tsx +638 -530
- package/dist/apps/admin/audit-log/index.js +38 -8
- package/dist/apps/admin/branding/index.js +4 -98
- package/dist/apps/admin/data-requests/index.js +52 -72
- package/dist/apps/admin/distribution-lists/[uid].js +1 -1
- package/dist/apps/admin/domains/[uid].js +5 -68
- package/dist/apps/admin/encryption-policy/index.js +8 -0
- package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
- package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
- package/dist/apps/admin/index.js +17 -1
- package/dist/apps/admin/ingest-queue/index.js +24 -6
- package/dist/apps/admin/mailbox-policy/index.js +8 -0
- package/dist/apps/admin/mailboxes/[uid].js +20 -4
- package/dist/apps/admin/mailboxes/new/index.js +4 -89
- package/dist/apps/admin/plugins/index.js +6 -0
- package/dist/apps/admin/quarantine/index.js +20 -11
- package/dist/apps/admin/retention-policy/index.js +3 -36
- package/dist/apps/admin/setup/index.js +6 -0
- package/dist/apps/admin/transport-rules/[uid].js +18 -2
- package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
- package/dist/apps/admin/transport-rules/index.js +22 -6
- package/dist/apps/admin/transport-rules/new/index.js +18 -2
- package/dist/apps/escrow/_layout.js +3 -3
- package/dist/apps/escrow/audit-log/index.js +26 -1
- package/dist/apps/escrow/matters/[uid].js +77 -42
- package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
- package/dist/apps/shared/components/admin/layout/AdminShell.js +37 -4
- package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
- package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
- package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +116 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +83 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +62 -0
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +112 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +74 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +572 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +73 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +147 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +208 -0
- package/dist/apps/shared/components/admin/signOut.js +75 -0
- package/dist/apps/shared/components/admin/usePagedList.js +99 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
- package/dist/apps/shared/components/calendar/EventModal.js +154 -21
- package/dist/apps/shared/components/calendar/MonthView.js +12 -5
- package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
- package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
- package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
- package/dist/apps/shared/components/calendar/allDay.js +111 -0
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
- package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
- package/dist/apps/shared/components/contacts/ContactForm.js +45 -12
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
- package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
- package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
- package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
- package/dist/apps/shared/components/layout/AppShell.js +100 -8
- package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +63 -14
- package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
- package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +136 -0
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
- package/dist/apps/shared/components/mail/MessageDetailPane.js +464 -34
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +807 -116
- package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +116 -41
- package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
- package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
- package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
- package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
- package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
- package/dist/apps/shared/mail/listAllPages.js +25 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +80 -0
- package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
- package/dist/apps/shared/search/localIndexBuilder.js +408 -0
- package/dist/apps/shared/search/localIndexKey.js +25 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +241 -0
- package/dist/apps/shared/search/localIndexSchema.js +185 -0
- package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
- package/dist/apps/shared/search/localIndexStorage.js +86 -0
- package/dist/apps/shared/search/localIndexVFS.js +258 -0
- package/dist/apps/shared/search/localIndexWorker.js +687 -0
- package/dist/apps/shared/search/searchTier2.js +38 -0
- package/dist/apps/www/calendar/index.js +38 -18
- package/dist/apps/www/contacts/[uid].js +13 -2
- package/dist/apps/www/contacts/index.js +94 -15
- package/dist/apps/www/index.js +770 -137
- package/dist/apps/www/messages/[uid].js +23 -7
- package/dist/apps/www/settings/auto-reply/index.js +9 -4
- package/dist/apps/www/settings/booking-types/[uid].js +17 -12
- package/dist/apps/www/settings/encryption/index.js +541 -96
- package/dist/apps/www/settings/filters/[uid].js +22 -7
- package/dist/apps/www/settings/filters/new/index.js +8 -2
- package/dist/apps/www/settings/focused-inbox/index.js +3 -1
- package/dist/apps/www/settings/privacy/index.js +57 -22
- package/dist/apps/www/settings/read-receipts/index.js +10 -3
- package/dist/apps/www/settings/sharing/index.js +124 -0
- package/dist/apps/www/tasks/index.js +76 -14
- package/package.json +3 -2
|
@@ -3,13 +3,15 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { useEffect, useState } from "react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
8
|
import { listAuditLog } from "@rapidmx/react-shared/admin/auditLogApi.js";
|
|
9
9
|
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
10
10
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
11
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
12
|
const PAGE_SIZE = 25;
|
|
13
|
+
/** How long typing in a filter box has to pause before the log is re-fetched. */
|
|
14
|
+
export const FILTER_DEBOUNCE_MS = 300;
|
|
13
15
|
const FILTER_KEYS = ["mailboxUid", "actorUserUid", "action", "targetType"];
|
|
14
16
|
/** Seeds the filter form from the query string on first render — lets a link (e.g. a future "View audit
|
|
15
17
|
* log" deep link from a mailbox's detail page) land here pre-filtered, same convention as `quarantine`'s
|
|
@@ -32,26 +34,54 @@ export default function AuditLogPage(props) {
|
|
|
32
34
|
return (_jsx(AdminShell, { ...props, active: "auditLog", children: _jsx(AuditLogContent, {}) }));
|
|
33
35
|
}
|
|
34
36
|
function AuditLogContent() {
|
|
37
|
+
// `filters` is what the boxes show; `queryFilters` is what was last fetched - it follows `filters` once typing
|
|
38
|
+
// pauses, and stays `null` until the URL's filters are read so no unfiltered fetch goes out first.
|
|
35
39
|
const [filters, setFilters] = useState({});
|
|
40
|
+
const [queryFilters, setQueryFilters] = useState(null);
|
|
36
41
|
const [page, setPage] = useState(0);
|
|
37
42
|
const [entries, setEntries] = useState([]);
|
|
38
43
|
const [loading, setLoading] = useState(true);
|
|
39
44
|
const [error, setError] = useState(null);
|
|
45
|
+
const debounceTimer = useRef(undefined);
|
|
40
46
|
useEffect(() => {
|
|
41
|
-
|
|
47
|
+
const initial = readFiltersFromUrl();
|
|
48
|
+
setFilters(initial);
|
|
49
|
+
setQueryFilters(initial);
|
|
50
|
+
return () => clearTimeout(debounceTimer.current);
|
|
42
51
|
}, []);
|
|
43
52
|
useEffect(() => {
|
|
53
|
+
if (!queryFilters) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// Only the latest filters/page's response is applied, so a slow earlier fetch can't overwrite it.
|
|
57
|
+
let cancelled = false;
|
|
44
58
|
setLoading(true);
|
|
45
59
|
setError(null);
|
|
46
|
-
listAuditLog(
|
|
47
|
-
.then(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
listAuditLog(queryFilters, { page, limit: PAGE_SIZE })
|
|
61
|
+
.then((data) => {
|
|
62
|
+
if (!cancelled)
|
|
63
|
+
setEntries(data);
|
|
64
|
+
})
|
|
65
|
+
.catch((err) => {
|
|
66
|
+
if (!cancelled)
|
|
67
|
+
setError(err instanceof ApiRequestError ? err.message : "Could not load the audit log.");
|
|
68
|
+
})
|
|
69
|
+
.finally(() => {
|
|
70
|
+
if (!cancelled)
|
|
71
|
+
setLoading(false);
|
|
72
|
+
});
|
|
73
|
+
return () => {
|
|
74
|
+
cancelled = true;
|
|
75
|
+
};
|
|
76
|
+
}, [queryFilters, page]);
|
|
51
77
|
function updateFilter(key, value) {
|
|
52
78
|
const next = { ...filters, [key]: value || undefined };
|
|
53
79
|
setFilters(next);
|
|
54
|
-
|
|
80
|
+
clearTimeout(debounceTimer.current);
|
|
81
|
+
debounceTimer.current = setTimeout(() => {
|
|
82
|
+
setQueryFilters(next);
|
|
83
|
+
setPage(0);
|
|
84
|
+
}, FILTER_DEBOUNCE_MS);
|
|
55
85
|
const params = new URLSearchParams();
|
|
56
86
|
for (const k of FILTER_KEYS) {
|
|
57
87
|
const v = next[k];
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
///////////////////////////////////////////////////////////////////////////////
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import { useEffect,
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getBranding } from "@rapidmx/react-shared/branding/brandingApi.js";
|
|
9
9
|
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
10
|
+
import BrandingForm from "../../shared/components/admin/settings/BrandingForm.js";
|
|
10
11
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
|
-
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
|
-
const INPUT_CLASS = "w-full text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
|
|
13
|
-
const MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
14
12
|
export default function BrandingPage(props) {
|
|
15
13
|
return (_jsx(AdminShell, { ...props, active: "branding", children: _jsx(BrandingContent, {}) }));
|
|
16
14
|
}
|
|
@@ -32,95 +30,3 @@ function BrandingContent() {
|
|
|
32
30
|
}
|
|
33
31
|
return _jsx(BrandingForm, { branding: branding, onChange: setBranding });
|
|
34
32
|
}
|
|
35
|
-
function BrandingForm({ branding, onChange }) {
|
|
36
|
-
const [companyName, setCompanyName] = useState(branding.companyName);
|
|
37
|
-
const [title, setTitle] = useState(branding.title);
|
|
38
|
-
const [headerHtml, setHeaderHtml] = useState(branding.headerHtml ?? "");
|
|
39
|
-
const [footerHtml, setFooterHtml] = useState(branding.footerHtml ?? "");
|
|
40
|
-
const [logoUrlInput, setLogoUrlInput] = useState(branding.logoUrl ?? "");
|
|
41
|
-
const [iconUrlInput, setIconUrlInput] = useState(branding.iconUrl ?? "");
|
|
42
|
-
const [stylesheetUrlInput, setStylesheetUrlInput] = useState(branding.stylesheetUrl ?? "");
|
|
43
|
-
const [error, setError] = useState(null);
|
|
44
|
-
const [saving, setSaving] = useState(false);
|
|
45
|
-
const [saved, setSaved] = useState(false);
|
|
46
|
-
const [assetBusy, setAssetBusy] = useState(null);
|
|
47
|
-
const logoFileRef = useRef(null);
|
|
48
|
-
const iconFileRef = useRef(null);
|
|
49
|
-
const stylesheetFileRef = useRef(null);
|
|
50
|
-
function runAsset(name, action) {
|
|
51
|
-
setError(null);
|
|
52
|
-
setAssetBusy(name);
|
|
53
|
-
return action()
|
|
54
|
-
.then((updated) => {
|
|
55
|
-
onChange(updated);
|
|
56
|
-
setLogoUrlInput(updated.logoUrl ?? "");
|
|
57
|
-
setIconUrlInput(updated.iconUrl ?? "");
|
|
58
|
-
setStylesheetUrlInput(updated.stylesheetUrl ?? "");
|
|
59
|
-
})
|
|
60
|
-
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not update branding."))
|
|
61
|
-
.finally(() => setAssetBusy(null));
|
|
62
|
-
}
|
|
63
|
-
function handleLogoFileChange(e) {
|
|
64
|
-
const file = e.target.files?.[0];
|
|
65
|
-
if (!file)
|
|
66
|
-
return;
|
|
67
|
-
if (file.size > MAX_UPLOAD_BYTES) {
|
|
68
|
-
setError(`"${file.name}" is too large — logos must be 5MB or smaller.`);
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
void runAsset("logo-upload", () => uploadBrandingLogo(file));
|
|
72
|
-
}
|
|
73
|
-
e.target.value = "";
|
|
74
|
-
}
|
|
75
|
-
function handleIconFileChange(e) {
|
|
76
|
-
const file = e.target.files?.[0];
|
|
77
|
-
if (!file)
|
|
78
|
-
return;
|
|
79
|
-
if (file.size > MAX_UPLOAD_BYTES) {
|
|
80
|
-
setError(`"${file.name}" is too large — icons must be 5MB or smaller.`);
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
void runAsset("icon-upload", () => uploadBrandingIcon(file));
|
|
84
|
-
}
|
|
85
|
-
e.target.value = "";
|
|
86
|
-
}
|
|
87
|
-
function handleStylesheetFileChange(e) {
|
|
88
|
-
const file = e.target.files?.[0];
|
|
89
|
-
if (!file)
|
|
90
|
-
return;
|
|
91
|
-
if (file.size > MAX_UPLOAD_BYTES) {
|
|
92
|
-
setError(`"${file.name}" is too large — stylesheets must be 5MB or smaller.`);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
void runAsset("stylesheet-upload", () => uploadBrandingStylesheet(file));
|
|
96
|
-
}
|
|
97
|
-
e.target.value = "";
|
|
98
|
-
}
|
|
99
|
-
async function handleSubmit(e) {
|
|
100
|
-
e.preventDefault();
|
|
101
|
-
setError(null);
|
|
102
|
-
setSaved(false);
|
|
103
|
-
setSaving(true);
|
|
104
|
-
try {
|
|
105
|
-
const updated = await updateBranding({ companyName, title, headerHtml, footerHtml });
|
|
106
|
-
onChange(updated);
|
|
107
|
-
setSaved(true);
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
setError(err instanceof ApiRequestError ? err.message : "Could not save branding.");
|
|
111
|
-
}
|
|
112
|
-
finally {
|
|
113
|
-
setSaving(false);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return (_jsxs("div", { className: "max-w-2xl", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Branding" }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Customize the logo, nav-header icon, product name, and chrome shown to every visitor of the webmail and admin console \u2014 including anonymous booking-page visitors." }), error && _jsx(Alert, { children: error }), saved && !error && _jsx("div", { className: "mb-4 text-sm text-success font-medium", children: "Saved." }), _jsxs("div", { className: "flex flex-col gap-6", children: [_jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Logo" }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.logoUrl || "/images/logo.svg", alt: "Current logo", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "logo-upload", disabled: assetBusy !== null, onClick: () => logoFileRef.current?.click(), children: "Upload logo" }), _jsx("input", { ref: logoFileRef, type: "file", accept: "image/*", "aria-label": "Upload logo", className: "hidden", onChange: handleLogoFileChange }), branding.logoUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "logo-delete", onClick: () => void runAsset("logo-delete", async () => {
|
|
117
|
-
await deleteBrandingLogo();
|
|
118
|
-
return { ...branding, logoUrl: undefined };
|
|
119
|
-
}), children: "Remove logo" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Logo URL", className: INPUT_CLASS, value: logoUrlInput, onChange: (e) => setLogoUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || logoUrlInput === (branding.logoUrl ?? ""), loading: assetBusy === "logo-url", onClick: () => void runAsset("logo-url", () => updateBranding({ logoUrl: logoUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Icon" }), _jsx("p", { className: "text-xs text-text-muted -mt-1", children: "A compact mark for navigation headers, independent of the full logo above. Falls back to the logo, then a default asset, when not set." }), _jsxs("div", { className: "flex items-center gap-4", children: [_jsx("img", { src: branding.iconUrl || branding.logoUrl || "/images/logo.svg", alt: "Current icon", width: "64", height: "64", className: "border border-border rounded-sm bg-surface-alt p-1" }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "icon-upload", disabled: assetBusy !== null, onClick: () => iconFileRef.current?.click(), children: "Upload icon" }), _jsx("input", { ref: iconFileRef, type: "file", accept: "image/*", "aria-label": "Upload icon", className: "hidden", onChange: handleIconFileChange }), branding.iconUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "icon-delete", onClick: () => void runAsset("icon-delete", async () => {
|
|
120
|
-
await deleteBrandingIcon();
|
|
121
|
-
return { ...branding, iconUrl: undefined };
|
|
122
|
-
}), children: "Remove icon" }))] })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external image URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Icon URL", className: INPUT_CLASS, value: iconUrlInput, onChange: (e) => setIconUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || iconUrlInput === (branding.iconUrl ?? ""), loading: assetBusy === "icon-url", onClick: () => void runAsset("icon-url", () => updateBranding({ iconUrl: iconUrlInput })), children: "Set" })] })] })] }), _jsxs("section", { className: "flex flex-col gap-3", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted", children: "Custom stylesheet" }), _jsx("div", { className: "flex items-center gap-3", children: _jsx("span", { className: "text-sm text-text-muted truncate", children: branding.stylesheetUrl || "None configured" }) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: assetBusy === "stylesheet-upload", disabled: assetBusy !== null, onClick: () => stylesheetFileRef.current?.click(), children: "Upload CSS file" }), _jsx("input", { ref: stylesheetFileRef, type: "file", accept: "text/css,.css", "aria-label": "Upload stylesheet", className: "hidden", onChange: handleStylesheetFileChange }), branding.stylesheetUrl && (_jsx(Button, { type: "button", variant: "text", disabled: assetBusy !== null, loading: assetBusy === "stylesheet-delete", onClick: () => void runAsset("stylesheet-delete", async () => {
|
|
123
|
-
await deleteBrandingStylesheet();
|
|
124
|
-
return { ...branding, stylesheetUrl: undefined };
|
|
125
|
-
}), children: "Remove stylesheet" }))] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Or use an external stylesheet URL" }), _jsxs("div", { className: "flex gap-2", children: [_jsx("input", { "aria-label": "Stylesheet URL", className: INPUT_CLASS, value: stylesheetUrlInput, onChange: (e) => setStylesheetUrlInput(e.target.value) }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: assetBusy !== null || stylesheetUrlInput === (branding.stylesheetUrl ?? ""), loading: assetBusy === "stylesheet-url", onClick: () => void runAsset("stylesheet-url", () => updateBranding({ stylesheetUrl: stylesheetUrlInput })), children: "Set" })] })] })] }), _jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-4 pt-2 border-t border-border", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Company name" }), _jsx("input", { "aria-label": "Company name", className: INPUT_CLASS, value: companyName, onChange: (e) => setCompanyName(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Product title" }), _jsx("input", { "aria-label": "Product title", className: INPUT_CLASS, value: title, onChange: (e) => setTitle(e.target.value) }), _jsx("span", { className: "text-xs text-text-muted", children: "Shown as the browser tab title." })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Header HTML (optional)" }), _jsx("textarea", { "aria-label": "Header HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: headerHtml, onChange: (e) => setHeaderHtml(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Footer HTML (optional)" }), _jsx("textarea", { "aria-label": "Footer HTML", className: `${INPUT_CLASS} font-mono`, rows: 3, value: footerHtml, onChange: (e) => setFooterHtml(e.target.value) })] }), _jsx("div", { children: _jsx(Button, { type: "submit", loading: saving, disabled: saving, className: "!w-auto", children: "Save" }) })] })] })] }));
|
|
126
|
-
}
|
|
@@ -10,6 +10,7 @@ import { listImportRequests, uploadMailboxImport, } from "@rapidmx/react-shared/
|
|
|
10
10
|
import { approveErasureRequest, denyErasureRequest, listErasureRequests, } from "@rapidmx/react-shared/mail/erasureRequestApi.js";
|
|
11
11
|
import { listFolders } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
12
12
|
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
13
|
+
import { LoadMoreButton, usePagedList } from "../../shared/components/admin/usePagedList.js";
|
|
13
14
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
14
15
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
15
16
|
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
@@ -18,43 +19,33 @@ const NON_MAIL_FOLDER_TYPES = new Set(["calendar", "contacts", "tasks", "notes"]
|
|
|
18
19
|
function importFormatFromFilename(filename) {
|
|
19
20
|
return filename.toLowerCase().endsWith(".pst") ? "pst" : "mbox";
|
|
20
21
|
}
|
|
22
|
+
/** Request statuses still waiting or running - shown as a neutral in-progress badge, never as success. Includes
|
|
23
|
+
* restapi's `processing` (exports/imports) and `in_progress` (an erasure that is running). */
|
|
24
|
+
const IN_PROGRESS_STATUSES = new Set(["pending", "processing", "in_progress"]);
|
|
21
25
|
function statusBadgeClass(status) {
|
|
22
26
|
if (status === "failed" || status === "denied")
|
|
23
27
|
return "bg-danger-bg text-danger";
|
|
24
|
-
if (status
|
|
28
|
+
if (IN_PROGRESS_STATUSES.has(status))
|
|
25
29
|
return "bg-surface-alt text-text-muted";
|
|
26
30
|
return "bg-success text-white";
|
|
27
31
|
}
|
|
32
|
+
/** A status as shown on its badge (`in_progress` -> `in progress`). */
|
|
33
|
+
function statusLabel(status) {
|
|
34
|
+
return status.replace(/_/g, " ");
|
|
35
|
+
}
|
|
28
36
|
export default function DataRequestsPage(props) {
|
|
29
|
-
return (_jsx(AdminShell, { ...props, active: "dataRequests", children: _jsxs("div", { className: "flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Data Requests" }), _jsx("p", { className: "text-sm text-text-muted", children: "Every GDPR export, mailbox-import, and account-erasure request across every mailbox." })] }), _jsx(ExportRequestsSection, {}), _jsx(ImportRequestsSection, {}), _jsx(ErasureRequestsSection, {})] }) }));
|
|
37
|
+
return (_jsx(AdminShell, { ...props, active: "dataRequests", children: _jsxs("div", { className: "flex flex-col gap-8", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide mb-1", children: "Data Requests" }), _jsx("p", { className: "text-sm text-text-muted", children: "Every GDPR export, mailbox-import, and account-erasure request across every mailbox, newest first." })] }), _jsx(ExportRequestsSection, {}), _jsx(ImportRequestsSection, {}), _jsx(ErasureRequestsSection, {})] }) }));
|
|
30
38
|
}
|
|
31
39
|
function ExportRequestsSection() {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
40
|
+
// The create form below isn't gated behind `loading`, so a fast create-then-reload can resolve before the
|
|
41
|
+
// initial mount fetch does - `usePagedList()` only ever applies the most recently started load.
|
|
42
|
+
const list = usePagedList(listExportRequests, "Could not load export requests.");
|
|
35
43
|
const [mailboxUid, setMailboxUid] = useState("");
|
|
36
44
|
const [format, setFormat] = useState("json");
|
|
37
45
|
const [creating, setCreating] = useState(false);
|
|
38
46
|
const [createError, setCreateError] = useState(null);
|
|
39
|
-
// The create form below isn't gated behind `loading`, so a fast create-then-reload can resolve before
|
|
40
|
-
// the initial mount fetch does; without this guard the mount fetch's now-stale response would land
|
|
41
|
-
// last and silently revert the list. Only the most-recently-issued load's response is ever applied.
|
|
42
|
-
const loadSeq = useRef(0);
|
|
43
|
-
function loadRequests() {
|
|
44
|
-
const seq = ++loadSeq.current;
|
|
45
|
-
return listExportRequests()
|
|
46
|
-
.then((data) => {
|
|
47
|
-
if (seq === loadSeq.current)
|
|
48
|
-
setRequests(data);
|
|
49
|
-
})
|
|
50
|
-
.catch((err) => {
|
|
51
|
-
if (seq === loadSeq.current)
|
|
52
|
-
setLoadError(err instanceof ApiRequestError ? err.message : "Could not load export requests.");
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
47
|
useEffect(() => {
|
|
56
|
-
|
|
57
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
48
|
+
void list.reload();
|
|
58
49
|
}, []);
|
|
59
50
|
async function handleCreate(e) {
|
|
60
51
|
e.preventDefault();
|
|
@@ -63,7 +54,7 @@ function ExportRequestsSection() {
|
|
|
63
54
|
try {
|
|
64
55
|
await createExportRequest({ format, mailboxUid: mailboxUid.trim() });
|
|
65
56
|
setMailboxUid("");
|
|
66
|
-
await
|
|
57
|
+
await list.reload();
|
|
67
58
|
}
|
|
68
59
|
catch (err) {
|
|
69
60
|
setCreateError(err instanceof ApiRequestError ? err.message : "Could not start this export.");
|
|
@@ -72,12 +63,12 @@ function ExportRequestsSection() {
|
|
|
72
63
|
setCreating(false);
|
|
73
64
|
}
|
|
74
65
|
}
|
|
75
|
-
return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Export requests" }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-4 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Mailbox UID" }), _jsx("input", { "aria-label": "Export mailbox UID", className: INPUT_CLASS, value: mailboxUid, onChange: (e) => setMailboxUid(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Format" }), _jsxs("select", { "aria-label": "Export format", className: INPUT_CLASS, value: format, onChange: (e) => setFormat(e.target.value), children: [_jsx("option", { value: "json", children: "JSON" }), _jsx("option", { value: "mbox", children: "Mbox" })] })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: creating, disabled: creating || !mailboxUid.trim(), children: "Create export" })] }), createError && _jsx(Alert, { children: createError }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) :
|
|
66
|
+
return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Export requests" }), _jsxs("form", { onSubmit: handleCreate, className: "flex items-end gap-3 mb-4 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Mailbox UID" }), _jsx("input", { "aria-label": "Export mailbox UID", className: INPUT_CLASS, value: mailboxUid, onChange: (e) => setMailboxUid(e.target.value) })] }), _jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Format" }), _jsxs("select", { "aria-label": "Export format", className: INPUT_CLASS, value: format, onChange: (e) => setFormat(e.target.value), children: [_jsx("option", { value: "json", children: "JSON" }), _jsx("option", { value: "mbox", children: "Mbox" })] })] }), _jsx(Button, { type: "submit", variant: "secondary", className: "!w-auto", loading: creating, disabled: creating || !mailboxUid.trim(), children: "Create export" })] }), createError && _jsx(Alert, { children: createError }), list.loadError && _jsx(Alert, { children: list.loadError }), list.loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : list.items.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No export requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: list.items.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.mailboxUid, " \u00B7 ", request.format.toUpperCase(), " \u00B7", " ", new Date(request.dateCreated).toLocaleString(), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) }), request.status === "ready" && (_jsx("a", { href: exportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) })), _jsx(LoadMoreButton, { list: list, label: "Load more export requests" })] }));
|
|
76
67
|
}
|
|
77
68
|
function ImportRequestsSection() {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
69
|
+
// Same stale-response guard as ExportRequestsSection - the Upload button is enabled independently of this
|
|
70
|
+
// section's own `loading`.
|
|
71
|
+
const list = usePagedList(listImportRequests, "Could not load import requests.");
|
|
81
72
|
const [mailboxUid, setMailboxUid] = useState("");
|
|
82
73
|
const [folders, setFolders] = useState([]);
|
|
83
74
|
const [targetFolderUid, setTargetFolderUid] = useState("");
|
|
@@ -85,25 +76,8 @@ function ImportRequestsSection() {
|
|
|
85
76
|
const [uploading, setUploading] = useState(false);
|
|
86
77
|
const [uploadError, setUploadError] = useState(null);
|
|
87
78
|
const fileInputRef = useRef(null);
|
|
88
|
-
// Same stale-response guard as ExportRequestsSection's loadRequests() - the Upload button is enabled
|
|
89
|
-
// independently of this section's own `loading`, so a fast upload-then-reload can resolve before the
|
|
90
|
-
// initial mount fetch does.
|
|
91
|
-
const loadSeq = useRef(0);
|
|
92
|
-
function loadRequests() {
|
|
93
|
-
const seq = ++loadSeq.current;
|
|
94
|
-
return listImportRequests()
|
|
95
|
-
.then((data) => {
|
|
96
|
-
if (seq === loadSeq.current)
|
|
97
|
-
setRequests(data);
|
|
98
|
-
})
|
|
99
|
-
.catch((err) => {
|
|
100
|
-
if (seq === loadSeq.current)
|
|
101
|
-
setLoadError(err instanceof ApiRequestError ? err.message : "Could not load import requests.");
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
79
|
useEffect(() => {
|
|
105
|
-
|
|
106
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
+
void list.reload();
|
|
107
81
|
}, []);
|
|
108
82
|
function handleLoadFolders() {
|
|
109
83
|
const uid = mailboxUid.trim();
|
|
@@ -130,46 +104,48 @@ function ImportRequestsSection() {
|
|
|
130
104
|
setUploadError(null);
|
|
131
105
|
setUploading(true);
|
|
132
106
|
uploadMailboxImport(file, { format: importFormatFromFilename(file.name), targetFolderUid, mailboxUid: mailboxUid.trim() })
|
|
133
|
-
.then(() =>
|
|
107
|
+
.then(() => list.reload())
|
|
134
108
|
.catch((err) => setUploadError(err instanceof ApiRequestError ? err.message : "Could not upload this file."))
|
|
135
109
|
.finally(() => setUploading(false));
|
|
136
110
|
}
|
|
137
|
-
return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Import requests" }), _jsxs("div", { className: "flex items-end gap-3 mb-2 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Mailbox UID" }), _jsx("input", { "aria-label": "Import mailbox UID", className: INPUT_CLASS, value: mailboxUid, onChange: (e) => setMailboxUid(e.target.value), onBlur: handleLoadFolders })] }), folders.length > 0 && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Destination folder" }), _jsx("select", { "aria-label": "Import destination folder", className: INPUT_CLASS, value: targetFolderUid, onChange: (e) => setTargetFolderUid(e.target.value), children: folders.map((folder) => (_jsx("option", { value: folder.uid, children: folder.name }, folder.uid))) })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: uploading, disabled: uploading || !targetFolderUid, onClick: () => fileInputRef.current?.click(), children: "Upload Mbox or PST file" }), _jsx("input", { ref: fileInputRef, type: "file", accept: ".mbox,.pst", "aria-label": "Upload mail archive", className: "hidden", onChange: handleFileChange })] }), folderError && _jsx(Alert, { children: folderError }), uploadError && _jsx(Alert, { children: uploadError }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) :
|
|
111
|
+
return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Import requests" }), _jsxs("div", { className: "flex items-end gap-3 mb-2 flex-wrap", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Mailbox UID" }), _jsx("input", { "aria-label": "Import mailbox UID", className: INPUT_CLASS, value: mailboxUid, onChange: (e) => setMailboxUid(e.target.value), onBlur: handleLoadFolders })] }), folders.length > 0 && (_jsxs("label", { className: "flex flex-col gap-1.5 text-sm", children: [_jsx("span", { className: "font-semibold", children: "Destination folder" }), _jsx("select", { "aria-label": "Import destination folder", className: INPUT_CLASS, value: targetFolderUid, onChange: (e) => setTargetFolderUid(e.target.value), children: folders.map((folder) => (_jsx("option", { value: folder.uid, children: folder.name }, folder.uid))) })] })), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: uploading, disabled: uploading || !targetFolderUid, onClick: () => fileInputRef.current?.click(), children: "Upload Mbox or PST file" }), _jsx("input", { ref: fileInputRef, type: "file", accept: ".mbox,.pst", "aria-label": "Upload mail archive", className: "hidden", onChange: handleFileChange })] }), folderError && _jsx(Alert, { children: folderError }), uploadError && _jsx(Alert, { children: uploadError }), list.loadError && _jsx(Alert, { children: list.loadError }), list.loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : list.items.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No import requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: list.items.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [request.mailboxUid, " \u00B7 ", request.format.toUpperCase(), " \u00B7", " ", new Date(request.dateCreated).toLocaleString(), request.status === "completed" && (_jsxs("span", { className: "text-text-muted", children: [" ", "\u2014 ", request.importedCount ?? 0, " imported", request.failedCount ? `, ${request.failedCount} failed` : ""] })), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) })] }, request.uid))) })), _jsx(LoadMoreButton, { list: list, label: "Load more import requests" })] }));
|
|
138
112
|
}
|
|
139
113
|
function ErasureRequestsSection() {
|
|
140
|
-
const
|
|
141
|
-
const [
|
|
142
|
-
const [
|
|
143
|
-
const [
|
|
144
|
-
const [actingOn, setActingOn] = useState({});
|
|
114
|
+
const list = usePagedList(listErasureRequests, "Could not load erasure requests.");
|
|
115
|
+
const [approveTarget, setApproveTarget] = useState(null);
|
|
116
|
+
const [approving, setApproving] = useState(false);
|
|
117
|
+
const [approveError, setApproveError] = useState(null);
|
|
145
118
|
const [denyTarget, setDenyTarget] = useState(null);
|
|
146
119
|
const [denyReason, setDenyReason] = useState("");
|
|
147
120
|
const [denying, setDenying] = useState(false);
|
|
148
121
|
const [denyError, setDenyError] = useState(null);
|
|
149
|
-
function loadRequests() {
|
|
150
|
-
return listErasureRequests()
|
|
151
|
-
.then(setRequests)
|
|
152
|
-
.catch((err) => setLoadError(err instanceof ApiRequestError ? err.message : "Could not load erasure requests."));
|
|
153
|
-
}
|
|
154
122
|
useEffect(() => {
|
|
155
|
-
|
|
156
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
123
|
+
void list.reload();
|
|
157
124
|
}, []);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
125
|
+
function closeApproveModal() {
|
|
126
|
+
setApproveTarget(null);
|
|
127
|
+
setApproveError(null);
|
|
128
|
+
}
|
|
129
|
+
// Escape/Close are ignored while a decision is being sent, so its result (or error) can't land on a modal
|
|
130
|
+
// reopened for a different request.
|
|
131
|
+
function dismissApproveModal() {
|
|
132
|
+
if (!approving)
|
|
133
|
+
closeApproveModal();
|
|
134
|
+
}
|
|
135
|
+
async function handleApprove() {
|
|
136
|
+
setApproveError(null);
|
|
137
|
+
setApproving(true);
|
|
161
138
|
try {
|
|
162
|
-
|
|
163
|
-
await
|
|
139
|
+
// Only invoked from the approve modal below, which only renders once approveTarget is set.
|
|
140
|
+
await approveErasureRequest(approveTarget.uid);
|
|
141
|
+
closeApproveModal();
|
|
142
|
+
await list.reload();
|
|
164
143
|
}
|
|
165
144
|
catch (err) {
|
|
166
|
-
|
|
167
|
-
...prev,
|
|
168
|
-
[request.uid]: err instanceof ApiRequestError ? err.message : "Could not approve this request.",
|
|
169
|
-
}));
|
|
145
|
+
setApproveError(err instanceof ApiRequestError ? err.message : "Could not approve this request.");
|
|
170
146
|
}
|
|
171
147
|
finally {
|
|
172
|
-
|
|
148
|
+
setApproving(false);
|
|
173
149
|
}
|
|
174
150
|
}
|
|
175
151
|
function closeDenyModal() {
|
|
@@ -177,6 +153,10 @@ function ErasureRequestsSection() {
|
|
|
177
153
|
setDenyReason("");
|
|
178
154
|
setDenyError(null);
|
|
179
155
|
}
|
|
156
|
+
function dismissDenyModal() {
|
|
157
|
+
if (!denying)
|
|
158
|
+
closeDenyModal();
|
|
159
|
+
}
|
|
180
160
|
async function handleDeny() {
|
|
181
161
|
setDenyError(null);
|
|
182
162
|
setDenying(true);
|
|
@@ -184,7 +164,7 @@ function ErasureRequestsSection() {
|
|
|
184
164
|
// Only invoked from the deny modal below, which only renders once denyTarget is set.
|
|
185
165
|
await denyErasureRequest(denyTarget.uid, denyReason);
|
|
186
166
|
closeDenyModal();
|
|
187
|
-
await
|
|
167
|
+
await list.reload();
|
|
188
168
|
}
|
|
189
169
|
catch (err) {
|
|
190
170
|
setDenyError(err instanceof ApiRequestError ? err.message : "Could not deny this request.");
|
|
@@ -193,5 +173,5 @@ function ErasureRequestsSection() {
|
|
|
193
173
|
setDenying(false);
|
|
194
174
|
}
|
|
195
175
|
}
|
|
196
|
-
return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Erasure requests" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Self-service only \u2014 a mailbox owner requests their own account's erasure; there is no admin-initiated path here, only review." }), loadError && _jsx(Alert, { children: loadError }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) :
|
|
176
|
+
return (_jsxs("section", { children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide text-text-muted mb-3", children: "Erasure requests" }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Self-service only \u2014 a mailbox owner requests their own account's erasure; there is no admin-initiated path here, only review." }), list.loadError && _jsx(Alert, { children: list.loadError }), list.loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : list.items.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No erasure requests." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: list.items.map((request) => (_jsxs("li", { className: "border border-border rounded-sm p-3", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("span", { className: "text-sm", children: [request.mailboxUid, " \u00B7 ", new Date(request.dateCreated).toLocaleString(), request.status === "denied" && request.reason && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.reason] })), request.status === "completed" && request.purgedCount !== undefined && (_jsxs("span", { className: "text-text-muted", children: [" \u2014 ", request.purgedCount, " records purged"] }))] }), _jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) })] }), request.status === "pending" && (_jsxs("div", { className: "flex gap-3 mt-2", children: [_jsx(Button, { type: "button", className: "!w-auto", onClick: () => setApproveTarget(request), children: "Approve" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setDenyTarget(request), children: "Deny" })] }))] }, request.uid))) })), _jsx(LoadMoreButton, { list: list, label: "Load more erasure requests" }), _jsxs(Modal, { open: approveTarget !== null, onClose: dismissApproveModal, title: "Approve erasure request", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Approving permanently erases the mailbox ", _jsx("strong", { className: "break-all", children: approveTarget?.mailboxUid }), " ", "and everything in it - mail, contacts, calendars, tasks, and notes."] }), _jsx("p", { className: "text-sm font-semibold text-danger mb-4", children: "This is irreversible and cannot be undone." }), approveError && _jsx(Alert, { children: approveError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: approving, onClick: closeApproveModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: approving, disabled: approving, onClick: handleApprove, children: "Erase mailbox" })] })] }), _jsxs(Modal, { open: denyTarget !== null, onClose: dismissDenyModal, title: "Deny erasure request", children: [_jsxs("label", { className: "flex flex-col gap-1.5 text-sm mb-4", children: [_jsx("span", { className: "font-semibold", children: "Reason" }), _jsx("input", { "aria-label": "Denial reason", className: INPUT_CLASS, value: denyReason, onChange: (e) => setDenyReason(e.target.value) })] }), denyError && _jsx(Alert, { children: denyError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: denying, onClick: closeDenyModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: denying, disabled: denying || !denyReason.trim(), onClick: handleDeny, children: "Deny" })] })] })] }));
|
|
197
177
|
}
|
|
@@ -30,5 +30,5 @@ function DistributionListDetailContent({ uid }) {
|
|
|
30
30
|
if (error || !list) {
|
|
31
31
|
return _jsx(Alert, { children: error ?? "Distribution list not found." });
|
|
32
32
|
}
|
|
33
|
-
return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/distribution-lists", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All distribution lists" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: list.primarySmtpAddress })] }), _jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Name" }), _jsx("dd", { children: list.name }), _jsx("dt", { className: "text-text-muted", children: "Description" }), _jsx("dd", { children: list.description ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Owner" }), _jsx("dd", { children: list.ownerUserUid ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Alias addresses" }), _jsx("dd", { children: list.aliasAddresses && list.aliasAddresses.length > 0 ? list.aliasAddresses.join(", ") : "None" }), _jsx("dt", { className: "text-text-muted", children: "Restrict senders" }),
|
|
33
|
+
return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/distribution-lists", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All distribution lists" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: list.primarySmtpAddress })] }), _jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Name" }), _jsx("dd", { children: list.name }), _jsx("dt", { className: "text-text-muted", children: "Description" }), _jsx("dd", { children: list.description ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Owner" }), _jsx("dd", { children: list.ownerUserUid ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Alias addresses" }), _jsx("dd", { children: list.aliasAddresses && list.aliasAddresses.length > 0 ? list.aliasAddresses.join(", ") : "None" }), _jsx("dt", { className: "text-text-muted", children: "Restrict senders" }), _jsxs("dd", { children: [list.restrictSenders ? "Only members may send" : "No", list.restrictSenders ? (_jsxs("p", { role: "status", className: "mt-1.5 py-2 px-3 rounded-sm text-xs bg-warning/15 text-text", children: ["Members are recognized only by a verified (DKIM) signature, which requires the mail server\u2019s Authentication-Results configuration (the trusted authserv id,", " ", _jsx("code", { children: "mail:security:trusted_authserv_id" }), "). If that isn\u2019t configured, all mail sent to this list is dropped."] })) : (_jsx("p", { className: "mt-1 text-xs text-text-muted", children: "Restricting senders to members requires the mail server\u2019s Authentication-Results configuration (the trusted authserv id); without it, all mail to the list is dropped." }))] }), _jsx("dt", { className: "text-text-muted", children: "Created" }), _jsx("dd", { children: new Date(list.dateCreated).toLocaleString() })] }) }), _jsx(MemberListCard, { list: list, onUpdate: setList })] }));
|
|
34
34
|
}
|
|
@@ -3,72 +3,26 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
4
4
|
// SPDX-License-Identifier: MPL-2.0
|
|
5
5
|
///////////////////////////////////////////////////////////////////////////////
|
|
6
|
-
import {
|
|
6
|
+
import { useState } from "react";
|
|
7
7
|
import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
|
|
8
|
-
import { deleteDomain
|
|
8
|
+
import { deleteDomain } from "@rapidmx/react-shared/admin/domainsApi.js";
|
|
9
9
|
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
10
|
+
import DomainDnsSetup from "../../shared/components/admin/settings/DomainDnsSetup.js";
|
|
10
11
|
import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
|
|
11
12
|
import Button from "@rapidmx/react-shared/components/buttons/Button.js";
|
|
12
13
|
import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
|
|
13
|
-
const RECORD_TYPE_LABELS = {
|
|
14
|
-
ownership: "Ownership (TXT)",
|
|
15
|
-
mx: "MX",
|
|
16
|
-
spf: "SPF",
|
|
17
|
-
dkim: "DKIM",
|
|
18
|
-
dmarc: "DMARC",
|
|
19
|
-
};
|
|
20
14
|
export default function DomainDetailPage(props) {
|
|
21
15
|
return (_jsx(AdminShell, { ...props, active: "domains", children: _jsx(DomainDetailContent, { uid: props.params.uid }) }));
|
|
22
16
|
}
|
|
23
17
|
function DomainDetailContent({ uid }) {
|
|
24
18
|
const [domain, setDomain] = useState(null);
|
|
25
|
-
const [dnsSetup, setDnsSetup] = useState([]);
|
|
26
|
-
const [loading, setLoading] = useState(true);
|
|
27
|
-
const [error, setError] = useState(null);
|
|
28
|
-
const [verifying, setVerifying] = useState(false);
|
|
29
|
-
const [copied, setCopied] = useState(false);
|
|
30
19
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
31
20
|
const [deleting, setDeleting] = useState(false);
|
|
32
21
|
const [deleteError, setDeleteError] = useState(null);
|
|
33
|
-
function reload(id) {
|
|
34
|
-
setLoading(true);
|
|
35
|
-
setError(null);
|
|
36
|
-
Promise.all([getDomain(id), getDnsSetup(id)])
|
|
37
|
-
.then(([d, checks]) => {
|
|
38
|
-
setDomain(d);
|
|
39
|
-
setDnsSetup(checks);
|
|
40
|
-
})
|
|
41
|
-
.catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this domain."))
|
|
42
|
-
.finally(() => setLoading(false));
|
|
43
|
-
}
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
reload(uid);
|
|
46
|
-
}, [uid]);
|
|
47
|
-
// Only ever invoked from the "Verify now" button below, which itself only renders once `domain` is
|
|
48
|
-
// resolved (children only render once loaded — see the early returns above) — the non-null assertion
|
|
49
|
-
// reflects that real invariant, not an unchecked assumption. Matches `QuarantineContent.handleRelease`'s
|
|
50
|
-
// identical pattern.
|
|
51
|
-
async function handleVerify() {
|
|
52
|
-
setVerifying(true);
|
|
53
|
-
setError(null);
|
|
54
|
-
try {
|
|
55
|
-
await verifyDomain(domain.uid);
|
|
56
|
-
reload(domain.uid);
|
|
57
|
-
}
|
|
58
|
-
catch (err) {
|
|
59
|
-
setError(err instanceof ApiRequestError ? err.message : "Could not verify this domain.");
|
|
60
|
-
}
|
|
61
|
-
finally {
|
|
62
|
-
setVerifying(false);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// Only ever invoked from the "Delete domain" confirmation modal below, which itself only renders once
|
|
66
|
-
// `domain` is resolved (children only render once loaded — see the early returns above) — the non-null
|
|
67
|
-
// assertion reflects that real invariant, not an unchecked assumption. Matches `handleVerify`'s identical
|
|
68
|
-
// pattern above.
|
|
69
22
|
function closeDeleteModal() {
|
|
70
23
|
setConfirmingDelete(false);
|
|
71
24
|
}
|
|
25
|
+
// Only ever invoked from the "Delete domain" confirmation modal, which only renders once `domain` is loaded.
|
|
72
26
|
async function handleDelete() {
|
|
73
27
|
setDeleting(true);
|
|
74
28
|
setDeleteError(null);
|
|
@@ -81,22 +35,5 @@ function DomainDetailContent({ uid }) {
|
|
|
81
35
|
setDeleting(false);
|
|
82
36
|
}
|
|
83
37
|
}
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
await navigator.clipboard.writeText(value);
|
|
87
|
-
setCopied(true);
|
|
88
|
-
setTimeout(() => setCopied(false), 2000);
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// Clipboard access can be denied by the browser — the value is still selectable/copyable by hand.
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (loading) {
|
|
95
|
-
return _jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" });
|
|
96
|
-
}
|
|
97
|
-
if (error || !domain) {
|
|
98
|
-
return _jsx(Alert, { children: error ?? "Domain not found." });
|
|
99
|
-
}
|
|
100
|
-
const ownershipCheck = dnsSetup.find((c) => c.type === "ownership");
|
|
101
|
-
return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/domains", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All domains" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: domain.name })] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0 !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete domain" })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Enabled" }), _jsx("dd", { children: domain.enabled ? "Yes" : "No" }), _jsx("dt", { className: "text-text-muted", children: "Verification" }), _jsx("dd", { children: domain.verified ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Verified" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Unverified" })) }), _jsx("dt", { className: "text-text-muted", children: "Last checked" }), _jsx("dd", { children: domain.lastCheckedAt ? new Date(domain.lastCheckedAt).toLocaleString() : "Never" })] }), !domain.verified && (_jsxs("div", { className: "mt-5 pt-5 border-t border-border", children: [_jsxs("p", { className: "text-sm mb-2", children: ["Add the following TXT record to ", _jsx("strong", { children: domain.name }), " to prove ownership, then verify:"] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("code", { className: "flex-1 text-xs bg-surface-alt border border-border rounded-sm py-2 px-3 overflow-x-auto whitespace-nowrap", children: ownershipCheck?.recommendedValue ?? `rapidmx-domain-verification=${domain.verificationToken}` }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => handleCopy(ownershipCheck?.recommendedValue ?? `rapidmx-domain-verification=${domain.verificationToken}`), children: copied ? "Copied" : "Copy" })] }), _jsx(Button, { type: "button", className: "!w-auto mt-3", loading: verifying, disabled: verifying, onClick: handleVerify, children: "Verify now" })] }))] }), dnsSetup.length > 0 && (_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-sm font-bold uppercase tracking-wide mb-3", children: "DNS setup checklist" }), _jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Record", "Status", "Recommended value"].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: dnsSetup.map((check) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: RECORD_TYPE_LABELS[check.type] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: !check.configured ? (_jsx("span", { className: "text-xs text-text-muted", children: "Not configured" })) : check.matches ? (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-success text-white", children: "Live" })) : (_jsx("span", { className: "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill bg-surface-alt text-text-muted", children: "Not found" })) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border text-text-muted", children: _jsx("code", { className: "text-xs", children: check.recommendedValue ?? "—" }) })] }, check.type))) })] }) })] })), _jsxs(Modal, { open: confirmingDelete, onClose: closeDeleteModal, title: "Delete domain", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Are you sure you want to delete ", _jsx("strong", { children: domain.name }), "? This cannot be undone."] }), deleteError && _jsx(Alert, { children: deleteError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: deleting, onClick: closeDeleteModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: deleting, disabled: deleting, onClick: handleDelete, children: "Delete" })] })] })] }));
|
|
38
|
+
return (_jsxs("div", { className: "max-w-3xl flex flex-col gap-5", children: [domain && (_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: "/admin/domains", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All domains" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: domain.name })] }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0 !border-danger !text-danger hover:!border-danger hover:!text-danger", onClick: () => setConfirmingDelete(true), children: "Delete domain" })] })), _jsx(DomainDnsSetup, { uid: uid, onLoaded: setDomain }), domain && (_jsxs(Modal, { open: confirmingDelete, onClose: closeDeleteModal, title: "Delete domain", children: [_jsxs("p", { className: "text-sm mb-5", children: ["Are you sure you want to delete ", _jsx("strong", { children: domain.name }), "? This cannot be undone."] }), deleteError && _jsx(Alert, { children: deleteError }), _jsxs("div", { className: "flex gap-3 justify-end mt-5", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: deleting, onClick: closeDeleteModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: deleting, disabled: deleting, onClick: handleDelete, children: "Delete" })] })] }))] }));
|
|
102
39
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { getEncryptionPolicy } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
|
|
3
|
+
import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
|
|
4
|
+
import EncryptionPolicyForm from "../../shared/components/admin/settings/EncryptionPolicyForm.js";
|
|
5
|
+
import LoadedSettingsForm from "../../shared/components/admin/settings/LoadedSettingsForm.js";
|
|
6
|
+
export default function EncryptionPolicyPage(props) {
|
|
7
|
+
return (_jsx(AdminShell, { ...props, active: "encryptionPolicy", children: _jsx(LoadedSettingsForm, { load: getEncryptionPolicy, loadErrorMessage: "Could not load the encryption policy.", children: (policy, onChange) => _jsx(EncryptionPolicyForm, { policy: policy, onChange: onChange }) }) }));
|
|
8
|
+
}
|