@rapidmx/web-client 0.37.0 → 0.38.1

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.
Files changed (123) hide show
  1. package/apps/admin/configuration-encryption/index.tsx +17 -0
  2. package/apps/admin/domains/[uid].tsx +17 -4
  3. package/apps/admin/integrations/dns-providers/[uid].tsx +205 -0
  4. package/apps/admin/integrations/dns-providers/index.tsx +164 -0
  5. package/apps/admin/integrations/dns-providers/new/index.tsx +55 -0
  6. package/apps/admin/integrations/dns-resolver/index.tsx +16 -0
  7. package/apps/admin/integrations/giphy/index.tsx +16 -0
  8. package/apps/admin/integrations/index.tsx +31 -0
  9. package/apps/admin/integrations/plugin-registry/index.tsx +16 -0
  10. package/apps/admin/integrations/signing-ca/index.tsx +16 -0
  11. package/apps/admin/integrations/stun/index.tsx +16 -0
  12. package/apps/shared/components/admin/configEncryption/ConfigKeysManager.tsx +529 -0
  13. package/apps/shared/components/admin/configEncryption/keyringParts.ts +37 -0
  14. package/apps/shared/components/admin/diagnostics/DiagnosticsManager.tsx +20 -4
  15. package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +23 -6
  16. package/apps/shared/components/admin/diagnostics/KeyringCard.tsx +144 -0
  17. package/apps/shared/components/admin/diagnostics/SettingsTable.tsx +21 -6
  18. package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +15 -3
  19. package/apps/shared/components/admin/integrations/DnsProviderForm.tsx +354 -0
  20. package/apps/shared/components/admin/integrations/DnsProviderHosts.tsx +197 -0
  21. package/apps/shared/components/admin/integrations/DnsProviderOperations.tsx +115 -0
  22. package/apps/shared/components/admin/integrations/DnsResolverForm.tsx +61 -0
  23. package/apps/shared/components/admin/integrations/GiphyForm.tsx +59 -0
  24. package/apps/shared/components/admin/integrations/IntegrationSectionForm.tsx +234 -0
  25. package/apps/shared/components/admin/integrations/IntegrationSectionPage.tsx +40 -0
  26. package/apps/shared/components/admin/integrations/IntegrationsLayout.tsx +72 -0
  27. package/apps/shared/components/admin/integrations/PluginRegistryForm.tsx +230 -0
  28. package/apps/shared/components/admin/integrations/SecretField.tsx +91 -0
  29. package/apps/shared/components/admin/integrations/SigningCaForm.tsx +95 -0
  30. package/apps/shared/components/admin/integrations/StunForm.tsx +63 -0
  31. package/apps/shared/components/admin/integrations/dnsProviderParts.tsx +72 -0
  32. package/apps/shared/components/admin/layout/AdminShell.tsx +12 -1
  33. package/apps/shared/components/admin/settings/CopyableValue.tsx +16 -0
  34. package/apps/shared/components/admin/settings/DomainApexTxt.tsx +319 -0
  35. package/apps/shared/components/admin/settings/DomainDkimRotation.tsx +408 -0
  36. package/apps/shared/components/admin/settings/DomainDnsProvider.tsx +147 -0
  37. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +174 -17
  38. package/apps/shared/components/admin/setup/SetupWizard.tsx +39 -6
  39. package/dist/apps/admin/configuration-encryption/index.d.ts +4 -0
  40. package/dist/apps/admin/configuration-encryption/index.js +7 -0
  41. package/dist/apps/admin/domains/[uid].js +11 -4
  42. package/dist/apps/admin/integrations/dns-providers/[uid].d.ts +7 -0
  43. package/dist/apps/admin/integrations/dns-providers/[uid].js +117 -0
  44. package/dist/apps/admin/integrations/dns-providers/index.d.ts +3 -0
  45. package/dist/apps/admin/integrations/dns-providers/index.js +68 -0
  46. package/dist/apps/admin/integrations/dns-providers/new/index.d.ts +3 -0
  47. package/dist/apps/admin/integrations/dns-providers/new/index.js +31 -0
  48. package/dist/apps/admin/integrations/dns-resolver/index.d.ts +3 -0
  49. package/dist/apps/admin/integrations/dns-resolver/index.js +6 -0
  50. package/dist/apps/admin/integrations/giphy/index.d.ts +3 -0
  51. package/dist/apps/admin/integrations/giphy/index.js +6 -0
  52. package/dist/apps/admin/integrations/index.d.ts +4 -0
  53. package/dist/apps/admin/integrations/index.js +7 -0
  54. package/dist/apps/admin/integrations/plugin-registry/index.d.ts +3 -0
  55. package/dist/apps/admin/integrations/plugin-registry/index.js +6 -0
  56. package/dist/apps/admin/integrations/signing-ca/index.d.ts +3 -0
  57. package/dist/apps/admin/integrations/signing-ca/index.js +6 -0
  58. package/dist/apps/admin/integrations/stun/index.d.ts +3 -0
  59. package/dist/apps/admin/integrations/stun/index.js +6 -0
  60. package/dist/apps/shared/components/admin/configEncryption/ConfigKeysManager.d.ts +9 -0
  61. package/dist/apps/shared/components/admin/configEncryption/ConfigKeysManager.js +187 -0
  62. package/dist/apps/shared/components/admin/configEncryption/keyringParts.d.ts +10 -0
  63. package/dist/apps/shared/components/admin/configEncryption/keyringParts.js +26 -0
  64. package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.js +20 -4
  65. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.d.ts +8 -4
  66. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +7 -5
  67. package/dist/apps/shared/components/admin/diagnostics/KeyringCard.d.ts +18 -0
  68. package/dist/apps/shared/components/admin/diagnostics/KeyringCard.js +51 -0
  69. package/dist/apps/shared/components/admin/diagnostics/SettingsTable.d.ts +9 -1
  70. package/dist/apps/shared/components/admin/diagnostics/SettingsTable.js +10 -3
  71. package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +13 -3
  72. package/dist/apps/shared/components/admin/integrations/DnsProviderForm.d.ts +17 -0
  73. package/dist/apps/shared/components/admin/integrations/DnsProviderForm.js +147 -0
  74. package/dist/apps/shared/components/admin/integrations/DnsProviderHosts.d.ts +10 -0
  75. package/dist/apps/shared/components/admin/integrations/DnsProviderHosts.js +101 -0
  76. package/dist/apps/shared/components/admin/integrations/DnsProviderOperations.d.ts +8 -0
  77. package/dist/apps/shared/components/admin/integrations/DnsProviderOperations.js +60 -0
  78. package/dist/apps/shared/components/admin/integrations/DnsResolverForm.d.ts +7 -0
  79. package/dist/apps/shared/components/admin/integrations/DnsResolverForm.js +15 -0
  80. package/dist/apps/shared/components/admin/integrations/GiphyForm.d.ts +7 -0
  81. package/dist/apps/shared/components/admin/integrations/GiphyForm.js +11 -0
  82. package/dist/apps/shared/components/admin/integrations/IntegrationSectionForm.d.ts +54 -0
  83. package/dist/apps/shared/components/admin/integrations/IntegrationSectionForm.js +131 -0
  84. package/dist/apps/shared/components/admin/integrations/IntegrationSectionPage.d.ts +12 -0
  85. package/dist/apps/shared/components/admin/integrations/IntegrationSectionPage.js +21 -0
  86. package/dist/apps/shared/components/admin/integrations/IntegrationsLayout.d.ts +14 -0
  87. package/dist/apps/shared/components/admin/integrations/IntegrationsLayout.js +46 -0
  88. package/dist/apps/shared/components/admin/integrations/PluginRegistryForm.d.ts +7 -0
  89. package/dist/apps/shared/components/admin/integrations/PluginRegistryForm.js +90 -0
  90. package/dist/apps/shared/components/admin/integrations/SecretField.d.ts +25 -0
  91. package/dist/apps/shared/components/admin/integrations/SecretField.js +25 -0
  92. package/dist/apps/shared/components/admin/integrations/SigningCaForm.d.ts +7 -0
  93. package/dist/apps/shared/components/admin/integrations/SigningCaForm.js +25 -0
  94. package/dist/apps/shared/components/admin/integrations/StunForm.d.ts +9 -0
  95. package/dist/apps/shared/components/admin/integrations/StunForm.js +26 -0
  96. package/dist/apps/shared/components/admin/integrations/dnsProviderParts.d.ts +28 -0
  97. package/dist/apps/shared/components/admin/integrations/dnsProviderParts.js +43 -0
  98. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
  99. package/dist/apps/shared/components/admin/layout/AdminShell.js +8 -1
  100. package/dist/apps/shared/components/admin/settings/CopyableValue.d.ts +6 -0
  101. package/dist/apps/shared/components/admin/settings/CopyableValue.js +6 -0
  102. package/dist/apps/shared/components/admin/settings/DomainApexTxt.d.ts +13 -0
  103. package/dist/apps/shared/components/admin/settings/DomainApexTxt.js +148 -0
  104. package/dist/apps/shared/components/admin/settings/DomainDkimRotation.d.ts +15 -0
  105. package/dist/apps/shared/components/admin/settings/DomainDkimRotation.js +200 -0
  106. package/dist/apps/shared/components/admin/settings/DomainDnsProvider.d.ts +12 -0
  107. package/dist/apps/shared/components/admin/settings/DomainDnsProvider.js +70 -0
  108. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +3 -3
  109. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +61 -8
  110. package/dist/apps/shared/components/admin/setup/SetupWizard.js +9 -1
  111. package/dist/lib/admin/configKeysApi.d.ts +96 -0
  112. package/dist/lib/admin/configKeysApi.js +34 -0
  113. package/dist/lib/admin/dnsProvidersApi.d.ts +172 -0
  114. package/dist/lib/admin/dnsProvidersApi.js +70 -0
  115. package/dist/lib/admin/domainsApi.d.ts +116 -1
  116. package/dist/lib/admin/domainsApi.js +33 -0
  117. package/dist/lib/admin/integrationsApi.d.ts +75 -0
  118. package/dist/lib/admin/integrationsApi.js +31 -0
  119. package/lib/admin/configKeysApi.ts +117 -0
  120. package/lib/admin/dnsProvidersApi.ts +241 -0
  121. package/lib/admin/domainsApi.ts +145 -1
  122. package/lib/admin/integrationsApi.ts +107 -0
  123. package/package.json +1 -1
@@ -0,0 +1,319 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { ReactNode, useEffect, useRef, useState } from "react";
6
+ import { DnsProviderType, listDnsProviders } from "../../../../../lib/admin/dnsProvidersApi.js";
7
+ import { ApexTxtView, Domain, getApexTxt, updateApexTxt } from "../../../../../lib/admin/domainsApi.js";
8
+ import Button from "../../../../../lib/components/buttons/Button.js";
9
+ import FormField from "../../../../../lib/components/forms/FormField.js";
10
+ import Modal from "../../../../../lib/components/overlays/Modal.js";
11
+ import Alert from "../ActionAlert.js";
12
+ import { actionErrorMessage } from "../elevation.js";
13
+
14
+ const INPUT_CLASS =
15
+ "w-full text-sm font-mono py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
16
+
17
+ /** The values in a "values to keep" box: one per line, trimmed, blank lines dropped, each once. */
18
+ function parseKeep(text: string): string[] {
19
+ return [...new Set(text.split("\n").map((line) => line.trim()).filter(Boolean))];
20
+ }
21
+
22
+ /** What the values to keep start as: while managed the saved list; otherwise the suggestion from the live lookup plus the values kept before an opt-out,
23
+ * which are restored as plain records only after a few minutes and so are not at the root yet. */
24
+ function startingKeep(view: ApexTxtView): string[] {
25
+ return view.managed ? view.keep : [...new Set([...view.suggestedKeep, ...view.keep])];
26
+ }
27
+
28
+ /** TXT values, each in full (they can be long). */
29
+ function Values({ label, values }: { label: string; values: string[] }) {
30
+ return (
31
+ <ul aria-label={label} className="flex flex-col gap-1 mb-3">
32
+ {values.map((value) => (
33
+ <li key={value}>
34
+ <code className="text-xs break-all">{value}</code>
35
+ </li>
36
+ ))}
37
+ </ul>
38
+ );
39
+ }
40
+
41
+ function Card({ children }: { children: ReactNode }) {
42
+ return (
43
+ <div className="bg-surface border border-border rounded-md p-6">
44
+ <h2 className="text-sm font-bold uppercase tracking-wide mb-3">Root TXT records</h2>
45
+ {children}
46
+ </div>
47
+ );
48
+ }
49
+
50
+ export interface DomainApexTxtProps {
51
+ domain: Domain;
52
+ /** Called after a change was saved, so the page can load the domain and its checklist again. */
53
+ onChanged: () => void;
54
+ }
55
+
56
+ /**
57
+ * Whether RapidMX owns the domain's root TXT records to publish its SPF record there. A provider that owns a record set owns every value in it, so
58
+ * nothing is published there unless the administrator opts in with the values to keep; while it is managed, values added at the DNS host are removed, so
59
+ * this is the only place to edit them (spec 9.4). Shown for a domain a DNS provider publishes.
60
+ */
61
+ export default function DomainApexTxt({ domain, onChanged }: DomainApexTxtProps) {
62
+ const [view, setView] = useState<ApexTxtView | null>(null);
63
+ // Whether the view is what the server said when the page last asked: not while the opt-in dialog is waiting for the values at the root now.
64
+ const [fresh, setFresh] = useState(false);
65
+ // The type of the domain's provider, which decides what it can keep; null while unknown (then the server decides).
66
+ const [providerType, setProviderType] = useState<DnsProviderType | null>(null);
67
+ const [loadError, setLoadError] = useState<string | null>(null);
68
+ const [keepText, setKeepText] = useState("");
69
+ const [saving, setSaving] = useState(false);
70
+ const [saved, setSaved] = useState(false);
71
+ const [error, setError] = useState<string | null>(null);
72
+ const [stopped, setStopped] = useState(false);
73
+ // Which confirmation is open: opting in (with what it will do), or stopping.
74
+ const [confirming, setConfirming] = useState<"in" | "out" | null>(null);
75
+
76
+ // An Azure provider keeps one value per name, so the set holds the SPF record alone; rfc2136 can't keep an SPF value at all (the server's `providerKeepsTxt`).
77
+ const keepsNothing = providerType === "azure";
78
+ const cantManage = providerType === "rfc2136";
79
+ // The newest request, so an answer to an older one is dropped, and whether the keep list holds edits the server hasn't seen (a reload leaves them).
80
+ const latest = useRef(0);
81
+ const dirty = useRef(false);
82
+
83
+ function load(resetKeep = true) {
84
+ const request = ++latest.current;
85
+ setLoadError(null);
86
+ return getApexTxt(domain.uid)
87
+ .then((loaded) => {
88
+ if (request !== latest.current) {
89
+ return;
90
+ }
91
+ setView(loaded);
92
+ setFresh(true);
93
+ if (resetKeep && !dirty.current) {
94
+ setKeepText(startingKeep(loaded).join("\n"));
95
+ }
96
+ // The opt-in must not go on without knowing what is at the root.
97
+ if (loaded.lookupFailed) {
98
+ setConfirming((open) => (open === "in" ? null : open));
99
+ }
100
+ })
101
+ .catch((err) => {
102
+ if (request !== latest.current) {
103
+ return;
104
+ }
105
+ setView(null);
106
+ setConfirming(null);
107
+ setLoadError(actionErrorMessage(err, "Could not load the root TXT records."));
108
+ });
109
+ }
110
+
111
+ // Again when the domain was saved (its version moved), which is how a change made here arrives.
112
+ useEffect(() => {
113
+ void load();
114
+ }, [domain.uid, domain.dnsProviderUid, domain.version]);
115
+
116
+ useEffect(() => {
117
+ let current = true;
118
+ listDnsProviders()
119
+ .then((providers) => current && setProviderType(providers.find((provider) => provider.uid === domain.dnsProviderUid)?.type ?? null))
120
+ .catch(() => current && setProviderType(null));
121
+ return () => {
122
+ current = false;
123
+ };
124
+ }, [domain.dnsProviderUid]);
125
+
126
+ async function save(managed: boolean) {
127
+ setSaving(true);
128
+ setError(null);
129
+ setSaved(false);
130
+ setStopped(false);
131
+ try {
132
+ const keep = keepsNothing ? [] : parseKeep(keepText);
133
+ await updateApexTxt(domain.uid, managed ? { managed, keep } : { managed });
134
+ dirty.current = false;
135
+ if (managed) {
136
+ // An edit while managed leaves the domain as it was, so the view takes the new list itself; an opt-in arrives with the reloaded domain.
137
+ setView((current) => (current!.managed ? { ...current!, keep } : current));
138
+ setSaved(true);
139
+ } else {
140
+ setStopped(true);
141
+ }
142
+ setConfirming(null);
143
+ onChanged();
144
+ } catch (err) {
145
+ setError(actionErrorMessage(err, "Could not change the root TXT records."));
146
+ } finally {
147
+ setSaving(false);
148
+ }
149
+ }
150
+
151
+ // The values removed are listed from what is at the root when the dialog opens, not when the page loaded; the keep list stays as it is.
152
+ function confirm(kind: "in" | "out") {
153
+ setError(null);
154
+ setConfirming(kind);
155
+ if (kind === "in") {
156
+ setFresh(false);
157
+ void load(false);
158
+ }
159
+ }
160
+
161
+ if (loadError) {
162
+ return (
163
+ <Card>
164
+ <Alert>{loadError}</Alert>
165
+ </Card>
166
+ );
167
+ }
168
+ if (!view) {
169
+ return (
170
+ <Card>
171
+ <p className="text-sm text-text-muted">Loading&hellip;</p>
172
+ </Card>
173
+ );
174
+ }
175
+
176
+ const keep = keepsNothing ? [] : parseKeep(keepText);
177
+ const removed = view.current.filter((value) => !keep.includes(value));
178
+ // What a stop restores, and what an earlier stop restored: the SPF value RapidMX published and the values it kept.
179
+ const published = [...(view.spf ? [view.spf] : []), ...view.keep];
180
+ const keepField = keepsNothing ? (
181
+ <p className="text-sm text-text-muted mb-3">
182
+ An Azure DNS provider keeps one value per name, so the root TXT records would hold the SPF record alone and RapidMX would remove every other value.
183
+ </p>
184
+ ) : (
185
+ <FormField label="Values to keep" htmlFor="apexTxtKeep">
186
+ <textarea
187
+ id="apexTxtKeep"
188
+ className={INPUT_CLASS}
189
+ rows={Math.max(3, keep.length + 1)}
190
+ value={keepText}
191
+ onChange={(e) => {
192
+ dirty.current = true;
193
+ setKeepText(e.target.value);
194
+ setSaved(false);
195
+ }}
196
+ />
197
+ <p className="text-xs text-text-muted mt-1">One value per line, exactly as it is in the DNS (a site-verification record, for example).</p>
198
+ </FormField>
199
+ );
200
+
201
+ return (
202
+ <Card>
203
+ {error && confirming === null && <Alert>{error}</Alert>}
204
+ {stopped && !view.managed && (
205
+ <p className="text-sm mb-3">
206
+ RapidMX no longer manages the root TXT records. The SPF record and the kept values are being restored as plain records; they are missing for a
207
+ few minutes meanwhile.
208
+ </p>
209
+ )}
210
+ {view.managed ? (
211
+ <>
212
+ <p className="text-sm font-semibold mb-3">Values added at your DNS host are removed within a minute; add them here.</p>
213
+ <p className="text-sm text-text-muted mb-1">RapidMX publishes this SPF record at the root, and the values to keep below:</p>
214
+ <Values label="Published SPF record" values={[view.spf!]} />
215
+ {keepField}
216
+ <div className="flex items-center gap-3">
217
+ <Button
218
+ type="button"
219
+ className="!w-auto"
220
+ loading={saving}
221
+ disabled={saving || keep.join("\n") === view.keep.join("\n")}
222
+ onClick={() => save(true)}
223
+ >
224
+ Save values to keep
225
+ </Button>
226
+ <Button type="button" variant="secondary" className="!w-auto" disabled={saving} onClick={() => confirm("out")}>
227
+ Stop managing
228
+ </Button>
229
+ {saved && <span className="text-sm text-text-muted">Saved.</span>}
230
+ </div>
231
+ </>
232
+ ) : cantManage ? (
233
+ <>
234
+ <p className="text-sm text-text-muted mb-3">
235
+ An rfc2136 DNS provider can't keep the SPF value among the root TXT records, so RapidMX can't manage them. Add the SPF record and the other
236
+ root TXT records at your DNS host.
237
+ </p>
238
+ {view.current.length > 0 && <Values label="Current root TXT records" values={view.current} />}
239
+ </>
240
+ ) : view.lookupFailed ? (
241
+ <>
242
+ <p className="text-sm text-text-muted mb-3">
243
+ The root TXT records couldn't be looked up, and taking them over without knowing what is there would delete it. Try again in a moment.
244
+ </p>
245
+ <Button type="button" variant="secondary" className="!w-auto" onClick={() => void load()}>
246
+ Try again
247
+ </Button>
248
+ </>
249
+ ) : (
250
+ <>
251
+ <p className="text-sm text-text-muted mb-3">
252
+ SPF is a TXT record at the root of the domain, shared with the verification records of other services. RapidMX publishes nothing there
253
+ unless you opt in, and then it owns every value in the set: it publishes SPF and the values you keep, and removes the rest.
254
+ </p>
255
+ {view.current.length > 0 ? (
256
+ <>
257
+ <p className="text-sm font-semibold mb-1">At the root now</p>
258
+ <Values label="Current root TXT records" values={view.current} />
259
+ </>
260
+ ) : (
261
+ <p className="text-sm text-text-muted mb-3">There are no TXT records at the root now.</p>
262
+ )}
263
+ {published.length > 0 && (
264
+ <>
265
+ <p className="text-sm font-semibold mb-1">Restored when RapidMX stopped managing them</p>
266
+ <Values label="Restored root TXT records" values={published} />
267
+ </>
268
+ )}
269
+ {keepField}
270
+ <Button type="button" className="!w-auto" onClick={() => confirm("in")}>
271
+ Manage the root TXT records
272
+ </Button>
273
+ </>
274
+ )}
275
+
276
+ <Modal open={confirming === "in"} onClose={() => setConfirming(null)} title="Manage the root TXT records">
277
+ <p className="text-sm mb-3">
278
+ RapidMX will publish an SPF record at the root of <strong>{domain.name}</strong> (yours if it already allows this domain's mail servers,
279
+ otherwise <code className="text-xs">v=spf1 mx ~all</code>), and these values:
280
+ </p>
281
+ {keep.length > 0 ? <Values label="Values RapidMX will keep" values={keep} /> : <p className="text-sm text-text-muted mb-3">(none)</p>}
282
+ <p className="text-sm mb-1">At the root now and not on that list, so removed (an SPF record is replaced by the one above):</p>
283
+ {removed.length > 0 ? (
284
+ <Values label="Values removed" values={removed} />
285
+ ) : (
286
+ <p className="text-sm text-text-muted mb-3">Nothing else is there to remove.</p>
287
+ )}
288
+ {!fresh && !error && <p className="text-sm text-text-muted mb-3">Checking what is at the root now&hellip;</p>}
289
+ <p className="text-sm mb-3">Anything you add at your DNS host later is removed within a minute; add it here instead.</p>
290
+ {error && <Alert>{error}</Alert>}
291
+ <div className="flex gap-3 justify-end mt-5">
292
+ <Button type="button" variant="secondary" className="!w-auto" disabled={saving} onClick={() => setConfirming(null)}>
293
+ Cancel
294
+ </Button>
295
+ <Button type="button" className="!w-auto" loading={saving} disabled={saving || !fresh} onClick={() => save(true)}>
296
+ Manage them
297
+ </Button>
298
+ </div>
299
+ </Modal>
300
+
301
+ <Modal open={confirming === "out"} onClose={() => setConfirming(null)} title="Stop managing the root TXT records">
302
+ <p className="text-sm mb-3">
303
+ RapidMX lets go of the set and restores these values as plain records you edit at your DNS host. The root TXT records are missing for a few
304
+ minutes while this happens.
305
+ </p>
306
+ <Values label="Values restored" values={published} />
307
+ {error && <Alert>{error}</Alert>}
308
+ <div className="flex gap-3 justify-end mt-5">
309
+ <Button type="button" variant="secondary" className="!w-auto" disabled={saving} onClick={() => setConfirming(null)}>
310
+ Cancel
311
+ </Button>
312
+ <Button type="button" className="!w-auto" loading={saving} disabled={saving} onClick={() => save(false)}>
313
+ Stop managing
314
+ </Button>
315
+ </div>
316
+ </Modal>
317
+ </Card>
318
+ );
319
+ }