import {
DeesElement,
css,
cssManager,
customElement,
html,
property,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import { type IStatsTile } from '@design.estate/dees-catalog';
import * as appstate from '../../appstate.js';
import * as interfaces from '../../../dist_ts_interfaces/index.js';
import { viewHostCss } from '../shared/css.js';
import {
getRouteOwner,
routeMatchesOwner,
type TRouteOwnerFilter,
} from './route-view-filters.js';
import {
getSpecialForwardDomains,
getSpecialForwardTarget,
letsEncryptHttp01ManagedRouteKind,
showLetsEncryptHttp01ForwardDialog,
} from './special-forward-dialog.js';
@customElement('ops-view-special-forwards')
export class OpsViewSpecialForwards extends DeesElement {
@property({ type: Boolean, reflect: true })
accessor embedded = false;
@property({ type: String })
accessor ownerFilter: TRouteOwnerFilter = 'all';
@state()
accessor routeState: appstate.IRouteManagementState = appstate.routeManagementStatePart.getState()!;
@state()
accessor profilesTargetsState: appstate.IProfilesTargetsState =
appstate.profilesTargetsStatePart.getState()!;
constructor() {
super();
this.rxSubscriptions.push(
appstate.routeManagementStatePart.select().subscribe((routeState) => {
this.routeState = routeState;
}),
appstate.profilesTargetsStatePart.select().subscribe((profilesTargetsState) => {
this.profilesTargetsState = profilesTargetsState;
}),
);
}
public static styles = [
cssManager.defaultStyles,
viewHostCss,
css`
:host {
display: block;
}
:host([embedded]) {
margin: 0;
max-width: none;
padding: 0;
}
.specialForwardsContainer {
display: flex;
flex-direction: column;
gap: 20px;
}
.explanation {
padding: 14px 16px;
border: 1px solid var(--dees-color-border-default);
border-radius: 8px;
color: var(--dees-color-text-secondary);
font-size: 14px;
line-height: 1.5;
}
.explanation strong {
color: var(--dees-color-text-primary);
}
.error {
padding: 12px 16px;
border: 1px solid #ef4444;
border-radius: 8px;
color: #ef4444;
}
`,
];
public render(): TemplateResult {
const forwards = this.routeState.mergedRoutes.filter((route) => {
return route.metadata?.managedRouteKind === letsEncryptHttp01ManagedRouteKind
&& routeMatchesOwner(route, this.ownerFilter);
});
const enabledCount = forwards.filter((route) => route.enabled).length;
const disabledCount = forwards.length - enabledCount;
const readOnlyCount = forwards.filter((route) => getRouteOwner(route) !== 'operator').length;
const statsTiles: IStatsTile[] = [
{
id: 'matchingSpecialForwards',
title: 'Matching',
type: 'number',
value: forwards.length,
icon: 'lucide:route',
description: 'HTTP-01 forwards in this owner scope',
color: '#3b82f6',
},
{
id: 'enabledSpecialForwards',
title: 'Enabled',
type: 'number',
value: enabledCount,
icon: 'lucide:circleCheck',
description: 'Currently forwarding challenges',
color: '#22c55e',
},
{
id: 'disabledSpecialForwards',
title: 'Disabled',
type: 'number',
value: disabledCount,
icon: 'lucide:circlePause',
description: 'Configured but inactive',
color: disabledCount > 0 ? '#f59e0b' : '#6b7280',
},
{
id: 'readOnlySpecialForwards',
title: 'Read-only',
type: 'number',
value: readOnlyCount,
icon: 'lucide:lockKeyhole',
description: 'Managed by gateway clients or system',
color: '#8b5cf6',
},
];
return html`
${this.embedded ? '' : html`
/.well-known/acme-challenge/* on public port 80. DCRouter manages the
path, priority, protocol, and PUBLIC source profile; you choose domains and a target.
This removes the managed HTTP-01 route for ${getSpecialForwardDomains(route).join(', ')}.
`, menuOptions: [ { name: 'Cancel', iconName: 'lucide:x', action: async (modalArg: any) => modalArg.destroy(), }, { name: 'Delete', iconName: 'lucide:trash2', action: async (modalArg: any) => { await appstate.routeManagementStatePart.dispatchAction( appstate.deleteLetsEncryptHttp01ForwardAction, route.id, ); await modalArg.destroy(); }, }, ], }); } }