feat(dns-providers): add provider-agnostic DNS provider form metadata and reusable UI for create/edit flows
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-08 - 13.7.0 - feat(dns-providers)
|
||||||
|
add provider-agnostic DNS provider form metadata and reusable UI for create/edit flows
|
||||||
|
|
||||||
|
- Introduce shared DNS provider type descriptors and credential field metadata to drive provider forms dynamically.
|
||||||
|
- Add a reusable dns-provider-form component and update provider create/edit dialogs to use typed provider selection and credential handling.
|
||||||
|
- Remove Cloudflare-specific ACME helper exposure and clarify provider-agnostic DNS manager and factory documentation for future provider implementations.
|
||||||
|
|
||||||
## 2026-04-08 - 13.6.0 - feat(dns)
|
## 2026-04-08 - 13.6.0 - feat(dns)
|
||||||
add db-backed DNS provider, domain, and record management with ops UI support
|
add db-backed DNS provider, domain, and record management with ops UI support
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.6.0',
|
version: '13.7.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,7 +302,9 @@ export class DnsManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an IConvenientDnsProvider that dispatches each ACME challenge to
|
* Build an IConvenientDnsProvider that dispatches each ACME challenge to
|
||||||
* the right CloudflareDnsProvider based on the challenge's hostName.
|
* the right provider client (whichever provider type owns the parent zone),
|
||||||
|
* based on the challenge's hostName. Provider-agnostic — uses the IDnsProviderClient
|
||||||
|
* interface, so any registered provider implementation works.
|
||||||
* Returned object plugs directly into smartacme's Dns01Handler.
|
* Returned object plugs directly into smartacme's Dns01Handler.
|
||||||
*/
|
*/
|
||||||
public buildAcmeConvenientDnsProvider(): plugins.tsclass.network.IConvenientDnsProvider {
|
public buildAcmeConvenientDnsProvider(): plugins.tsclass.network.IConvenientDnsProvider {
|
||||||
|
|||||||
@@ -27,14 +27,6 @@ export class CloudflareDnsProvider implements IDnsProviderClient {
|
|||||||
this.cfAccount = new plugins.cloudflare.CloudflareAccount(apiToken);
|
this.cfAccount = new plugins.cloudflare.CloudflareAccount(apiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the underlying CloudflareAccount — used by ACME DNS-01
|
|
||||||
* to wrap into a smartacme Dns01Handler.
|
|
||||||
*/
|
|
||||||
public getCloudflareAccount(): plugins.cloudflare.CloudflareAccount {
|
|
||||||
return this.cfAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async testConnection(): Promise<IConnectionTestResult> {
|
public async testConnection(): Promise<IConnectionTestResult> {
|
||||||
try {
|
try {
|
||||||
// Listing zones is the lightest-weight call that proves the token works.
|
// Listing zones is the lightest-weight call that proves the token works.
|
||||||
|
|||||||
@@ -9,6 +9,21 @@ import { CloudflareDnsProvider } from './cloudflare.provider.js';
|
|||||||
* Instantiate a runtime DNS provider client from a stored DnsProviderDoc.
|
* Instantiate a runtime DNS provider client from a stored DnsProviderDoc.
|
||||||
*
|
*
|
||||||
* @throws if the provider type is not supported.
|
* @throws if the provider type is not supported.
|
||||||
|
*
|
||||||
|
* ## Adding a new provider (e.g. Route53)
|
||||||
|
*
|
||||||
|
* 1. **Type union** — extend `TDnsProviderType` in
|
||||||
|
* `ts_interfaces/data/dns-provider.ts` (e.g. `'cloudflare' | 'route53'`).
|
||||||
|
* 2. **Credentials interface** — add `IRoute53Credentials` and append it to
|
||||||
|
* the `TDnsProviderCredentials` discriminated union.
|
||||||
|
* 3. **Descriptor** — append a new entry to `dnsProviderTypeDescriptors` so
|
||||||
|
* the OpsServer UI picks up the new type and renders the right credential
|
||||||
|
* form fields automatically.
|
||||||
|
* 4. **Provider class** — create `ts/dns/providers/route53.provider.ts`
|
||||||
|
* implementing `IDnsProviderClient`.
|
||||||
|
* 5. **Factory case** — add a new `case 'route53':` below. The
|
||||||
|
* `_exhaustive: never` line will fail to compile until you do.
|
||||||
|
* 6. **Index** — re-export the new class from `ts/dns/providers/index.ts`.
|
||||||
*/
|
*/
|
||||||
export function createDnsProvider(
|
export function createDnsProvider(
|
||||||
type: TDnsProviderType,
|
type: TDnsProviderType,
|
||||||
@@ -24,6 +39,8 @@ export function createDnsProvider(
|
|||||||
return new CloudflareDnsProvider(credentials.apiToken);
|
return new CloudflareDnsProvider(credentials.apiToken);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
// If you see a TypeScript error here after extending TDnsProviderType,
|
||||||
|
// add a `case` for the new type above. The `never` enforces exhaustiveness.
|
||||||
const _exhaustive: never = type;
|
const _exhaustive: never = type;
|
||||||
throw new Error(`createDnsProvider: unsupported provider type: ${_exhaustive}`);
|
throw new Error(`createDnsProvider: unsupported provider type: ${_exhaustive}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,3 +71,72 @@ export interface IProviderDomainListing {
|
|||||||
/** Authoritative nameservers reported by the provider. */
|
/** Authoritative nameservers reported by the provider. */
|
||||||
nameservers: string[];
|
nameservers: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schema entry for a single credential field, used by the OpsServer UI to
|
||||||
|
* render a provider's credential form dynamically.
|
||||||
|
*/
|
||||||
|
export interface IDnsProviderCredentialField {
|
||||||
|
/** Key under which the value is stored in the credentials object. */
|
||||||
|
key: string;
|
||||||
|
/** Label shown to the user. */
|
||||||
|
label: string;
|
||||||
|
/** Optional inline help text. */
|
||||||
|
helpText?: string;
|
||||||
|
/** Whether the field must be filled. */
|
||||||
|
required: boolean;
|
||||||
|
/** True for secret fields (rendered as password input, never echoed back). */
|
||||||
|
secret: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata describing a DNS provider type. Drives:
|
||||||
|
* - the OpsServer UI's provider type picker + credential form,
|
||||||
|
* - documentation of which credentials each provider needs,
|
||||||
|
* - end-to-end consistency between the type union, the discriminated
|
||||||
|
* credentials union, the runtime factory, and the form rendering.
|
||||||
|
*
|
||||||
|
* To add a new provider, append a new entry to `dnsProviderTypeDescriptors`
|
||||||
|
* below — and follow the checklist in `ts/dns/providers/factory.ts`.
|
||||||
|
*/
|
||||||
|
export interface IDnsProviderTypeDescriptor {
|
||||||
|
type: TDnsProviderType;
|
||||||
|
/** Human-readable name for the UI. */
|
||||||
|
displayName: string;
|
||||||
|
/** One-line description shown next to the type picker. */
|
||||||
|
description: string;
|
||||||
|
/** Schema for the credentials form. */
|
||||||
|
credentialFields: IDnsProviderCredentialField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for which DNS provider types exist and what
|
||||||
|
* credentials each one needs. Used by both backend and frontend.
|
||||||
|
*/
|
||||||
|
export const dnsProviderTypeDescriptors: ReadonlyArray<IDnsProviderTypeDescriptor> = [
|
||||||
|
{
|
||||||
|
type: 'cloudflare',
|
||||||
|
displayName: 'Cloudflare',
|
||||||
|
description:
|
||||||
|
'Manages records via the Cloudflare API. Provider stays authoritative; dcrouter pushes record changes.',
|
||||||
|
credentialFields: [
|
||||||
|
{
|
||||||
|
key: 'apiToken',
|
||||||
|
label: 'API Token',
|
||||||
|
helpText:
|
||||||
|
'A Cloudflare API token with Zone:Read and DNS:Edit permissions for the target zones.',
|
||||||
|
required: true,
|
||||||
|
secret: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up the descriptor for a given provider type.
|
||||||
|
*/
|
||||||
|
export function getDnsProviderTypeDescriptor(
|
||||||
|
type: TDnsProviderType,
|
||||||
|
): IDnsProviderTypeDescriptor | undefined {
|
||||||
|
return dnsProviderTypeDescriptors.find((d) => d.type === type);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.6.0',
|
version: '13.7.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
216
ts_web/elements/domains/dns-provider-form.ts
Normal file
216
ts_web/elements/domains/dns-provider-form.ts
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
import {
|
||||||
|
DeesElement,
|
||||||
|
html,
|
||||||
|
customElement,
|
||||||
|
type TemplateResult,
|
||||||
|
css,
|
||||||
|
state,
|
||||||
|
property,
|
||||||
|
cssManager,
|
||||||
|
} from '@design.estate/dees-element';
|
||||||
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'dns-provider-form': DnsProviderForm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reactive credential form for a DNS provider. Renders the type picker
|
||||||
|
* and the credential fields for the currently-selected type.
|
||||||
|
*
|
||||||
|
* Provider-agnostic — driven entirely by `dnsProviderTypeDescriptors` from
|
||||||
|
* `ts_interfaces/data/dns-provider.ts`. Adding a new provider type means
|
||||||
|
* appending one entry to the descriptors array; this form picks it up
|
||||||
|
* automatically.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
*
|
||||||
|
* const formEl = document.createElement('dns-provider-form');
|
||||||
|
* formEl.providerName = 'My provider';
|
||||||
|
* // ... pass element into a DeesModal as content ...
|
||||||
|
* // on submit:
|
||||||
|
* const data = formEl.collectData();
|
||||||
|
* // → { name, type, credentials }
|
||||||
|
*
|
||||||
|
* In edit mode, set `lockType = true` so the user cannot change provider
|
||||||
|
* type after creation (credentials shapes don't transfer between types).
|
||||||
|
*/
|
||||||
|
@customElement('dns-provider-form')
|
||||||
|
export class DnsProviderForm extends DeesElement {
|
||||||
|
/** Pre-populated provider name. */
|
||||||
|
@property({ type: String })
|
||||||
|
accessor providerName: string = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Currently selected provider type. Initialized to the first descriptor;
|
||||||
|
* caller can override before mounting (e.g. for edit dialogs).
|
||||||
|
*/
|
||||||
|
@state()
|
||||||
|
accessor selectedType: interfaces.data.TDnsProviderType =
|
||||||
|
interfaces.data.dnsProviderTypeDescriptors[0]?.type ?? 'cloudflare';
|
||||||
|
|
||||||
|
/** When true, hide the type picker — used in edit dialogs. */
|
||||||
|
@property({ type: Boolean })
|
||||||
|
accessor lockType: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Help text shown above credentials. Useful for edit dialogs to indicate
|
||||||
|
* that fields can be left blank to keep current values.
|
||||||
|
*/
|
||||||
|
@property({ type: String })
|
||||||
|
accessor credentialsHint: string = '';
|
||||||
|
|
||||||
|
/** Internal map of credential field values, keyed by the descriptor's `key`. */
|
||||||
|
@state()
|
||||||
|
accessor credentialValues: Record<string, string> = {};
|
||||||
|
|
||||||
|
public static styles = [
|
||||||
|
cssManager.defaultStyles,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpText {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-top: -6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typeDescription {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
margin: 4px 0 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: ${cssManager.bdTheme('#f3f4f6', '#1f2937')};
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.credentialsHint {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
public render(): TemplateResult {
|
||||||
|
const descriptors = interfaces.data.dnsProviderTypeDescriptors;
|
||||||
|
const descriptor = interfaces.data.getDnsProviderTypeDescriptor(this.selectedType);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dees-form>
|
||||||
|
<div class="field">
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'name'}
|
||||||
|
.label=${'Provider name'}
|
||||||
|
.value=${this.providerName}
|
||||||
|
.required=${true}
|
||||||
|
></dees-input-text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${this.lockType
|
||||||
|
? html`
|
||||||
|
<div class="field">
|
||||||
|
<dees-input-text
|
||||||
|
.key=${'__type_display'}
|
||||||
|
.label=${'Type'}
|
||||||
|
.value=${descriptor?.displayName ?? this.selectedType}
|
||||||
|
.disabled=${true}
|
||||||
|
></dees-input-text>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<div class="field">
|
||||||
|
<dees-input-dropdown
|
||||||
|
.key=${'__type'}
|
||||||
|
.label=${'Provider type'}
|
||||||
|
.options=${descriptors.map((d) => ({ option: d.displayName, key: d.type }))}
|
||||||
|
.selectedOption=${descriptor
|
||||||
|
? { option: descriptor.displayName, key: descriptor.type }
|
||||||
|
: undefined}
|
||||||
|
@selectedOption=${(e: CustomEvent) => {
|
||||||
|
const newType = (e.detail as any)?.key as
|
||||||
|
| interfaces.data.TDnsProviderType
|
||||||
|
| undefined;
|
||||||
|
if (newType && newType !== this.selectedType) {
|
||||||
|
this.selectedType = newType;
|
||||||
|
this.credentialValues = {};
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></dees-input-dropdown>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
${descriptor
|
||||||
|
? html`
|
||||||
|
<div class="typeDescription">${descriptor.description}</div>
|
||||||
|
${this.credentialsHint
|
||||||
|
? html`<div class="credentialsHint">${this.credentialsHint}</div>`
|
||||||
|
: ''}
|
||||||
|
${descriptor.credentialFields.map(
|
||||||
|
(f) => html`
|
||||||
|
<div class="field">
|
||||||
|
<dees-input-text
|
||||||
|
.key=${f.key}
|
||||||
|
.label=${f.label}
|
||||||
|
.required=${f.required && !this.lockType}
|
||||||
|
></dees-input-text>
|
||||||
|
${f.helpText ? html`<div class="helpText">${f.helpText}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
`
|
||||||
|
: html`<p>No provider types registered.</p>`}
|
||||||
|
</dees-form>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the form values and assemble the create/update payload.
|
||||||
|
* Returns the typed credentials object built from the descriptor's keys.
|
||||||
|
*/
|
||||||
|
public async collectData(): Promise<{
|
||||||
|
name: string;
|
||||||
|
type: interfaces.data.TDnsProviderType;
|
||||||
|
credentials: interfaces.data.TDnsProviderCredentials;
|
||||||
|
credentialsTouched: boolean;
|
||||||
|
} | null> {
|
||||||
|
const form = this.shadowRoot?.querySelector('dees-form') as any;
|
||||||
|
if (!form) return null;
|
||||||
|
const data = await form.collectFormData();
|
||||||
|
const descriptor = interfaces.data.getDnsProviderTypeDescriptor(this.selectedType);
|
||||||
|
if (!descriptor) return null;
|
||||||
|
|
||||||
|
// Build the credentials object from the descriptor's field keys.
|
||||||
|
const credsBody: Record<string, string> = {};
|
||||||
|
let credentialsTouched = false;
|
||||||
|
for (const f of descriptor.credentialFields) {
|
||||||
|
const value = data[f.key];
|
||||||
|
if (value !== undefined && value !== null && String(value).length > 0) {
|
||||||
|
credsBody[f.key] = String(value);
|
||||||
|
credentialsTouched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The discriminator goes on the credentials object so the backend
|
||||||
|
// factory and the discriminated union both stay happy.
|
||||||
|
const credentials = {
|
||||||
|
type: this.selectedType,
|
||||||
|
...credsBody,
|
||||||
|
} as unknown as interfaces.data.TDnsProviderCredentials;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: String(data.name ?? ''),
|
||||||
|
type: this.selectedType,
|
||||||
|
credentials,
|
||||||
|
credentialsTouched,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export * from './dns-provider-form.js';
|
||||||
export * from './ops-view-providers.js';
|
export * from './ops-view-providers.js';
|
||||||
export * from './ops-view-domains.js';
|
export * from './ops-view-domains.js';
|
||||||
export * from './ops-view-dns.js';
|
export * from './ops-view-dns.js';
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
import * as appstate from '../../appstate.js';
|
import * as appstate from '../../appstate.js';
|
||||||
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
import * as interfaces from '../../../dist_ts_interfaces/index.js';
|
||||||
import { viewHostCss } from '../shared/css.js';
|
import { viewHostCss } from '../shared/css.js';
|
||||||
|
import './dns-provider-form.js';
|
||||||
|
import type { DnsProviderForm } from './dns-provider-form.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -80,12 +82,12 @@ export class OpsViewProviders extends DeesElement {
|
|||||||
<div class="providersContainer">
|
<div class="providersContainer">
|
||||||
<dees-table
|
<dees-table
|
||||||
.heading1=${'Providers'}
|
.heading1=${'Providers'}
|
||||||
.heading2=${'External DNS provider accounts (Cloudflare, etc.)'}
|
.heading2=${'External DNS provider accounts'}
|
||||||
.data=${providers}
|
.data=${providers}
|
||||||
.showColumnFilters=${true}
|
.showColumnFilters=${true}
|
||||||
.displayFunction=${(p: interfaces.data.IDnsProviderPublic) => ({
|
.displayFunction=${(p: interfaces.data.IDnsProviderPublic) => ({
|
||||||
Name: p.name,
|
Name: p.name,
|
||||||
Type: p.type,
|
Type: this.providerTypeLabel(p.type),
|
||||||
Status: this.renderStatusBadge(p.status),
|
Status: this.renderStatusBadge(p.status),
|
||||||
'Last Tested': p.lastTestedAt ? new Date(p.lastTestedAt).toLocaleString() : 'never',
|
'Last Tested': p.lastTestedAt ? new Date(p.lastTestedAt).toLocaleString() : 'never',
|
||||||
Error: p.lastError || '-',
|
Error: p.lastError || '-',
|
||||||
@@ -147,34 +149,39 @@ export class OpsViewProviders extends DeesElement {
|
|||||||
return html`<span class="statusBadge ${status}">${status}</span>`;
|
return html`<span class="statusBadge ${status}">${status}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private providerTypeLabel(type: interfaces.data.TDnsProviderType): string {
|
||||||
|
return interfaces.data.getDnsProviderTypeDescriptor(type)?.displayName ?? type;
|
||||||
|
}
|
||||||
|
|
||||||
private async showCreateDialog() {
|
private async showCreateDialog() {
|
||||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
|
||||||
|
const formEl = document.createElement('dns-provider-form') as DnsProviderForm;
|
||||||
DeesModal.createAndShow({
|
DeesModal.createAndShow({
|
||||||
heading: 'Add DNS Provider',
|
heading: 'Add DNS Provider',
|
||||||
content: html`
|
content: html`${formEl}`,
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .key=${'name'} .label=${'Provider name'} .required=${true}></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'apiToken'}
|
|
||||||
.label=${'Cloudflare API token'}
|
|
||||||
.required=${true}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
{
|
{
|
||||||
name: 'Create',
|
name: 'Create',
|
||||||
action: async (modalArg: any) => {
|
action: async (modalArg: any) => {
|
||||||
const form = modalArg.shadowRoot
|
const data = await formEl.collectData();
|
||||||
?.querySelector('.content')
|
if (!data) return;
|
||||||
?.querySelector('dees-form');
|
if (!data.name) {
|
||||||
if (!form) return;
|
DeesToast.show({ message: 'Name is required', type: 'warning', duration: 2500 });
|
||||||
const data = await form.collectFormData();
|
return;
|
||||||
|
}
|
||||||
|
if (!data.credentialsTouched) {
|
||||||
|
DeesToast.show({
|
||||||
|
message: 'Fill in the provider credentials',
|
||||||
|
type: 'warning',
|
||||||
|
duration: 2500,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
await appstate.domainsStatePart.dispatchAction(appstate.createDnsProviderAction, {
|
await appstate.domainsStatePart.dispatchAction(appstate.createDnsProviderAction, {
|
||||||
name: String(data.name),
|
name: data.name,
|
||||||
type: 'cloudflare',
|
type: data.type,
|
||||||
credentials: { type: 'cloudflare', apiToken: String(data.apiToken) },
|
credentials: data.credentials,
|
||||||
});
|
});
|
||||||
modalArg.destroy();
|
modalArg.destroy();
|
||||||
},
|
},
|
||||||
@@ -185,34 +192,28 @@ export class OpsViewProviders extends DeesElement {
|
|||||||
|
|
||||||
private async showEditDialog(provider: interfaces.data.IDnsProviderPublic) {
|
private async showEditDialog(provider: interfaces.data.IDnsProviderPublic) {
|
||||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||||
|
const formEl = document.createElement('dns-provider-form') as DnsProviderForm;
|
||||||
|
formEl.providerName = provider.name;
|
||||||
|
formEl.selectedType = provider.type;
|
||||||
|
formEl.lockType = true;
|
||||||
|
formEl.credentialsHint =
|
||||||
|
'Leave credential fields blank to keep the current values. Fill them to rotate.';
|
||||||
DeesModal.createAndShow({
|
DeesModal.createAndShow({
|
||||||
heading: `Edit Provider: ${provider.name}`,
|
heading: `Edit Provider: ${provider.name}`,
|
||||||
content: html`
|
content: html`${formEl}`,
|
||||||
<dees-form>
|
|
||||||
<dees-input-text .key=${'name'} .label=${'Provider name'} .value=${provider.name}></dees-input-text>
|
|
||||||
<dees-input-text
|
|
||||||
.key=${'apiToken'}
|
|
||||||
.label=${'New API token (leave blank to keep current)'}
|
|
||||||
></dees-input-text>
|
|
||||||
</dees-form>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
|
||||||
{
|
{
|
||||||
name: 'Save',
|
name: 'Save',
|
||||||
action: async (modalArg: any) => {
|
action: async (modalArg: any) => {
|
||||||
const form = modalArg.shadowRoot
|
const data = await formEl.collectData();
|
||||||
?.querySelector('.content')
|
if (!data) return;
|
||||||
?.querySelector('dees-form');
|
|
||||||
if (!form) return;
|
|
||||||
const data = await form.collectFormData();
|
|
||||||
const apiToken = data.apiToken ? String(data.apiToken) : '';
|
|
||||||
await appstate.domainsStatePart.dispatchAction(appstate.updateDnsProviderAction, {
|
await appstate.domainsStatePart.dispatchAction(appstate.updateDnsProviderAction, {
|
||||||
id: provider.id,
|
id: provider.id,
|
||||||
name: String(data.name),
|
name: data.name || provider.name,
|
||||||
credentials: apiToken
|
// Only send credentials if the user actually entered something —
|
||||||
? { type: 'cloudflare', apiToken }
|
// otherwise we keep the current secret untouched.
|
||||||
: undefined,
|
credentials: data.credentialsTouched ? data.credentials : undefined,
|
||||||
});
|
});
|
||||||
modalArg.destroy();
|
modalArg.destroy();
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user