feat(remoteingress): add Remote Ingress hub and management for edge tunnel nodes, including backend managers, tunnel hub integration, opsserver handlers, typedrequest APIs, and web UI
This commit is contained in:
@@ -116,7 +116,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
||||
// Determine initial view from URL path
|
||||
const getInitialView = (): string => {
|
||||
const path = typeof window !== 'undefined' ? window.location.pathname : '/';
|
||||
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates'];
|
||||
const validViews = ['overview', 'network', 'emails', 'logs', 'configuration', 'security', 'certificates', 'remoteingress'];
|
||||
const segments = path.split('/').filter(Boolean);
|
||||
const view = segments[0];
|
||||
return validViews.includes(view) ? view : 'overview';
|
||||
@@ -192,6 +192,34 @@ export const certificateStatePart = await appState.getStatePart<ICertificateStat
|
||||
'soft'
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Remote Ingress State
|
||||
// ============================================================================
|
||||
|
||||
export interface IRemoteIngressState {
|
||||
edges: interfaces.data.IRemoteIngress[];
|
||||
statuses: interfaces.data.IRemoteIngressStatus[];
|
||||
selectedEdgeId: string | null;
|
||||
newEdgeSecret: string | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngressState>(
|
||||
'remoteIngress',
|
||||
{
|
||||
edges: [],
|
||||
statuses: [],
|
||||
selectedEdgeId: null,
|
||||
newEdgeSecret: null,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: 0,
|
||||
},
|
||||
'soft'
|
||||
);
|
||||
|
||||
// Actions for state management
|
||||
interface IActionContext {
|
||||
identity: interfaces.data.IIdentity | null;
|
||||
@@ -378,6 +406,13 @@ export const setActiveViewAction = uiStatePart.createAction<string>(async (state
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// If switching to remoteingress view, ensure we fetch edge data
|
||||
if (viewName === 'remoteingress' && currentState.activeView !== 'remoteingress') {
|
||||
setTimeout(() => {
|
||||
remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
activeView: viewName,
|
||||
@@ -745,6 +780,150 @@ export const reprovisionCertificateAction = certificateStatePart.createAction<st
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Remote Ingress Actions
|
||||
// ============================================================================
|
||||
|
||||
export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const edgesRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetRemoteIngresses
|
||||
>('/typedrequest', 'getRemoteIngresses');
|
||||
|
||||
const statusRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetRemoteIngressStatus
|
||||
>('/typedrequest', 'getRemoteIngressStatus');
|
||||
|
||||
const [edgesResponse, statusResponse] = await Promise.all([
|
||||
edgesRequest.fire({ identity: context.identity }),
|
||||
statusRequest.fire({ identity: context.identity }),
|
||||
]);
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
edges: edgesResponse.edges,
|
||||
statuses: statusResponse.statuses,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
isLoading: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch remote ingress data',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const createRemoteIngressAction = remoteIngressStatePart.createAction<{
|
||||
name: string;
|
||||
listenPorts: number[];
|
||||
tags?: string[];
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_CreateRemoteIngress
|
||||
>('/typedrequest', 'createRemoteIngress');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
name: dataArg.name,
|
||||
listenPorts: dataArg.listenPorts,
|
||||
tags: dataArg.tags,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// Refresh the list and store the new secret for display
|
||||
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||
return {
|
||||
...statePartArg.getState(),
|
||||
newEdgeSecret: response.edge.secret,
|
||||
};
|
||||
}
|
||||
|
||||
return currentState;
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to create edge',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteRemoteIngressAction = remoteIngressStatePart.createAction<string>(
|
||||
async (statePartArg, edgeId) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DeleteRemoteIngress
|
||||
>('/typedrequest', 'deleteRemoteIngress');
|
||||
|
||||
await request.fire({
|
||||
identity: context.identity,
|
||||
id: edgeId,
|
||||
});
|
||||
|
||||
await remoteIngressStatePart.dispatchAction(fetchRemoteIngressAction, null);
|
||||
return statePartArg.getState();
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete edge',
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
|
||||
async (statePartArg, edgeId) => {
|
||||
const context = getActionContext();
|
||||
const currentState = statePartArg.getState();
|
||||
|
||||
try {
|
||||
const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_RegenerateRemoteIngressSecret
|
||||
>('/typedrequest', 'regenerateRemoteIngressSecret');
|
||||
|
||||
const response = await request.fire({
|
||||
identity: context.identity,
|
||||
id: edgeId,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
return {
|
||||
...currentState,
|
||||
newEdgeSecret: response.secret,
|
||||
};
|
||||
}
|
||||
|
||||
return currentState;
|
||||
} catch (error) {
|
||||
return {
|
||||
...currentState,
|
||||
error: error instanceof Error ? error.message : 'Failed to regenerate secret',
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const clearNewEdgeSecretAction = remoteIngressStatePart.createAction(
|
||||
async (statePartArg) => {
|
||||
return {
|
||||
...statePartArg.getState(),
|
||||
newEdgeSecret: null,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// Combined refresh action for efficient polling
|
||||
async function dispatchCombinedRefreshAction() {
|
||||
const context = getActionContext();
|
||||
|
||||
Reference in New Issue
Block a user