2026-04-08 11:11:53 +00:00
|
|
|
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 = '';
|
|
|
|
|
|
|
|
|
|
/**
|
2026-04-08 14:54:49 +00:00
|
|
|
* Currently selected provider type. Initialized to the first user-creatable
|
|
|
|
|
* descriptor; caller can override before mounting (e.g. for edit dialogs).
|
|
|
|
|
* The built-in 'dcrouter' pseudo-provider is excluded from the picker —
|
|
|
|
|
* operators cannot create another one.
|
2026-04-08 11:11:53 +00:00
|
|
|
*/
|
|
|
|
|
@state()
|
|
|
|
|
accessor selectedType: interfaces.data.TDnsProviderType =
|
2026-04-08 14:54:49 +00:00
|
|
|
interfaces.data.dnsProviderTypeDescriptors.find((d) => d.type !== 'dcrouter')?.type ??
|
|
|
|
|
'cloudflare';
|
2026-04-08 11:11:53 +00:00
|
|
|
|
|
|
|
|
/** 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 {
|
2026-04-08 14:54:49 +00:00
|
|
|
// Exclude the built-in 'dcrouter' pseudo-provider from the type picker —
|
|
|
|
|
// operators cannot create another one, it's surfaced at read time by the
|
|
|
|
|
// backend handler instead.
|
|
|
|
|
const descriptors = interfaces.data.dnsProviderTypeDescriptors.filter(
|
|
|
|
|
(d) => d.type !== 'dcrouter',
|
|
|
|
|
);
|
2026-04-08 11:11:53 +00:00
|
|
|
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,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|