feat(appstore): add App Store install and upgrade workflows

This commit is contained in:
2026-05-26 11:06:21 +00:00
parent bfda4b4ca1
commit 2b65ddc193
10 changed files with 1656 additions and 43 deletions
+210
View File
@@ -35,6 +35,63 @@ export interface IDataState {
backups?: any[];
}
export type TAppStoreUpgradeStatus = 'running' | 'success' | 'failed';
export type TAppStoreUpgradeStep =
| 'queued'
| 'validating'
| 'migration'
| 'applying'
| 'updating-service'
| 'pushing-config'
| 'complete'
| 'failed';
export interface IAppStoreUpgradeChange {
field: string;
currentValue: string;
targetValue: string;
}
export interface IAppStoreUpgradePreview {
serviceId: string;
serviceName: string;
appTemplateId: string;
fromVersion: string;
targetVersion: string;
resolvedTargetVersion: string;
hasMigration: boolean;
requiresManualReview: boolean;
changes: IAppStoreUpgradeChange[];
warnings: string[];
blockers: string[];
config: plugins.interfaces.appstore.IAppStoreVersionConfig;
appMeta: plugins.interfaces.appstore.IAppStoreAppMeta;
}
export interface IAppStoreUpgradeOperation {
id: string;
serviceId: string;
serviceName: string;
appTemplateId: string;
fromVersion: string;
targetVersion: string;
status: TAppStoreUpgradeStatus;
step: TAppStoreUpgradeStep;
progressLines: string[];
warnings: string[];
error?: string;
startedAt: number;
updatedAt: number;
completedAt?: number;
service?: plugins.interfaces.data.IService;
}
export interface IAppStoreState {
apps: plugins.interfaces.appstore.IAppStoreApp[];
upgradeableServices: Array<plugins.interfaces.appstore.IUpgradeableAppStoreService & { serviceId?: string }>;
upgradeOperations: IAppStoreUpgradeOperation[];
}
const emptyDataState: IDataState = {
secretGroups: [],
secretBundles: [],
@@ -54,6 +111,12 @@ const emptyDataState: IDataState = {
backups: [],
};
const emptyAppStoreState: IAppStoreState = {
apps: [],
upgradeableServices: [],
upgradeOperations: [],
};
interface IReq_AdminValidateIdentity {
method: 'adminValidateIdentity';
request: {
@@ -119,6 +182,7 @@ export const logoutAction = loginStatePart.createAction(async (statePartArg) =>
try {
apiClient.identity = null;
dataState.setState({ ...emptyDataState });
appStoreStatePart.setState({ ...emptyAppStoreState });
} catch {}
return {
...currentState,
@@ -132,6 +196,12 @@ export const dataState = await appstate.getStatePart<IDataState>(
'soft'
);
export const appStoreStatePart = await appstate.getStatePart<IAppStoreState>(
'appstore',
{ ...emptyAppStoreState },
'soft',
);
// Shared API client instance (used by UI actions)
type TCloudlyApiClientWithNullableIdentity = Omit<plugins.servezoneApi.CloudlyApiClient, 'identity'> & {
identity: plugins.interfaces.data.IIdentity | null;
@@ -142,6 +212,54 @@ export const apiClient = new plugins.servezoneApi.CloudlyApiClient({
cloudlyUrl: (typeof window !== 'undefined' && window.location?.origin) ? window.location.origin : undefined,
}) as TCloudlyApiClientWithNullableIdentity;
const upsertUpgradeOperation = (
operationsArg: IAppStoreUpgradeOperation[],
operationArg: IAppStoreUpgradeOperation,
) => {
const operations = operationsArg.filter((existingOperation) => existingOperation.id !== operationArg.id);
operations.unshift(operationArg);
return operations.slice(0, 25);
};
const upsertService = (
servicesArg: plugins.interfaces.data.IService[] = [],
serviceArg: plugins.interfaces.data.IService,
) => {
const services = servicesArg.filter((existingService) => existingService.id !== serviceArg.id);
services.unshift(serviceArg);
return services;
};
apiClient.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<any>(
'pushAppStoreUpgradeProgress',
async (dataArg: { operation: IAppStoreUpgradeOperation }) => {
const appStoreState = appStoreStatePart.getState() || {
apps: [],
upgradeableServices: [],
upgradeOperations: [],
};
appStoreStatePart.setState({
...appStoreState,
upgradeOperations: upsertUpgradeOperation(appStoreState.upgradeOperations, dataArg.operation),
upgradeableServices: dataArg.operation.status === 'success'
? appStoreState.upgradeableServices.filter((serviceArg) => {
return serviceArg.serviceId !== dataArg.operation.serviceId && serviceArg.serviceName !== dataArg.operation.serviceName;
})
: appStoreState.upgradeableServices,
});
if (dataArg.operation.service) {
const currentDataState = dataState.getState() || {};
dataState.setState({
...currentDataState,
services: upsertService(currentDataState.services, dataArg.operation.service),
});
}
return {};
},
),
);
let identityExpiryTimer: number | undefined;
let identityInvalidationRunning = false;
@@ -184,6 +302,7 @@ export const invalidateIdentity = async (reasonArg = 'identity is not valid'): P
identity: null,
});
dataState.setState({ ...emptyDataState });
appStoreStatePart.setState({ ...emptyAppStoreState });
} finally {
identityInvalidationRunning = false;
}
@@ -737,3 +856,94 @@ export const addClusterAction = dataState.createAction(
return await context.dispatch(getAllDataAction, null);
}
);
const getIdentityForRequest = () => {
const identity = loginStatePart.getState()?.identity ?? null;
if (!identity) {
throw new Error('No Cloudly identity is available');
}
return identity;
};
export const fetchAppStoreTemplatesAction = appStoreStatePart.createAction(
async (statePartArg) => {
const request = new plugins.typedrequest.TypedRequest<any>('/typedrequest', 'getAppStoreTemplates');
const response = await request.fire({ identity: getIdentityForRequest() });
return {
...(statePartArg.getState() || { apps: [], upgradeableServices: [], upgradeOperations: [] }),
apps: response.apps || [],
};
},
);
export const fetchUpgradeableAppStoreServicesAction = appStoreStatePart.createAction(
async (statePartArg) => {
const request = new plugins.typedrequest.TypedRequest<any>('/typedrequest', 'getUpgradeableAppStoreServices');
const response = await request.fire({ identity: getIdentityForRequest() });
return {
...(statePartArg.getState() || { apps: [], upgradeableServices: [], upgradeOperations: [] }),
upgradeableServices: response.services || [],
};
},
);
export const fetchAppStoreUpgradeOperationsAction = appStoreStatePart.createAction(
async (statePartArg) => {
const request = new plugins.typedrequest.TypedRequest<any>('/typedrequest', 'getAppStoreUpgradeOperations');
const response = await request.fire({ identity: getIdentityForRequest() });
return {
...(statePartArg.getState() || { apps: [], upgradeableServices: [], upgradeOperations: [] }),
upgradeOperations: response.operations || [],
};
},
);
export const startAppStoreServiceUpgradeAction = appStoreStatePart.createAction<{
serviceId: string;
targetVersion: string;
}>(
async (statePartArg, payloadArg) => {
const request = new plugins.typedrequest.TypedRequest<any>('/typedrequest', 'startAppStoreServiceUpgrade');
const response = await request.fire({
identity: getIdentityForRequest(),
serviceId: payloadArg.serviceId,
targetVersion: payloadArg.targetVersion,
});
const currentState = statePartArg.getState() || { apps: [], upgradeableServices: [], upgradeOperations: [] };
return {
...currentState,
upgradeOperations: upsertUpgradeOperation(currentState.upgradeOperations, response.operation),
};
},
);
export const getAppStoreConfig = async (appIdArg: string, versionArg: string) => {
const request = new plugins.typedrequest.TypedRequest<any>('/typedrequest', 'getAppStoreConfig');
return await request.fire({
identity: getIdentityForRequest(),
appId: appIdArg,
version: versionArg,
}) as {
config: plugins.interfaces.appstore.IAppStoreVersionConfig;
appMeta: plugins.interfaces.appstore.IAppStoreAppMeta;
};
};
export const getAppStoreUpgradePreview = async (serviceIdArg: string, targetVersionArg?: string) => {
const request = new plugins.typedrequest.TypedRequest<any>('/typedrequest', 'getAppStoreUpgradePreview');
const response = await request.fire({
identity: getIdentityForRequest(),
serviceId: serviceIdArg,
targetVersion: targetVersionArg,
});
return response.preview as IAppStoreUpgradePreview;
};
export const installAppStoreApp = async (installArg: plugins.interfaces.appstore.IAppStoreInstallRequest) => {
const request = new plugins.typedrequest.TypedRequest<any>('/typedrequest', 'installAppStoreApp');
const response = await request.fire({
identity: getIdentityForRequest(),
install: installArg,
});
return response.service as plugins.interfaces.data.IService;
};