@rapidmx/web-client 0.37.1 → 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.
- package/apps/admin/configuration-encryption/index.tsx +17 -0
- package/apps/admin/domains/[uid].tsx +17 -4
- package/apps/admin/integrations/dns-providers/[uid].tsx +205 -0
- package/apps/admin/integrations/dns-providers/index.tsx +164 -0
- package/apps/admin/integrations/dns-providers/new/index.tsx +55 -0
- package/apps/admin/integrations/dns-resolver/index.tsx +16 -0
- package/apps/admin/integrations/giphy/index.tsx +16 -0
- package/apps/admin/integrations/index.tsx +31 -0
- package/apps/admin/integrations/plugin-registry/index.tsx +16 -0
- package/apps/admin/integrations/signing-ca/index.tsx +16 -0
- package/apps/admin/integrations/stun/index.tsx +16 -0
- package/apps/shared/components/admin/configEncryption/ConfigKeysManager.tsx +529 -0
- package/apps/shared/components/admin/configEncryption/keyringParts.ts +37 -0
- package/apps/shared/components/admin/diagnostics/DiagnosticsManager.tsx +20 -4
- package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +23 -6
- package/apps/shared/components/admin/diagnostics/KeyringCard.tsx +144 -0
- package/apps/shared/components/admin/diagnostics/SettingsTable.tsx +21 -6
- package/apps/shared/components/admin/diagnostics/diagnosticsApi.ts +15 -3
- package/apps/shared/components/admin/integrations/DnsProviderForm.tsx +354 -0
- package/apps/shared/components/admin/integrations/DnsProviderHosts.tsx +197 -0
- package/apps/shared/components/admin/integrations/DnsProviderOperations.tsx +115 -0
- package/apps/shared/components/admin/integrations/DnsResolverForm.tsx +61 -0
- package/apps/shared/components/admin/integrations/GiphyForm.tsx +59 -0
- package/apps/shared/components/admin/integrations/IntegrationSectionForm.tsx +234 -0
- package/apps/shared/components/admin/integrations/IntegrationSectionPage.tsx +40 -0
- package/apps/shared/components/admin/integrations/IntegrationsLayout.tsx +72 -0
- package/apps/shared/components/admin/integrations/PluginRegistryForm.tsx +230 -0
- package/apps/shared/components/admin/integrations/SecretField.tsx +91 -0
- package/apps/shared/components/admin/integrations/SigningCaForm.tsx +95 -0
- package/apps/shared/components/admin/integrations/StunForm.tsx +63 -0
- package/apps/shared/components/admin/integrations/dnsProviderParts.tsx +72 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +12 -1
- package/apps/shared/components/admin/settings/CopyableValue.tsx +16 -0
- package/apps/shared/components/admin/settings/DomainApexTxt.tsx +319 -0
- package/apps/shared/components/admin/settings/DomainDkimRotation.tsx +408 -0
- package/apps/shared/components/admin/settings/DomainDnsProvider.tsx +147 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +174 -17
- package/apps/shared/components/admin/setup/SetupWizard.tsx +39 -6
- package/dist/apps/admin/configuration-encryption/index.d.ts +4 -0
- package/dist/apps/admin/configuration-encryption/index.js +7 -0
- package/dist/apps/admin/domains/[uid].js +11 -4
- package/dist/apps/admin/integrations/dns-providers/[uid].d.ts +7 -0
- package/dist/apps/admin/integrations/dns-providers/[uid].js +117 -0
- package/dist/apps/admin/integrations/dns-providers/index.d.ts +3 -0
- package/dist/apps/admin/integrations/dns-providers/index.js +68 -0
- package/dist/apps/admin/integrations/dns-providers/new/index.d.ts +3 -0
- package/dist/apps/admin/integrations/dns-providers/new/index.js +31 -0
- package/dist/apps/admin/integrations/dns-resolver/index.d.ts +3 -0
- package/dist/apps/admin/integrations/dns-resolver/index.js +6 -0
- package/dist/apps/admin/integrations/giphy/index.d.ts +3 -0
- package/dist/apps/admin/integrations/giphy/index.js +6 -0
- package/dist/apps/admin/integrations/index.d.ts +4 -0
- package/dist/apps/admin/integrations/index.js +7 -0
- package/dist/apps/admin/integrations/plugin-registry/index.d.ts +3 -0
- package/dist/apps/admin/integrations/plugin-registry/index.js +6 -0
- package/dist/apps/admin/integrations/signing-ca/index.d.ts +3 -0
- package/dist/apps/admin/integrations/signing-ca/index.js +6 -0
- package/dist/apps/admin/integrations/stun/index.d.ts +3 -0
- package/dist/apps/admin/integrations/stun/index.js +6 -0
- package/dist/apps/shared/components/admin/configEncryption/ConfigKeysManager.d.ts +9 -0
- package/dist/apps/shared/components/admin/configEncryption/ConfigKeysManager.js +187 -0
- package/dist/apps/shared/components/admin/configEncryption/keyringParts.d.ts +10 -0
- package/dist/apps/shared/components/admin/configEncryption/keyringParts.js +26 -0
- package/dist/apps/shared/components/admin/diagnostics/DiagnosticsManager.js +20 -4
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.d.ts +8 -4
- package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +7 -5
- package/dist/apps/shared/components/admin/diagnostics/KeyringCard.d.ts +18 -0
- package/dist/apps/shared/components/admin/diagnostics/KeyringCard.js +51 -0
- package/dist/apps/shared/components/admin/diagnostics/SettingsTable.d.ts +9 -1
- package/dist/apps/shared/components/admin/diagnostics/SettingsTable.js +10 -3
- package/dist/apps/shared/components/admin/diagnostics/diagnosticsApi.d.ts +13 -3
- package/dist/apps/shared/components/admin/integrations/DnsProviderForm.d.ts +17 -0
- package/dist/apps/shared/components/admin/integrations/DnsProviderForm.js +147 -0
- package/dist/apps/shared/components/admin/integrations/DnsProviderHosts.d.ts +10 -0
- package/dist/apps/shared/components/admin/integrations/DnsProviderHosts.js +101 -0
- package/dist/apps/shared/components/admin/integrations/DnsProviderOperations.d.ts +8 -0
- package/dist/apps/shared/components/admin/integrations/DnsProviderOperations.js +60 -0
- package/dist/apps/shared/components/admin/integrations/DnsResolverForm.d.ts +7 -0
- package/dist/apps/shared/components/admin/integrations/DnsResolverForm.js +15 -0
- package/dist/apps/shared/components/admin/integrations/GiphyForm.d.ts +7 -0
- package/dist/apps/shared/components/admin/integrations/GiphyForm.js +11 -0
- package/dist/apps/shared/components/admin/integrations/IntegrationSectionForm.d.ts +54 -0
- package/dist/apps/shared/components/admin/integrations/IntegrationSectionForm.js +131 -0
- package/dist/apps/shared/components/admin/integrations/IntegrationSectionPage.d.ts +12 -0
- package/dist/apps/shared/components/admin/integrations/IntegrationSectionPage.js +21 -0
- package/dist/apps/shared/components/admin/integrations/IntegrationsLayout.d.ts +14 -0
- package/dist/apps/shared/components/admin/integrations/IntegrationsLayout.js +46 -0
- package/dist/apps/shared/components/admin/integrations/PluginRegistryForm.d.ts +7 -0
- package/dist/apps/shared/components/admin/integrations/PluginRegistryForm.js +90 -0
- package/dist/apps/shared/components/admin/integrations/SecretField.d.ts +25 -0
- package/dist/apps/shared/components/admin/integrations/SecretField.js +25 -0
- package/dist/apps/shared/components/admin/integrations/SigningCaForm.d.ts +7 -0
- package/dist/apps/shared/components/admin/integrations/SigningCaForm.js +25 -0
- package/dist/apps/shared/components/admin/integrations/StunForm.d.ts +9 -0
- package/dist/apps/shared/components/admin/integrations/StunForm.js +26 -0
- package/dist/apps/shared/components/admin/integrations/dnsProviderParts.d.ts +28 -0
- package/dist/apps/shared/components/admin/integrations/dnsProviderParts.js +43 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +1 -1
- package/dist/apps/shared/components/admin/layout/AdminShell.js +8 -1
- package/dist/apps/shared/components/admin/settings/CopyableValue.d.ts +6 -0
- package/dist/apps/shared/components/admin/settings/CopyableValue.js +6 -0
- package/dist/apps/shared/components/admin/settings/DomainApexTxt.d.ts +13 -0
- package/dist/apps/shared/components/admin/settings/DomainApexTxt.js +148 -0
- package/dist/apps/shared/components/admin/settings/DomainDkimRotation.d.ts +15 -0
- package/dist/apps/shared/components/admin/settings/DomainDkimRotation.js +200 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsProvider.d.ts +12 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsProvider.js +70 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +3 -3
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +61 -8
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +9 -1
- package/dist/lib/admin/configKeysApi.d.ts +96 -0
- package/dist/lib/admin/configKeysApi.js +34 -0
- package/dist/lib/admin/dnsProvidersApi.d.ts +172 -0
- package/dist/lib/admin/dnsProvidersApi.js +70 -0
- package/dist/lib/admin/domainsApi.d.ts +116 -1
- package/dist/lib/admin/domainsApi.js +33 -0
- package/dist/lib/admin/integrationsApi.d.ts +75 -0
- package/dist/lib/admin/integrationsApi.js +31 -0
- package/lib/admin/configKeysApi.ts +117 -0
- package/lib/admin/dnsProvidersApi.ts +241 -0
- package/lib/admin/domainsApi.ts +145 -1
- package/lib/admin/integrationsApi.ts +107 -0
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import AdminShell, { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
|
|
7
|
+
import ConfigKeysManager from "../../shared/components/admin/configEncryption/ConfigKeysManager.js";
|
|
8
|
+
|
|
9
|
+
/** The configuration keyring: the keys that encrypt the credentials saved in the console, and their rotation. */
|
|
10
|
+
export default function ConfigurationEncryptionPage(props: Omit<AdminShellProps, "active">) {
|
|
11
|
+
return (
|
|
12
|
+
<AdminShell {...props} active="configurationEncryption">
|
|
13
|
+
<h1 className="text-xl font-bold uppercase tracking-wide mb-3">Configuration encryption</h1>
|
|
14
|
+
<ConfigKeysManager />
|
|
15
|
+
</AdminShell>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
///////////////////////////////////////////////////////////////////////////////
|
|
5
5
|
import React, { useState } from "react";
|
|
6
6
|
import { useRouter } from "@rapidrest/react/client";
|
|
7
|
-
import { deleteDomain, Domain, updateDomain } from "../../../lib/admin/domainsApi.js";
|
|
7
|
+
import { deleteDomain, DnsRecordCheck, Domain, updateDomain } from "../../../lib/admin/domainsApi.js";
|
|
8
8
|
import AdminShell, { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
|
|
9
|
+
import DomainApexTxt from "../../shared/components/admin/settings/DomainApexTxt.js";
|
|
10
|
+
import DomainDkimRotation from "../../shared/components/admin/settings/DomainDkimRotation.js";
|
|
11
|
+
import DomainDnsProvider from "../../shared/components/admin/settings/DomainDnsProvider.js";
|
|
9
12
|
import DomainDnsSetup from "../../shared/components/admin/settings/DomainDnsSetup.js";
|
|
10
13
|
import Alert from "../../shared/components/admin/ActionAlert.js";
|
|
11
14
|
import Button from "../../../lib/components/buttons/Button.js";
|
|
@@ -27,6 +30,8 @@ export default function DomainDetailPage(props: Omit<AdminShellProps, "active">
|
|
|
27
30
|
function DomainDetailContent({ uid }: { uid: string }) {
|
|
28
31
|
const { navigate } = useRouter();
|
|
29
32
|
const [domain, setDomain] = useState<Domain | null>(null);
|
|
33
|
+
// The checklist the DNS setup panel loaded, which the DKIM rotation panel reads the rotation's records from.
|
|
34
|
+
const [checks, setChecks] = useState<DnsRecordCheck[]>([]);
|
|
30
35
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
31
36
|
const [deleting, setDeleting] = useState(false);
|
|
32
37
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
@@ -36,9 +41,10 @@ function DomainDetailContent({ uid }: { uid: string }) {
|
|
|
36
41
|
const [savingAlias, setSavingAlias] = useState(false);
|
|
37
42
|
const [aliasError, setAliasError] = useState<string | null>(null);
|
|
38
43
|
const [aliasSaved, setAliasSaved] = useState(false);
|
|
39
|
-
// Bumped after a successful
|
|
44
|
+
// Bumped after a successful save (the alias, the DNS provider, the root TXT records) to force `DomainDnsSetup` (which owns its own `domain` state,
|
|
40
45
|
// fetched only on mount/uid change) to reload and reflect the new value in its own status panel.
|
|
41
46
|
const [refreshCount, setRefreshCount] = useState(0);
|
|
47
|
+
const refresh = () => setRefreshCount((c) => c + 1);
|
|
42
48
|
|
|
43
49
|
function closeDeleteModal() {
|
|
44
50
|
setConfirmingDelete(false);
|
|
@@ -57,7 +63,8 @@ function DomainDetailContent({ uid }: { uid: string }) {
|
|
|
57
63
|
}
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
function handleLoaded(d: Domain) {
|
|
66
|
+
function handleLoaded(d: Domain, loadedChecks: DnsRecordCheck[]) {
|
|
67
|
+
setChecks(loadedChecks);
|
|
61
68
|
setDomain(d);
|
|
62
69
|
setAliasInput((current) => (current === null ? d?.aliasOf ?? "" : current));
|
|
63
70
|
}
|
|
@@ -72,7 +79,7 @@ function DomainDetailContent({ uid }: { uid: string }) {
|
|
|
72
79
|
setDomain(updated);
|
|
73
80
|
setAliasInput(updated.aliasOf ?? "");
|
|
74
81
|
setAliasSaved(true);
|
|
75
|
-
|
|
82
|
+
refresh();
|
|
76
83
|
} catch (err) {
|
|
77
84
|
setAliasError(actionErrorMessage(err, "Could not update this domain."));
|
|
78
85
|
} finally {
|
|
@@ -103,6 +110,12 @@ function DomainDetailContent({ uid }: { uid: string }) {
|
|
|
103
110
|
|
|
104
111
|
<DomainDnsSetup key={refreshCount} uid={uid} onLoaded={handleLoaded} />
|
|
105
112
|
|
|
113
|
+
{domain && <DomainDnsProvider domain={domain} onChanged={refresh} />}
|
|
114
|
+
|
|
115
|
+
{domain && <DomainDkimRotation domain={domain} checks={checks} onChanged={refresh} />}
|
|
116
|
+
|
|
117
|
+
{domain?.dnsProviderUid && <DomainApexTxt domain={domain} onChanged={refresh} />}
|
|
118
|
+
|
|
106
119
|
{domain && (
|
|
107
120
|
<div className="bg-surface border border-border rounded-md p-6">
|
|
108
121
|
<h2 className="text-sm font-bold uppercase tracking-wide mb-3">Alias</h2>
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
|
+
import {
|
|
8
|
+
deleteDnsProvider,
|
|
9
|
+
DnsProvider,
|
|
10
|
+
DnsProviderInput,
|
|
11
|
+
DnsProviderTypeInfo,
|
|
12
|
+
getDnsProvider,
|
|
13
|
+
listDnsProviderTypes,
|
|
14
|
+
updateDnsProvider,
|
|
15
|
+
} from "../../../../lib/admin/dnsProvidersApi.js";
|
|
16
|
+
import Button from "../../../../lib/components/buttons/Button.js";
|
|
17
|
+
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
18
|
+
import Modal from "../../../../lib/components/overlays/Modal.js";
|
|
19
|
+
import AdminShell, { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
20
|
+
import DnsProviderForm from "../../../shared/components/admin/integrations/DnsProviderForm.js";
|
|
21
|
+
import DnsProviderHosts from "../../../shared/components/admin/integrations/DnsProviderHosts.js";
|
|
22
|
+
import DnsProviderOperations from "../../../shared/components/admin/integrations/DnsProviderOperations.js";
|
|
23
|
+
import IntegrationsLayout from "../../../shared/components/admin/integrations/IntegrationsLayout.js";
|
|
24
|
+
import { Badge, dnsProviderStatus, domainsText, formatTime } from "../../../shared/components/admin/integrations/dnsProviderParts.js";
|
|
25
|
+
import Alert from "../../../shared/components/admin/ActionAlert.js";
|
|
26
|
+
import { actionErrorMessage } from "../../../shared/components/admin/elevation.js";
|
|
27
|
+
|
|
28
|
+
const LIST_HREF = "/admin/integrations/dns-providers";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What of a config an administrator can change, as the server shows it: the controller's status reports move the row's version without touching any of it.
|
|
32
|
+
* A credential's `unreadable` flag is the server's own finding (a keyring that changed), not something an administrator set, so it is left out.
|
|
33
|
+
*/
|
|
34
|
+
function configOf(provider: DnsProvider): string {
|
|
35
|
+
const { name, zones, options, credentials, enabled, allowTakeover } = provider;
|
|
36
|
+
const shown: Record<string, unknown> = {};
|
|
37
|
+
for (const [key, mask] of Object.entries(credentials)) {
|
|
38
|
+
shown[key] = { ...mask, unreadable: undefined };
|
|
39
|
+
}
|
|
40
|
+
return JSON.stringify({ name, zones, options, credentials: shown, enabled, allowTakeover });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default function DnsProviderPage(props: Omit<AdminShellProps, "active"> & { params: { uid: string } }) {
|
|
44
|
+
return (
|
|
45
|
+
<AdminShell {...props} active="integrations">
|
|
46
|
+
<IntegrationsLayout active="dnsProviders">
|
|
47
|
+
<DnsProviderDetail uid={props.params.uid} />
|
|
48
|
+
</IntegrationsLayout>
|
|
49
|
+
</AdminShell>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function DnsProviderDetail({ uid }: { uid: string }) {
|
|
54
|
+
const { navigate } = useRouter();
|
|
55
|
+
const [provider, setProvider] = useState<DnsProvider | null>(null);
|
|
56
|
+
// The version a save is made against: the one shown, or a newer one found after a refusal (the form isn't reset to show it).
|
|
57
|
+
const version = useRef(0);
|
|
58
|
+
// The config an administrator last saw, which a refusal compares the latest one against: the one loaded or saved, then the one a refusal reported.
|
|
59
|
+
const seen = useRef("");
|
|
60
|
+
const [types, setTypes] = useState<DnsProviderTypeInfo[]>([]);
|
|
61
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
62
|
+
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
|
63
|
+
const [deleting, setDeleting] = useState(false);
|
|
64
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
Promise.all([getDnsProvider(uid), listDnsProviderTypes()])
|
|
68
|
+
.then(([loaded, loadedTypes]) => {
|
|
69
|
+
setProvider(loaded);
|
|
70
|
+
version.current = loaded.version;
|
|
71
|
+
seen.current = configOf(loaded);
|
|
72
|
+
setTypes(loadedTypes);
|
|
73
|
+
})
|
|
74
|
+
.catch((err) => setLoadError(actionErrorMessage(err, "Could not load the DNS provider.")));
|
|
75
|
+
}, [uid]);
|
|
76
|
+
|
|
77
|
+
if (loadError) {
|
|
78
|
+
return <Alert>{loadError}</Alert>;
|
|
79
|
+
}
|
|
80
|
+
if (!provider) {
|
|
81
|
+
return <p className="text-sm text-text-muted">Loading…</p>;
|
|
82
|
+
}
|
|
83
|
+
const spec: DnsProviderTypeInfo | undefined = types.find((candidate) => candidate.type === provider.type);
|
|
84
|
+
const status = dnsProviderStatus(provider);
|
|
85
|
+
|
|
86
|
+
function accept(saved: DnsProvider) {
|
|
87
|
+
version.current = saved.version;
|
|
88
|
+
seen.current = configOf(saved);
|
|
89
|
+
setProvider(saved);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Made against the version shown, so a change someone else saved meanwhile is refused (409) rather than overwritten. The DNS controller moves the
|
|
93
|
+
// version too (every status report that changes, and at least every few minutes), so a refusal reads the config again: when nothing an administrator
|
|
94
|
+
// sets differs, only the status moved and the save is made again against the new version; otherwise the entries stay in the form, the new version is
|
|
95
|
+
// kept, and saving once more replaces the other change (a refusal after that compares against what the first one reported, so the change is told once).
|
|
96
|
+
async function handleSave(input: DnsProviderInput) {
|
|
97
|
+
const sent: number = version.current;
|
|
98
|
+
try {
|
|
99
|
+
accept(await updateDnsProvider(uid, { ...input, version: sent }));
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (!(err instanceof ApiRequestError) || err.status !== 409) {
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
let latest: DnsProvider;
|
|
105
|
+
try {
|
|
106
|
+
latest = await getDnsProvider(uid);
|
|
107
|
+
} catch {
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
if (latest.version === sent) {
|
|
111
|
+
// Refused for another reason than a newer version.
|
|
112
|
+
throw err;
|
|
113
|
+
}
|
|
114
|
+
version.current = latest.version;
|
|
115
|
+
const reported: string = configOf(latest);
|
|
116
|
+
if (reported !== seen.current) {
|
|
117
|
+
seen.current = reported;
|
|
118
|
+
throw new ApiRequestError(
|
|
119
|
+
"This DNS provider was changed by someone else. Your entries are kept; save again to replace their change, or reload the page to see it.",
|
|
120
|
+
409
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
accept(await updateDnsProvider(uid, { ...input, version: latest.version }));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function handleDelete() {
|
|
128
|
+
setDeleting(true);
|
|
129
|
+
setDeleteError(null);
|
|
130
|
+
try {
|
|
131
|
+
await deleteDnsProvider(provider!.uid);
|
|
132
|
+
void navigate(LIST_HREF);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
setDeleteError(actionErrorMessage(err, "Could not delete the DNS provider."));
|
|
135
|
+
setDeleting(false);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<>
|
|
141
|
+
<a href={LIST_HREF} className="text-sm text-primary-dark hover:underline">
|
|
142
|
+
← DNS providers
|
|
143
|
+
</a>
|
|
144
|
+
<h2 className="text-lg font-bold uppercase tracking-wide mt-2 mb-1">
|
|
145
|
+
{provider.name}
|
|
146
|
+
{provider.origin === "installer" && <Badge>Added by the installer</Badge>}
|
|
147
|
+
</h2>
|
|
148
|
+
<div className="flex items-center text-sm text-text-muted mb-4">
|
|
149
|
+
<p>Type: {spec?.label ?? provider.type}</p>
|
|
150
|
+
{spec && !spec.tested && <Badge>Not yet live-tested</Badge>}
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div className="border border-border rounded-sm bg-surface p-4 mb-6 max-w-xl text-sm flex flex-col gap-1">
|
|
154
|
+
<p className="font-semibold">Status: {status.label}</p>
|
|
155
|
+
{status.error && <p className="text-danger">{status.error}</p>}
|
|
156
|
+
<p className="text-text-muted">
|
|
157
|
+
{provider.domainCount === 0 ? "No domains are published through it yet." : `Publishing ${domainsText(provider.domainCount)}.`}
|
|
158
|
+
</p>
|
|
159
|
+
{provider.status?.lastSyncAt && <p className="text-text-muted">Last synchronized {formatTime(provider.status.lastSyncAt)}.</p>}
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{spec ? (
|
|
163
|
+
<DnsProviderForm types={types} provider={provider} submitLabel="Save" onSave={handleSave} />
|
|
164
|
+
) : (
|
|
165
|
+
<p className="text-sm text-text-muted max-w-xl">
|
|
166
|
+
This console doesn’t know this type, so its settings can’t be edited here. A newer admin console can edit it.
|
|
167
|
+
</p>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
<DnsProviderHosts uid={provider.uid} />
|
|
171
|
+
|
|
172
|
+
<DnsProviderOperations uid={provider.uid} />
|
|
173
|
+
|
|
174
|
+
<div className="mt-8">
|
|
175
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={() => setConfirmingDelete(true)}>
|
|
176
|
+
Delete DNS provider
|
|
177
|
+
</Button>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<Modal open={confirmingDelete} onClose={() => setConfirmingDelete(false)} title="Delete DNS provider?">
|
|
181
|
+
<div className="flex flex-col gap-3 text-sm">
|
|
182
|
+
{deleteError && <Alert>{deleteError}</Alert>}
|
|
183
|
+
{provider.domainCount > 0 && (
|
|
184
|
+
<p>
|
|
185
|
+
The server refuses while domains are assigned to it ({domainsText(provider.domainCount)}): assign them to another provider, or to
|
|
186
|
+
manual publishing, first.
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
189
|
+
<p>
|
|
190
|
+
The records it publishes for this server’s own host names are removed from your DNS host, then its external-dns instance and its
|
|
191
|
+
credentials are deleted.
|
|
192
|
+
</p>
|
|
193
|
+
<div className="flex gap-3 justify-end">
|
|
194
|
+
<Button type="button" variant="secondary" className="!w-auto" onClick={() => setConfirmingDelete(false)}>
|
|
195
|
+
Cancel
|
|
196
|
+
</Button>
|
|
197
|
+
<Button type="button" className="!w-auto" loading={deleting} disabled={deleting} onClick={handleDelete}>
|
|
198
|
+
Delete
|
|
199
|
+
</Button>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</Modal>
|
|
203
|
+
</>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useEffect, useState } from "react";
|
|
6
|
+
import {
|
|
7
|
+
DnsControllerState,
|
|
8
|
+
DnsProvider,
|
|
9
|
+
DnsProviderTypeInfo,
|
|
10
|
+
getDnsControllerState,
|
|
11
|
+
listDnsProviders,
|
|
12
|
+
listDnsProviderTypes,
|
|
13
|
+
} from "../../../../lib/admin/dnsProvidersApi.js";
|
|
14
|
+
import Button from "../../../../lib/components/buttons/Button.js";
|
|
15
|
+
import { ApiRequestError } from "../../../../lib/util/api.js";
|
|
16
|
+
import AdminShell, { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
17
|
+
import IntegrationsLayout from "../../../shared/components/admin/integrations/IntegrationsLayout.js";
|
|
18
|
+
import { Badge, dnsProviderStatus, formatTime, isStaleReport } from "../../../shared/components/admin/integrations/dnsProviderParts.js";
|
|
19
|
+
import Alert from "../../../shared/components/admin/ActionAlert.js";
|
|
20
|
+
import { actionErrorMessage } from "../../../shared/components/admin/elevation.js";
|
|
21
|
+
|
|
22
|
+
export default function DnsProvidersPage(props: Omit<AdminShellProps, "active">) {
|
|
23
|
+
return (
|
|
24
|
+
<AdminShell {...props} active="integrations">
|
|
25
|
+
<IntegrationsLayout active="dnsProviders">
|
|
26
|
+
<DnsProvidersList />
|
|
27
|
+
</IntegrationsLayout>
|
|
28
|
+
</AdminShell>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What the DNS controller last said about itself, in a sentence: whether it works with this server, and when it last reported (a warning when that
|
|
33
|
+
* was too long ago: it may be down). */
|
|
34
|
+
function ControllerNotice({ state }: { state: DnsControllerState }) {
|
|
35
|
+
const { handshake, lastReport } = state;
|
|
36
|
+
if (handshake && !handshake.compatible) {
|
|
37
|
+
return (
|
|
38
|
+
<Alert>
|
|
39
|
+
{`DNS controller ${handshake.controllerVersion} can't work with this server (${handshake.reason}). Nothing is published until matching versions are installed.`}
|
|
40
|
+
</Alert>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
// The last time it was heard from at all: a handshake after a report (a restart of either side) shows it alive.
|
|
44
|
+
const heard: string | undefined = [lastReport?.at, handshake?.at].reduce<string | undefined>(
|
|
45
|
+
(latest, at) => (at && (!latest || Date.parse(at) > Date.parse(latest)) ? at : latest),
|
|
46
|
+
undefined
|
|
47
|
+
);
|
|
48
|
+
if (heard && isStaleReport(heard)) {
|
|
49
|
+
return (
|
|
50
|
+
<Alert>
|
|
51
|
+
{`The DNS controller hasn't been heard from since ${formatTime(heard)}; it may be down. Nothing is published or removed until it is back.`}
|
|
52
|
+
</Alert>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
let text: string;
|
|
56
|
+
if (lastReport) {
|
|
57
|
+
text = `DNS controller ${lastReport.controllerVersion}, last report ${formatTime(lastReport.at)}.`;
|
|
58
|
+
} else if (handshake) {
|
|
59
|
+
text = `DNS controller ${handshake.controllerVersion} has connected and not reported yet.`;
|
|
60
|
+
} else {
|
|
61
|
+
text = "The DNS controller hasn’t contacted this server yet, so nothing is published automatically.";
|
|
62
|
+
}
|
|
63
|
+
return <p className="text-sm text-text-muted mb-4">{text}</p>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function DnsProvidersList() {
|
|
67
|
+
const [providers, setProviders] = useState<DnsProvider[] | null>(null);
|
|
68
|
+
const [types, setTypes] = useState<DnsProviderTypeInfo[]>([]);
|
|
69
|
+
const [controller, setController] = useState<DnsControllerState | null>(null);
|
|
70
|
+
const [error, setError] = useState<string | null>(null);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
Promise.all([listDnsProviders(), listDnsProviderTypes()])
|
|
74
|
+
.then(([loaded, loadedTypes]) => {
|
|
75
|
+
setProviders(loaded);
|
|
76
|
+
setTypes(loadedTypes);
|
|
77
|
+
})
|
|
78
|
+
.catch((err) =>
|
|
79
|
+
setError(
|
|
80
|
+
// A server from before DNS publishing has no such route.
|
|
81
|
+
err instanceof ApiRequestError && err.status === 404
|
|
82
|
+
? "This server doesn't offer DNS providers yet. Upgrade it to manage them here."
|
|
83
|
+
: actionErrorMessage(err, "Could not load the DNS providers.")
|
|
84
|
+
)
|
|
85
|
+
);
|
|
86
|
+
// The list stands without it: a server that can't say what the controller reported shows nothing about it.
|
|
87
|
+
getDnsControllerState()
|
|
88
|
+
.then(setController)
|
|
89
|
+
.catch(() => setController(null));
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<>
|
|
94
|
+
<div className="flex items-center justify-between mb-3">
|
|
95
|
+
<h2 className="text-lg font-bold uppercase tracking-wide">DNS providers</h2>
|
|
96
|
+
<a href="/admin/integrations/dns-providers/new">
|
|
97
|
+
<Button type="button" className="!w-auto">
|
|
98
|
+
+ New DNS provider
|
|
99
|
+
</Button>
|
|
100
|
+
</a>
|
|
101
|
+
</div>
|
|
102
|
+
<p className="text-sm text-text-muted mb-4 max-w-2xl">
|
|
103
|
+
The DNS hosting accounts this server publishes DNS records through: each domain’s, and the server’s own host names. A domain is
|
|
104
|
+
assigned to one on its own page.
|
|
105
|
+
</p>
|
|
106
|
+
{controller && <ControllerNotice state={controller} />}
|
|
107
|
+
|
|
108
|
+
{error && <Alert>{error}</Alert>}
|
|
109
|
+
{!providers ? (
|
|
110
|
+
!error && <p className="text-sm text-text-muted">Loading…</p>
|
|
111
|
+
) : providers.length === 0 ? (
|
|
112
|
+
<p className="text-sm text-text-muted">
|
|
113
|
+
No DNS providers yet. Until one is added, every domain’s records are published by hand, from the checklist on its page.
|
|
114
|
+
</p>
|
|
115
|
+
) : (
|
|
116
|
+
<div className="overflow-x-auto">
|
|
117
|
+
<table className="w-full text-sm border-collapse">
|
|
118
|
+
<thead>
|
|
119
|
+
<tr>
|
|
120
|
+
{["Name", "Type", "Zones", "Status", "Domains", ""].map((h) => (
|
|
121
|
+
<th key={h} className="text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border">
|
|
122
|
+
{h}
|
|
123
|
+
</th>
|
|
124
|
+
))}
|
|
125
|
+
</tr>
|
|
126
|
+
</thead>
|
|
127
|
+
<tbody>
|
|
128
|
+
{providers.map((provider) => {
|
|
129
|
+
const spec: DnsProviderTypeInfo | undefined = types.find((candidate) => candidate.type === provider.type);
|
|
130
|
+
const status = dnsProviderStatus(provider);
|
|
131
|
+
return (
|
|
132
|
+
<tr key={provider.uid}>
|
|
133
|
+
<td className="py-2.5 px-2.5 border-b border-border">
|
|
134
|
+
{provider.name}
|
|
135
|
+
{provider.origin === "installer" && <Badge>Added by the installer</Badge>}
|
|
136
|
+
</td>
|
|
137
|
+
<td className="py-2.5 px-2.5 border-b border-border">
|
|
138
|
+
{spec?.label ?? provider.type}
|
|
139
|
+
{spec && !spec.tested && <Badge>Not yet live-tested</Badge>}
|
|
140
|
+
</td>
|
|
141
|
+
<td className="py-2.5 px-2.5 border-b border-border">{provider.zones.join(", ")}</td>
|
|
142
|
+
<td className="py-2.5 px-2.5 border-b border-border">
|
|
143
|
+
{status.label}
|
|
144
|
+
{status.error && <span className="block text-xs text-danger">{status.error}</span>}
|
|
145
|
+
</td>
|
|
146
|
+
<td className="py-2.5 px-2.5 border-b border-border">{provider.domainCount}</td>
|
|
147
|
+
<td className="py-2.5 px-2.5 border-b border-border text-right">
|
|
148
|
+
<a
|
|
149
|
+
href={`/admin/integrations/dns-providers/${encodeURIComponent(provider.uid)}`}
|
|
150
|
+
className="text-primary-dark hover:underline font-medium"
|
|
151
|
+
>
|
|
152
|
+
View
|
|
153
|
+
</a>
|
|
154
|
+
</td>
|
|
155
|
+
</tr>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
158
|
+
</tbody>
|
|
159
|
+
</table>
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
</>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React, { useEffect, useState } from "react";
|
|
6
|
+
import { useRouter } from "@rapidrest/react/client";
|
|
7
|
+
import { createDnsProvider, DnsProviderInput, DnsProviderTypeInfo, listDnsProviderTypes } from "../../../../../lib/admin/dnsProvidersApi.js";
|
|
8
|
+
import AdminShell, { AdminShellProps } from "../../../../shared/components/admin/layout/AdminShell.js";
|
|
9
|
+
import DnsProviderForm from "../../../../shared/components/admin/integrations/DnsProviderForm.js";
|
|
10
|
+
import IntegrationsLayout from "../../../../shared/components/admin/integrations/IntegrationsLayout.js";
|
|
11
|
+
import Alert from "../../../../shared/components/admin/ActionAlert.js";
|
|
12
|
+
import { actionErrorMessage } from "../../../../shared/components/admin/elevation.js";
|
|
13
|
+
|
|
14
|
+
export default function NewDnsProviderPage(props: Omit<AdminShellProps, "active">) {
|
|
15
|
+
return (
|
|
16
|
+
<AdminShell {...props} active="integrations">
|
|
17
|
+
<IntegrationsLayout active="dnsProviders">
|
|
18
|
+
<NewDnsProviderContent />
|
|
19
|
+
</IntegrationsLayout>
|
|
20
|
+
</AdminShell>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function NewDnsProviderContent() {
|
|
25
|
+
const { navigate } = useRouter();
|
|
26
|
+
const [types, setTypes] = useState<DnsProviderTypeInfo[] | null>(null);
|
|
27
|
+
const [error, setError] = useState<string | null>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
listDnsProviderTypes()
|
|
31
|
+
.then(setTypes)
|
|
32
|
+
.catch((err) => setError(actionErrorMessage(err, "Could not load the DNS provider types.")));
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
async function handleSave(input: DnsProviderInput) {
|
|
36
|
+
const created = await createDnsProvider(input);
|
|
37
|
+
void navigate(`/admin/integrations/dns-providers/${encodeURIComponent(created.uid)}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<>
|
|
42
|
+
<h2 className="text-lg font-bold uppercase tracking-wide mb-1">New DNS provider</h2>
|
|
43
|
+
<p className="text-sm text-text-muted mb-5 max-w-2xl">
|
|
44
|
+
A DNS hosting account this server can publish records through. Prefer credentials scoped to these zones alone.
|
|
45
|
+
</p>
|
|
46
|
+
{error ? (
|
|
47
|
+
<Alert>{error}</Alert>
|
|
48
|
+
) : !types ? (
|
|
49
|
+
<p className="text-sm text-text-muted">Loading…</p>
|
|
50
|
+
) : (
|
|
51
|
+
<DnsProviderForm types={types} submitLabel="Create DNS provider" onSave={handleSave} />
|
|
52
|
+
)}
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
7
|
+
import DnsResolverForm from "../../../shared/components/admin/integrations/DnsResolverForm.js";
|
|
8
|
+
import IntegrationSectionPage from "../../../shared/components/admin/integrations/IntegrationSectionPage.js";
|
|
9
|
+
|
|
10
|
+
export default function DnsResolverPage(props: Omit<AdminShellProps, "active">) {
|
|
11
|
+
return (
|
|
12
|
+
<IntegrationSectionPage {...props} area="dnsResolver">
|
|
13
|
+
{(settings, onChange) => <DnsResolverForm settings={settings} onChange={onChange} />}
|
|
14
|
+
</IntegrationSectionPage>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
7
|
+
import GiphyForm from "../../../shared/components/admin/integrations/GiphyForm.js";
|
|
8
|
+
import IntegrationSectionPage from "../../../shared/components/admin/integrations/IntegrationSectionPage.js";
|
|
9
|
+
|
|
10
|
+
export default function GiphyPage(props: Omit<AdminShellProps, "active">) {
|
|
11
|
+
return (
|
|
12
|
+
<IntegrationSectionPage {...props} area="giphy">
|
|
13
|
+
{(settings, onChange) => <GiphyForm settings={settings} onChange={onChange} />}
|
|
14
|
+
</IntegrationSectionPage>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import AdminShell, { AdminShellProps } from "../../shared/components/admin/layout/AdminShell.js";
|
|
7
|
+
import IntegrationsLayout, { INTEGRATION_AREAS } from "../../shared/components/admin/integrations/IntegrationsLayout.js";
|
|
8
|
+
|
|
9
|
+
/** The Integrations overview: each third-party service this deployment can use, with what it is for. */
|
|
10
|
+
export default function IntegrationsPage(props: Omit<AdminShellProps, "active">) {
|
|
11
|
+
return (
|
|
12
|
+
<AdminShell {...props} active="integrations">
|
|
13
|
+
<IntegrationsLayout>
|
|
14
|
+
<p className="text-sm text-text-muted mb-5 max-w-2xl">
|
|
15
|
+
The services this server works with. A setting saved here takes effect at once and overrides the deployment’s configuration until it
|
|
16
|
+
is reset.
|
|
17
|
+
</p>
|
|
18
|
+
<ul aria-label="Integration areas" className="grid gap-3 sm:grid-cols-2">
|
|
19
|
+
{INTEGRATION_AREAS.map((area) => (
|
|
20
|
+
<li key={area.id}>
|
|
21
|
+
<a href={area.href} className="block h-full border border-border rounded-sm bg-surface p-4 hover:border-primary">
|
|
22
|
+
<span className="block font-semibold text-primary-dark">{area.label}</span>
|
|
23
|
+
<span className="block text-sm text-text-muted mt-1">{area.description}</span>
|
|
24
|
+
</a>
|
|
25
|
+
</li>
|
|
26
|
+
))}
|
|
27
|
+
</ul>
|
|
28
|
+
</IntegrationsLayout>
|
|
29
|
+
</AdminShell>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
7
|
+
import PluginRegistryForm from "../../../shared/components/admin/integrations/PluginRegistryForm.js";
|
|
8
|
+
import IntegrationSectionPage from "../../../shared/components/admin/integrations/IntegrationSectionPage.js";
|
|
9
|
+
|
|
10
|
+
export default function PluginRegistryPage(props: Omit<AdminShellProps, "active">) {
|
|
11
|
+
return (
|
|
12
|
+
<IntegrationSectionPage {...props} area="pluginRegistry">
|
|
13
|
+
{(settings, onChange) => <PluginRegistryForm settings={settings} onChange={onChange} />}
|
|
14
|
+
</IntegrationSectionPage>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
7
|
+
import SigningCaForm from "../../../shared/components/admin/integrations/SigningCaForm.js";
|
|
8
|
+
import IntegrationSectionPage from "../../../shared/components/admin/integrations/IntegrationSectionPage.js";
|
|
9
|
+
|
|
10
|
+
export default function SigningCaPage(props: Omit<AdminShellProps, "active">) {
|
|
11
|
+
return (
|
|
12
|
+
<IntegrationSectionPage {...props} area="signingCa">
|
|
13
|
+
{(settings, onChange) => <SigningCaForm settings={settings} onChange={onChange} />}
|
|
14
|
+
</IntegrationSectionPage>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { AdminShellProps } from "../../../shared/components/admin/layout/AdminShell.js";
|
|
7
|
+
import StunForm from "../../../shared/components/admin/integrations/StunForm.js";
|
|
8
|
+
import IntegrationSectionPage from "../../../shared/components/admin/integrations/IntegrationSectionPage.js";
|
|
9
|
+
|
|
10
|
+
export default function StunPage(props: Omit<AdminShellProps, "active">) {
|
|
11
|
+
return (
|
|
12
|
+
<IntegrationSectionPage {...props} area="stun">
|
|
13
|
+
{(settings, onChange) => <StunForm settings={settings} onChange={onChange} />}
|
|
14
|
+
</IntegrationSectionPage>
|
|
15
|
+
);
|
|
16
|
+
}
|