feat(sync): add sync subsystem: SyncManager, OpsServer sync handlers, Sync UI and state, provider groupFilter support, and realtime sync log streaming via TypedSocket
This commit is contained in:
@@ -179,6 +179,7 @@ export const createConnectionAction = connectionsStatePart.createAction<{
|
||||
providerType: interfaces.data.TProviderType;
|
||||
baseUrl: string;
|
||||
token: string;
|
||||
groupFilter?: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
@@ -279,6 +280,7 @@ export const updateConnectionAction = connectionsStatePart.createAction<{
|
||||
name?: string;
|
||||
baseUrl?: string;
|
||||
token?: string;
|
||||
groupFilter?: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
@@ -701,3 +703,225 @@ export const setRefreshIntervalAction = uiStatePart.createAction<{ interval: num
|
||||
return { ...statePartArg.getState(), refreshInterval: dataArg.interval };
|
||||
},
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// Sync State
|
||||
// ============================================================================
|
||||
|
||||
export interface ISyncState {
|
||||
configs: interfaces.data.ISyncConfig[];
|
||||
repoStatuses: interfaces.data.ISyncRepoStatus[];
|
||||
}
|
||||
|
||||
export const syncStatePart = await appState.getStatePart<ISyncState>(
|
||||
'sync',
|
||||
{ configs: [], repoStatuses: [] },
|
||||
'soft',
|
||||
);
|
||||
|
||||
export const fetchSyncConfigsAction = syncStatePart.createAction(async (statePartArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSyncConfigs
|
||||
>('/typedrequest', 'getSyncConfigs');
|
||||
const response = await typedRequest.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), configs: response.configs };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch sync configs:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const createSyncConfigAction = syncStatePart.createAction<{
|
||||
name: string;
|
||||
sourceConnectionId: string;
|
||||
targetConnectionId: string;
|
||||
targetGroupOffset?: string;
|
||||
intervalMinutes?: number;
|
||||
enforceDelete?: boolean;
|
||||
enforceGroupDelete?: boolean;
|
||||
addMirrorHint?: boolean;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_CreateSyncConfig
|
||||
>('/typedrequest', 'createSyncConfig');
|
||||
await typedRequest.fire({ identity: context.identity!, ...dataArg });
|
||||
// Re-fetch
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSyncConfigs
|
||||
>('/typedrequest', 'getSyncConfigs');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), configs: listResp.configs };
|
||||
} catch (err) {
|
||||
console.error('Failed to create sync config:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const updateSyncConfigAction = syncStatePart.createAction<{
|
||||
syncConfigId: string;
|
||||
name?: string;
|
||||
targetGroupOffset?: string;
|
||||
intervalMinutes?: number;
|
||||
enforceDelete?: boolean;
|
||||
enforceGroupDelete?: boolean;
|
||||
addMirrorHint?: boolean;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_UpdateSyncConfig
|
||||
>('/typedrequest', 'updateSyncConfig');
|
||||
await typedRequest.fire({ identity: context.identity!, ...dataArg });
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSyncConfigs
|
||||
>('/typedrequest', 'getSyncConfigs');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), configs: listResp.configs };
|
||||
} catch (err) {
|
||||
console.error('Failed to update sync config:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteSyncConfigAction = syncStatePart.createAction<{
|
||||
syncConfigId: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_DeleteSyncConfig
|
||||
>('/typedrequest', 'deleteSyncConfig');
|
||||
await typedRequest.fire({ identity: context.identity!, ...dataArg });
|
||||
const state = statePartArg.getState();
|
||||
return { ...state, configs: state.configs.filter((c) => c.id !== dataArg.syncConfigId) };
|
||||
} catch (err) {
|
||||
console.error('Failed to delete sync config:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const pauseSyncConfigAction = syncStatePart.createAction<{
|
||||
syncConfigId: string;
|
||||
paused: boolean;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_PauseSyncConfig
|
||||
>('/typedrequest', 'pauseSyncConfig');
|
||||
await typedRequest.fire({ identity: context.identity!, ...dataArg });
|
||||
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSyncConfigs
|
||||
>('/typedrequest', 'getSyncConfigs');
|
||||
const listResp = await listReq.fire({ identity: context.identity! });
|
||||
return { ...statePartArg.getState(), configs: listResp.configs };
|
||||
} catch (err) {
|
||||
console.error('Failed to pause/resume sync config:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const triggerSyncAction = syncStatePart.createAction<{
|
||||
syncConfigId: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_TriggerSync
|
||||
>('/typedrequest', 'triggerSync');
|
||||
await typedRequest.fire({ identity: context.identity!, ...dataArg });
|
||||
return statePartArg.getState();
|
||||
} catch (err) {
|
||||
console.error('Failed to trigger sync:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchSyncRepoStatusesAction = syncStatePart.createAction<{
|
||||
syncConfigId: string;
|
||||
}>(async (statePartArg, dataArg) => {
|
||||
const context = getActionContext();
|
||||
try {
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSyncRepoStatuses
|
||||
>('/typedrequest', 'getSyncRepoStatuses');
|
||||
const response = await typedRequest.fire({ identity: context.identity!, ...dataArg });
|
||||
return { ...statePartArg.getState(), repoStatuses: response.statuses };
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch sync repo statuses:', err);
|
||||
return statePartArg.getState();
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Sync Log — TypedSocket client for server-push entries
|
||||
// ============================================================================
|
||||
|
||||
export async function fetchSyncLogs(limit = 200): Promise<interfaces.data.ISyncLogEntry[]> {
|
||||
const identity = loginStatePart.getState().identity;
|
||||
if (!identity) throw new Error('Not logged in');
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_GetSyncLogs
|
||||
>('/typedrequest', 'getSyncLogs');
|
||||
const response = await typedRequest.fire({ identity, limit });
|
||||
return response.logs;
|
||||
}
|
||||
|
||||
let syncLogSocketInitialized = false;
|
||||
|
||||
/**
|
||||
* Create a TypedSocket client that handles server-push sync log entries.
|
||||
* Dispatches 'gitops-sync-log-entry' custom events on document.
|
||||
* Call once after login.
|
||||
*/
|
||||
export async function initSyncLogSocket(): Promise<void> {
|
||||
if (syncLogSocketInitialized) return;
|
||||
syncLogSocketInitialized = true;
|
||||
|
||||
try {
|
||||
const typedrouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
|
||||
|
||||
typedrouter.addTypedHandler(
|
||||
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushSyncLog>(
|
||||
'pushSyncLog',
|
||||
async (dataArg) => {
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('gitops-sync-log-entry', { detail: dataArg.entry }),
|
||||
);
|
||||
return {};
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
await plugins.typedsocket.TypedSocket.createClient(
|
||||
typedrouter,
|
||||
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl(),
|
||||
{ autoReconnect: true },
|
||||
);
|
||||
} catch (err) {
|
||||
console.error('Failed to init sync log TypedSocket client:', err);
|
||||
syncLogSocketInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Preview Helper
|
||||
// ============================================================================
|
||||
|
||||
export async function previewSync(syncConfigId: string): Promise<{
|
||||
mappings: Array<{ sourceFullPath: string; targetFullPath: string }>;
|
||||
deletions: string[];
|
||||
groupDeletions: string[];
|
||||
}> {
|
||||
const identity = loginStatePart.getState().identity;
|
||||
if (!identity) throw new Error('Not logged in');
|
||||
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||
interfaces.requests.IReq_PreviewSync
|
||||
>('/typedrequest', 'previewSync');
|
||||
const response = await typedRequest.fire({ identity, syncConfigId });
|
||||
return { mappings: response.mappings, deletions: response.deletions, groupDeletions: response.groupDeletions };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user