feat(appstore): add App Store install and upgrade workflows
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user