import * as plugins from './plugins.js'; import * as domtools from '@design.estate/dees-domtools'; const appstate = new plugins.deesDomtools.plugins.smartstate.Smartstate(); export interface ILoginState { identity: plugins.interfaces.data.IIdentity | null; } export const loginStatePart: plugins.smartstate.StatePart = await appstate.getStatePart( 'login', { identity: null }, 'persistent' ); export interface IUiState { activeView: string; activeSubview: string | null; } export interface IDataState { secretGroups?: plugins.interfaces.data.ISecretGroup[]; secretBundles?: plugins.interfaces.data.ISecretBundle[]; clusters?: plugins.interfaces.data.ICluster[]; externalRegistries?: plugins.interfaces.data.IExternalRegistry[]; images?: plugins.interfaces.data.IImage[]; services?: plugins.interfaces.data.IService[]; deployments?: plugins.interfaces.data.IDeployment[]; domains?: plugins.interfaces.data.IDomain[]; dnsEntries?: plugins.interfaces.data.IDnsEntry[]; tasks?: any[]; taskExecutions?: plugins.interfaces.data.ITaskExecution[]; mails?: any[]; logs?: any[]; s3?: any[]; dbs?: any[]; 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; upgradeOperations: IAppStoreUpgradeOperation[]; } export interface IHostedRuntimeState { isHosted: boolean; loading: boolean; unavailableReason?: string; upgradeState: plugins.interfaces.data.IHostedAppUpgradeState | null; } const emptyDataState: IDataState = { secretGroups: [], secretBundles: [], clusters: [], externalRegistries: [], images: [], services: [], deployments: [], domains: [], dnsEntries: [], tasks: [], taskExecutions: [], mails: [], logs: [], s3: [], dbs: [], backups: [], }; const emptyAppStoreState: IAppStoreState = { apps: [], upgradeableServices: [], upgradeOperations: [], }; const emptyHostedRuntimeState: IHostedRuntimeState = { isHosted: false, loading: false, upgradeState: null, }; interface IReq_AdminValidateIdentity { method: 'adminValidateIdentity'; request: { identity: plugins.interfaces.data.IIdentity; }; response: { valid: boolean; reason?: string; }; } const getInitialView = (): string => { const path = typeof window !== 'undefined' ? window.location.pathname : '/'; const validViews = ['overview', 'platform', 'runtime', 'registry', 'secrets', 'domains', 'storage', 'logs']; const segments = path.split('/').filter(Boolean); const view = segments[0]; return validViews.includes(view) ? view : 'overview'; }; const getInitialSubview = (): string | null => { const path = typeof window !== 'undefined' ? window.location.pathname : '/'; const segments = path.split('/').filter(Boolean); return segments[1] ?? null; }; export const uiStatePart = await appstate.getStatePart( 'ui', { activeView: getInitialView(), activeSubview: getInitialSubview(), }, ); export const loginAction = loginStatePart.createAction<{ username: string; password: string }>( async (statePartArg, payloadArg) => { const currentState = statePartArg.getState() || { identity: null }; let identity: plugins.interfaces.data.IIdentity | null = null; try { identity = await apiClient.loginWithUsernameAndPassword(payloadArg.username, payloadArg.password); } catch (err) { console.log(err); } const newState = { ...currentState, identity, }; try { // Keep shared API client in sync and establish WS for modules using sockets apiClient.identity = identity || null; if (apiClient.identity) { if (!apiClient['typedsocketClient']) { await apiClient.start(); } try { await apiClient.typedsocketClient.setTag('identity', apiClient.identity); } catch {} } } catch {} return newState; } ); export const logoutAction = loginStatePart.createAction(async (statePartArg) => { const currentState = statePartArg.getState() || { identity: null }; try { apiClient.identity = null; dataState.setState({ ...emptyDataState }); appStoreStatePart.setState({ ...emptyAppStoreState }); hostedRuntimeStatePart.setState({ ...emptyHostedRuntimeState }); clearHostedRuntimeUpgradePoll(); } catch {} return { ...currentState, identity: null, }; }); export const dataState = await appstate.getStatePart( 'data', { ...emptyDataState }, 'soft' ); export const appStoreStatePart = await appstate.getStatePart( 'appstore', { ...emptyAppStoreState }, 'soft', ); export const hostedRuntimeStatePart = await appstate.getStatePart( 'hostedRuntime', { ...emptyHostedRuntimeState }, 'soft', ); // Shared API client instance (used by UI actions) type TCloudlyApiClientWithNullableIdentity = Omit & { identity: plugins.interfaces.data.IIdentity | null; }; export const apiClient = new plugins.servezoneApi.CloudlyApiClient({ registerAs: 'api', 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( '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; const getErrorText = (errorArg: unknown): string => { if (!errorArg) return ''; if (typeof errorArg === 'string') return errorArg; const errorLike = errorArg as { errorText?: string; message?: string; text?: string }; return errorLike.errorText || errorLike.message || errorLike.text || ''; }; const isAuthRejectionText = (errorTextArg: string): boolean => { const errorText = errorTextArg.toLowerCase(); return [ 'identity is not valid', 'jwt expired', 'identity is expired', 'user not logged in', 'has been tampered with', 'invalid jwt', 'invalid signature', ].some((textPart) => errorText.includes(textPart)); }; export const isIdentityExpired = (identityArg: plugins.interfaces.data.IIdentity | null | undefined): boolean => { return typeof identityArg?.expiresAt === 'number' && identityArg.expiresAt <= Date.now(); }; export const invalidateIdentity = async (reasonArg = 'identity is not valid'): Promise => { if (identityInvalidationRunning) return; identityInvalidationRunning = true; try { const currentLoginState = loginStatePart.getState() || { identity: null }; if (currentLoginState.identity) { console.warn(`Cloudly session invalidated: ${reasonArg}`); } apiClient.identity = null; try { await apiClient.typedsocketClient?.setTag('identity', null); } catch {} loginStatePart.setState({ ...currentLoginState, identity: null, }); dataState.setState({ ...emptyDataState }); appStoreStatePart.setState({ ...emptyAppStoreState }); hostedRuntimeStatePart.setState({ ...emptyHostedRuntimeState }); clearHostedRuntimeUpgradePoll(); } finally { identityInvalidationRunning = false; } }; export const validateStoredIdentity = async (): Promise => { const identity = loginStatePart.getState()?.identity ?? null; if (!identity) return false; if (isIdentityExpired(identity)) { await invalidateIdentity('identity expired'); return false; } const validateIdentityRequest = new plugins.typedrequest.TypedRequest( '/typedrequest', 'adminValidateIdentity', ); try { const response = await validateIdentityRequest.fire({ identity }); if (!response?.valid) { await invalidateIdentity(response?.reason || 'identity rejected by server'); return false; } } catch (error) { const errorText = getErrorText(error); if (isAuthRejectionText(errorText)) { await invalidateIdentity(errorText); return false; } console.warn('Could not validate stored identity:', error); } return !!loginStatePart.getState()?.identity; }; const scheduleIdentityExpiryTimer = () => { if (identityExpiryTimer) { window.clearTimeout(identityExpiryTimer); identityExpiryTimer = undefined; } const identity = loginStatePart.getState()?.identity ?? null; if (!identity?.expiresAt) return; const msUntilExpiry = identity.expiresAt - Date.now(); if (msUntilExpiry <= 0) { void invalidateIdentity('identity expired'); return; } identityExpiryTimer = window.setTimeout(() => { void invalidateIdentity('identity expired'); }, Math.min(msUntilExpiry, 2147483647)); }; plugins.typedrequest.TypedRouter.setGlobalHooks({ onIncomingResponse: (entryArg) => { if (entryArg.error && isAuthRejectionText(entryArg.error)) { void invalidateIdentity(entryArg.error); } }, }); loginStatePart.select((stateArg) => stateArg?.identity ?? null).subscribe(() => { scheduleIdentityExpiryTimer(); }); scheduleIdentityExpiryTimer(); // Getting data export const getAllDataAction = dataState.createAction(async (statePartArg) => { let currentState = statePartArg.getState() || {}; // SecretsGroups try { apiClient.identity = loginStatePart.getState()?.identity ?? null; const secretGroups = await apiClient.secretgroup.getSecretGroups(); currentState = { ...currentState, secretGroups: secretGroups, }; } catch (err) { console.error('Failed to fetch secret groups:', err); currentState = { ...currentState, secretGroups: [], }; } // SecretBundles try { apiClient.identity = loginStatePart.getState()?.identity ?? null; const responseSecretBundles = await apiClient.secretbundle.getSecretBundles(); currentState = { ...currentState, secretBundles: responseSecretBundles, }; } catch (err) { console.error('Failed to fetch secret bundles:', err); currentState = { ...currentState, secretBundles: [], }; } // images try { apiClient.identity = loginStatePart.getState()?.identity ?? null; const images = await apiClient.image.getImages(); currentState = { ...currentState, images: images, }; } catch (err) { console.error('Failed to fetch images:', err); currentState = { ...currentState, images: [], }; } // Clusters try { apiClient.identity = loginStatePart.getState()?.identity ?? null; const clusters = await apiClient.cluster.getClusters(); currentState = { ...currentState, clusters: clusters, } } catch (err) { console.error('Failed to fetch clusters:', err); currentState = { ...currentState, clusters: [], } } // External Registries via shared API client try { apiClient.identity = loginStatePart.getState()?.identity ?? null; const registries = await apiClient.externalRegistry.getRegistries(); currentState = { ...currentState, externalRegistries: registries, }; } catch (error) { console.error('Failed to fetch external registries:', error); currentState = { ...currentState, externalRegistries: [], }; } // Services try { apiClient.identity = loginStatePart.getState()?.identity ?? null; const services = await apiClient.services.getServices(); currentState = { ...currentState, services: services, }; } catch (error) { console.error('Failed to fetch services:', error); currentState = { ...currentState, services: [], }; } // Deployments try { apiClient.identity = loginStatePart.getState()?.identity ?? null; const responseDeployments = await apiClient.deployments.getDeployments(); currentState = { ...currentState, deployments: responseDeployments?.deployments || [], }; } catch (error) { console.error('Failed to fetch deployments:', error); currentState = { ...currentState, deployments: [], }; } // Domains via API client try { apiClient.identity = loginStatePart.getState()?.identity ?? null; const responseDomains = await apiClient.domains.getDomains(); currentState = { ...currentState, domains: responseDomains?.domains || [], }; } catch (error) { console.error('Failed to fetch domains:', error); currentState = { ...currentState, domains: [], }; } // DNS Entries via API client try { apiClient.identity = loginStatePart.getState()?.identity ?? null; const responseDnsEntries = await apiClient.dns.getDnsEntries(); currentState = { ...currentState, dnsEntries: responseDnsEntries?.dnsEntries || [], }; } catch (error) { console.error('Failed to fetch DNS entries:', error); currentState = { ...currentState, dnsEntries: [], }; } return currentState; }); // Service Actions export const createServiceAction = dataState.createAction( async (statePartArg, payloadArg: { serviceData: plugins.interfaces.data.IService['data'] }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.services.createService(payloadArg.serviceData); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); export const updateServiceAction = dataState.createAction( async (statePartArg, payloadArg: { serviceId: string; serviceData: plugins.interfaces.data.IService['data'] }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.services.updateService(payloadArg.serviceId, payloadArg.serviceData); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); export const deleteServiceAction = dataState.createAction( async (statePartArg, payloadArg: { serviceId: string }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.services.deleteService(payloadArg.serviceId); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); // SecretGroup Actions export const createSecretGroupAction = dataState.createAction( async (statePartArg, payloadArg: { data: plugins.interfaces.data.ISecretGroup['data'] }, context) => { let currentState = statePartArg.getState() || {}; try { apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.secretgroup.createSecretGroup(payloadArg.data); currentState = await context.dispatch(getAllDataAction, null); } catch (err) { console.error('Failed to create secret group:', err); } return currentState; } ); export const deleteSecretGroupAction = dataState.createAction( async (statePartArg, payloadArg: { secretGroupId: string }, context) => { let currentState = statePartArg.getState() || {}; try { apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.secretgroup.deleteSecretGroupById(payloadArg.secretGroupId); currentState = await context.dispatch(getAllDataAction, null); } catch (err) { console.error('Failed to delete secret group:', err); } return currentState; } ); // SecretBundle Actions export const deleteSecretBundleAction = dataState.createAction( async (statePartArg, payloadArg: { configBundleId: string }, context) => { let currentState = statePartArg.getState() || {}; try { apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.secretbundle.deleteSecretBundleById(payloadArg.configBundleId); currentState = await context.dispatch(getAllDataAction, null); } catch (err) { console.error('Failed to delete secret bundle:', err); } return currentState; } ); // image actions export const createImageAction = dataState.createAction( async (statePartArg, payloadArg: { imageName: string, description: string }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.image.createImage({ name: payloadArg.imageName, description: payloadArg.description }); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); export const deleteImageAction = dataState.createAction( async (statePartArg, payloadArg: { imageId: string }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.image.deleteImage(payloadArg.imageId); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); // Deployment Actions export const createDeploymentAction = dataState.createAction( async (statePartArg, payloadArg: { deploymentData: Partial }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.deployments.createDeployment(payloadArg.deploymentData); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); export const updateDeploymentAction = dataState.createAction( async (statePartArg, payloadArg: { deploymentId: string; deploymentData: Partial }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.deployments.updateDeployment(payloadArg.deploymentId, payloadArg.deploymentData); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); export const deleteDeploymentAction = dataState.createAction( async (statePartArg, payloadArg: { deploymentId: string }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.deployments.deleteDeployment(payloadArg.deploymentId); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); // DNS Actions export const createDnsEntryAction = dataState.createAction( async (statePartArg, payloadArg: { dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.dns.createDnsEntry(payloadArg.dnsEntryData); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); export const updateDnsEntryAction = dataState.createAction( async (statePartArg, payloadArg: { dnsEntryId: string; dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.dns.updateDnsEntry(payloadArg.dnsEntryId, payloadArg.dnsEntryData); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); export const deleteDnsEntryAction = dataState.createAction( async (statePartArg, payloadArg: { dnsEntryId: string }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.dns.deleteDnsEntry(payloadArg.dnsEntryId); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); // Domain Actions export const createDomainAction = dataState.createAction( async (statePartArg, payloadArg: { domainData: plugins.interfaces.data.IDomain['data'] }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.domains.createDomain(payloadArg.domainData); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); export const updateDomainAction = dataState.createAction( async (statePartArg, payloadArg: { domainId: string; domainData: plugins.interfaces.data.IDomain['data'] }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.domains.updateDomain(payloadArg.domainId, payloadArg.domainData); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); export const deleteDomainAction = dataState.createAction( async (statePartArg, payloadArg: { domainId: string }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.domains.deleteDomain(payloadArg.domainId); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); export const verifyDomainAction = dataState.createAction( async (statePartArg, payloadArg: { domainId: string; verificationMethod?: 'dns' | 'http' | 'email' | 'manual' }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.domains.verifyDomain(payloadArg.domainId, payloadArg.verificationMethod); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); // External Registry Actions export const createExternalRegistryAction = dataState.createAction( async (statePartArg, payloadArg: { registryData: plugins.interfaces.data.IExternalRegistry['data'] }, context) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.externalRegistry.createRegistry(payloadArg.registryData); currentState = await context.dispatch(getAllDataAction, null); return currentState; } ); export const updateExternalRegistryAction = dataState.createAction( async (statePartArg, payloadArg: { registryId: string; registryData: plugins.interfaces.data.IExternalRegistry['data'] }, context) => { let currentState = statePartArg.getState() || {}; try { apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.externalRegistry.updateRegistry(payloadArg.registryId, payloadArg.registryData); currentState = await context.dispatch(getAllDataAction, null); } catch (err) { console.error('Failed to update external registry:', err); } return currentState; } ); export const deleteExternalRegistryAction = dataState.createAction( async (statePartArg, payloadArg: { registryId: string }, context) => { let currentState = statePartArg.getState() || {}; try { apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.externalRegistry.deleteRegistry(payloadArg.registryId); currentState = await context.dispatch(getAllDataAction, null); } catch (err) { console.error('Failed to delete external registry:', err); } return currentState; } ); export const verifyExternalRegistryAction = dataState.createAction( async (statePartArg, payloadArg: { registryId: string }) => { let currentState = statePartArg.getState() || {}; try { apiClient.identity = loginStatePart.getState()?.identity ?? null; const result = await apiClient.externalRegistry.verifyRegistry(payloadArg.registryId); if (result.success && result.registry) { const regs = (currentState.externalRegistries || []).slice(); const idx = regs.findIndex(r => r.id === payloadArg.registryId); if (idx >= 0) { // Preserve instance; update its data + shallow props const instance: any = regs[idx]; instance.data = result.registry.data; instance.id = result.registry.id; regs[idx] = instance; } currentState = { ...currentState, externalRegistries: regs, }; } } catch (err) { console.error('Failed to verify external registry:', err); } return currentState; } ); // Task Actions export const taskActions = { getTasks: dataState.createAction( async (statePartArg, payloadArg: {}) => { const currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; const response = await apiClient.tasks.getTasks(); return { ...currentState, tasks: response.tasks, }; } ), getTaskExecutions: dataState.createAction( async (statePartArg, payloadArg: { filter?: any }) => { const currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; const response = await apiClient.tasks.getTaskExecutions(payloadArg.filter); return { ...currentState, taskExecutions: response.executions, }; } ), getTaskExecutionById: dataState.createAction( async (statePartArg, payloadArg: { executionId: string }) => { const currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.tasks.getTaskExecutionById(payloadArg.executionId); return currentState; } ), triggerTask: dataState.createAction( async (statePartArg, payloadArg: { taskName: string; userId?: string }) => { const currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.tasks.triggerTask(payloadArg.taskName, payloadArg.userId); return currentState; } ), cancelTask: dataState.createAction( async (statePartArg, payloadArg: { executionId: string }) => { const currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.tasks.cancelTask(payloadArg.executionId); return currentState; } ), }; // cluster export const addClusterAction = dataState.createAction( async ( statePartArg, payloadArg: { clusterName: string; setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean'; }, context ) => { let currentState = statePartArg.getState() || {}; apiClient.identity = loginStatePart.getState()?.identity ?? null; await apiClient.cluster.createClusterAdvanced(payloadArg.clusterName, payloadArg.setupMode); 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; }; let hostedRuntimePollTimer: number | undefined; function clearHostedRuntimeUpgradePoll() { if (hostedRuntimePollTimer) { window.clearTimeout(hostedRuntimePollTimer); hostedRuntimePollTimer = undefined; } } const scheduleHostedRuntimeUpgradePoll = (stateArg: IHostedRuntimeState) => { clearHostedRuntimeUpgradePoll(); if (stateArg.upgradeState?.status !== 'running') { return; } hostedRuntimePollTimer = window.setTimeout(() => { void hostedRuntimeStatePart.dispatchAction(fetchHostedRuntimeUpgradeStatusAction, null); }, 5000); }; export const fetchHostedRuntimeUpgradeStatusAction = hostedRuntimeStatePart.createAction( async (statePartArg) => { const currentState = statePartArg.getState() || { ...emptyHostedRuntimeState }; statePartArg.setState({ ...currentState, loading: true }); try { const request = new plugins.typedrequest.TypedRequest( '/typedrequest', 'getHostedAppParentUpgradeStatus', ); const response = await request.fire({ identity: getIdentityForRequest() }); const nextState: IHostedRuntimeState = { isHosted: response.isHosted, loading: false, unavailableReason: response.unavailableReason, upgradeState: response.upgradeState, }; scheduleHostedRuntimeUpgradePoll(nextState); return nextState; } catch (error) { const nextState: IHostedRuntimeState = { ...currentState, loading: false, unavailableReason: getErrorText(error) || 'Could not load hosted runtime status.', }; scheduleHostedRuntimeUpgradePoll(nextState); return nextState; } }, ); export const startHostedRuntimeParentUpgradeAction = hostedRuntimeStatePart.createAction<{ targetVersion?: string; } | null>( async (statePartArg, payloadArg) => { const currentState = statePartArg.getState() || { ...emptyHostedRuntimeState }; statePartArg.setState({ ...currentState, loading: true }); try { const request = new plugins.typedrequest.TypedRequest( '/typedrequest', 'startHostedAppParentUpgrade', ); const response = await request.fire({ identity: getIdentityForRequest(), targetVersion: payloadArg?.targetVersion, }); const nextState: IHostedRuntimeState = { isHosted: response.isHosted, loading: false, unavailableReason: response.unavailableReason, upgradeState: response.upgradeState, }; scheduleHostedRuntimeUpgradePoll(nextState); return nextState; } catch (error) { const nextState: IHostedRuntimeState = { ...currentState, loading: false, unavailableReason: getErrorText(error) || 'Could not start hosted runtime upgrade.', }; statePartArg.setState(nextState); scheduleHostedRuntimeUpgradePoll(nextState); throw error; } }, ); export const fetchAppStoreTemplatesAction = appStoreStatePart.createAction( async (statePartArg) => { const request = new plugins.typedrequest.TypedRequest('/typedrequest', 'getAppStoreTemplates'); const response = await request.fire({ identity: getIdentityForRequest() }); if (!response?.apps) { throw new Error('The App Store returned an empty template response. Please retry.'); } return { ...(statePartArg.getState() || { apps: [], upgradeableServices: [], upgradeOperations: [] }), apps: response.apps, }; }, ); export const fetchUpgradeableAppStoreServicesAction = appStoreStatePart.createAction( async (statePartArg) => { const request = new plugins.typedrequest.TypedRequest('/typedrequest', 'getUpgradeableAppStoreServices'); const response = await request.fire({ identity: getIdentityForRequest() }); if (!response?.services) { throw new Error('The App Store returned an empty upgradeable-services response. Please retry.'); } return { ...(statePartArg.getState() || { apps: [], upgradeableServices: [], upgradeOperations: [] }), upgradeableServices: response.services, }; }, ); export const fetchAppStoreUpgradeOperationsAction = appStoreStatePart.createAction( async (statePartArg) => { const request = new plugins.typedrequest.TypedRequest('/typedrequest', 'getAppStoreUpgradeOperations'); const response = await request.fire({ identity: getIdentityForRequest() }); if (!response?.operations) { throw new Error('The App Store returned an empty upgrade-operations response. Please retry.'); } 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('/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('/typedrequest', 'getAppStoreConfig'); const response = await request.fire({ identity: getIdentityForRequest(), appId: appIdArg, version: versionArg, }) as { config: plugins.interfaces.appstore.IAppStoreVersionConfig; appMeta: plugins.interfaces.appstore.IAppStoreAppMeta; }; if (!response?.config || !response?.appMeta) { throw new Error('The App Store returned an empty config response. Please retry.'); } return response; }; export const getAppStoreUpgradePreview = async (serviceIdArg: string, targetVersionArg?: string) => { const request = new plugins.typedrequest.TypedRequest('/typedrequest', 'getAppStoreUpgradePreview'); const response = await request.fire({ identity: getIdentityForRequest(), serviceId: serviceIdArg, targetVersion: targetVersionArg, }); if (!response?.preview) { throw new Error('The App Store returned an empty upgrade preview response. Please retry.'); } return response.preview as IAppStoreUpgradePreview; }; export const installAppStoreApp = async (installArg: plugins.interfaces.appstore.IAppStoreInstallRequest) => { const request = new plugins.typedrequest.TypedRequest('/typedrequest', 'installAppStoreApp'); const response = await request.fire({ identity: getIdentityForRequest(), install: installArg, }); if (!response?.service) { throw new Error('The App Store returned an empty install response. Please retry.'); } return response.service as plugins.interfaces.data.IService; };