Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31a6510d8b | |||
| b5e760ae07 |
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-17 - 6.7.0 - feat(remote-ingress)
|
||||
Support auto-derived effective listen ports, make listenPorts optional, add toggle action and refine remote ingress creation/management UI
|
||||
|
||||
- Add effectiveListenPorts?: number[] to IRemoteIngress interface (present in API responses)
|
||||
- Make createRemoteIngressAction.listenPorts optional and update creation modal to allow empty ports (auto-derived)
|
||||
- Add toggleRemoteIngressAction to enable/disable remote ingress edges and wire up Enable/Disable row/context-menu actions
|
||||
- Update getPortsHtml to prefer manual listenPorts, fall back to effectiveListenPorts, show '(auto)' when derived and 'none' when no ports
|
||||
- Standardize UI actions to use inRow/contextmenu and actionFunc signatures; update create modal to use explicit Cancel/Create menu options and collect form data programmatically
|
||||
|
||||
## 2026-02-17 - 6.6.1 - fix(icons)
|
||||
standardize icon identifiers to lucide-prefixed names across operational views
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "6.6.1",
|
||||
"version": "6.7.0",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '6.6.1',
|
||||
version: '6.7.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface IRemoteIngress {
|
||||
tags?: string[];
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
/** Effective ports derived from route configs — only present in API responses. */
|
||||
effectiveListenPorts?: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '6.6.1',
|
||||
version: '6.7.0',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
@@ -821,7 +821,7 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn
|
||||
|
||||
export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
name: string;
|
||||
listenPorts: number[];
|
||||
listenPorts?: number[];
|
||||
tags?: string[];
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
@@ -924,6 +924,34 @@ export const clearNewEdgeSecretAction = remoteIngressStatePart.createAction(
|
||||
}
|
||||
);
|
||||
|
||||
export const toggleRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_UpdateRemoteIngress
|
||||
>('/typedrequest', 'updateRemoteIngress');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
id: dataArg.id,
|
||||
enabled: dataArg.enabled,
|
||||
});
|
||||
|
||||
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||
return statePartArg.getState();
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to toggle edge',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Combined refresh action for efficient polling
|
||||
async function dispatchCombinedRefreshAction() {
|
||||
const context = getActionContext();
|
||||
|
||||
@@ -187,7 +187,7 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
name: edge.name,
|
||||
status: this.getEdgeStatusHtml(edge),
|
||||
publicIp: this.getEdgePublicIp(edge.id),
|
||||
ports: this.getPortsHtml(edge.listenPorts),
|
||||
ports: this.getPortsHtml(edge),
|
||||
tunnels: this.getEdgeTunnelCount(edge.id),
|
||||
lastHeartbeat: this.getLastHeartbeat(edge.id),
|
||||
})}
|
||||
@@ -198,42 +198,80 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
type: ['header'],
|
||||
actionFunc: async () => {
|
||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
||||
const result = await DeesModal.createAndShow({
|
||||
const modal = await DeesModal.createAndShow({
|
||||
heading: 'Create Edge Node',
|
||||
content: html`
|
||||
<dees-form>
|
||||
<dees-input-text .key=${'name'} .label=${'Name'} .required=${true}></dees-input-text>
|
||||
<dees-input-text .key=${'listenPorts'} .label=${'Listen Ports (comma-separated)'} .required=${true} .value=${'443,25'}></dees-input-text>
|
||||
<dees-input-text .key=${'listenPorts'} .label=${'Listen Ports (comma-separated, auto-derived if empty)'}></dees-input-text>
|
||||
<dees-input-text .key=${'tags'} .label=${'Tags (comma-separated, optional)'}></dees-input-text>
|
||||
</dees-form>
|
||||
`,
|
||||
menuOptions: [],
|
||||
});
|
||||
if (result) {
|
||||
const formData = result as any;
|
||||
const ports = (formData.name ? formData.listenPorts : '443')
|
||||
.split(',')
|
||||
.map((p: string) => parseInt(p.trim(), 10))
|
||||
.filter((p: number) => !isNaN(p));
|
||||
const tags = formData.tags
|
||||
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
await appstate.remoteIngressStatePart.dispatchAction(
|
||||
appstate.createRemoteIngressAction,
|
||||
menuOptions: [
|
||||
{
|
||||
name: formData.name,
|
||||
listenPorts: ports,
|
||||
tags,
|
||||
name: 'Cancel',
|
||||
iconName: 'lucide:x',
|
||||
action: async (modalArg: any) => await modalArg.destroy(),
|
||||
},
|
||||
);
|
||||
}
|
||||
{
|
||||
name: 'Create',
|
||||
iconName: 'lucide:plus',
|
||||
action: async (modalArg: any) => {
|
||||
const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
|
||||
if (!form) return;
|
||||
const formData = await form.collectFormData();
|
||||
const name = formData.name;
|
||||
if (!name) return;
|
||||
const portsStr = formData.listenPorts?.trim();
|
||||
const listenPorts = portsStr
|
||||
? portsStr.split(',').map((p: string) => parseInt(p.trim(), 10)).filter((p: number) => !isNaN(p))
|
||||
: undefined;
|
||||
const tags = formData.tags
|
||||
? formData.tags.split(',').map((t: string) => t.trim()).filter(Boolean)
|
||||
: undefined;
|
||||
await appstate.remoteIngressStatePart.dispatchAction(
|
||||
appstate.createRemoteIngressAction,
|
||||
{ name, listenPorts, tags },
|
||||
);
|
||||
await modalArg.destroy();
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Enable',
|
||||
iconName: 'lucide:play',
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionRelevancyCheckFunc: (actionData: any) => !actionData.item.enabled,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||
await appstate.remoteIngressStatePart.dispatchAction(
|
||||
appstate.toggleRemoteIngressAction,
|
||||
{ id: edge.id, enabled: true },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Disable',
|
||||
iconName: 'lucide:pause',
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionRelevancyCheckFunc: (actionData: any) => actionData.item.enabled,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||
await appstate.remoteIngressStatePart.dispatchAction(
|
||||
appstate.toggleRemoteIngressAction,
|
||||
{ id: edge.id, enabled: false },
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Regenerate Secret',
|
||||
iconName: 'lucide:key',
|
||||
type: ['row'],
|
||||
action: async (edge: interfaces.data.IRemoteIngress) => {
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||
await appstate.remoteIngressStatePart.dispatchAction(
|
||||
appstate.regenerateRemoteIngressSecretAction,
|
||||
edge.id,
|
||||
@@ -243,8 +281,9 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
{
|
||||
name: 'Delete',
|
||||
iconName: 'lucide:trash2',
|
||||
type: ['row'],
|
||||
action: async (edge: interfaces.data.IRemoteIngress) => {
|
||||
type: ['inRow', 'contextmenu'] as any,
|
||||
actionFunc: async (actionData: any) => {
|
||||
const edge = actionData.item as interfaces.data.IRemoteIngress;
|
||||
await appstate.remoteIngressStatePart.dispatchAction(
|
||||
appstate.deleteRemoteIngressAction,
|
||||
edge.id,
|
||||
@@ -277,8 +316,14 @@ export class OpsViewRemoteIngress extends DeesElement {
|
||||
return status?.publicIp || '-';
|
||||
}
|
||||
|
||||
private getPortsHtml(ports: number[]): TemplateResult {
|
||||
return html`<div class="portsDisplay">${ports.map(p => html`<span class="portBadge">${p}</span>`)}</div>`;
|
||||
private getPortsHtml(edge: interfaces.data.IRemoteIngress): TemplateResult {
|
||||
const hasManualPorts = edge.listenPorts && edge.listenPorts.length > 0;
|
||||
const ports = hasManualPorts ? edge.listenPorts : (edge.effectiveListenPorts || []);
|
||||
const isAuto = !hasManualPorts && ports.length > 0;
|
||||
if (ports.length === 0) {
|
||||
return html`<span style="color: var(--text-muted, #6b7280); font-size: 12px;">none</span>`;
|
||||
}
|
||||
return html`<div class="portsDisplay">${ports.map(p => html`<span class="portBadge">${p}</span>`)}${isAuto ? html`<span style="font-size: 11px; color: var(--text-muted, #6b7280); align-self: center;">(auto)</span>` : ''}</div>`;
|
||||
}
|
||||
|
||||
private getEdgeTunnelCount(edgeId: string): number {
|
||||
|
||||
Reference in New Issue
Block a user