Files
cloudly/ts_web/appstate.ts
T

950 lines
32 KiB
TypeScript
Raw Permalink Normal View History

2024-04-20 12:21:41 +02:00
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 {
2026-05-08 13:56:20 +00:00
identity: plugins.interfaces.data.IIdentity | null;
2024-04-20 12:21:41 +02:00
}
2024-06-13 09:36:02 +02:00
export const loginStatePart: plugins.smartstate.StatePart<unknown, ILoginState> = await appstate.getStatePart<ILoginState>(
2024-04-20 12:21:41 +02:00
'login',
{ identity: null },
2024-04-20 12:21:41 +02:00
'persistent'
);
2026-05-21 16:16:00 +00:00
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<plugins.interfaces.appstore.IUpgradeableAppStoreService & { serviceId?: string }>;
upgradeOperations: IAppStoreUpgradeOperation[];
}
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: [],
};
interface IReq_AdminValidateIdentity {
method: 'adminValidateIdentity';
request: {
identity: plugins.interfaces.data.IIdentity;
};
response: {
valid: boolean;
reason?: string;
};
}
2026-05-21 16:16:00 +00:00
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<IUiState>(
'ui',
{
activeView: getInitialView(),
activeSubview: getInitialSubview(),
},
);
2024-04-20 12:21:41 +02:00
export const loginAction = loginStatePart.createAction<{ username: string; password: string }>(
async (statePartArg, payloadArg) => {
2026-05-08 13:56:20 +00:00
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 = {
2024-04-20 12:21:41 +02:00
...currentState,
identity,
2024-04-20 12:21:41 +02:00
};
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();
}
2026-05-08 13:56:20 +00:00
try { await apiClient.typedsocketClient.setTag('identity', apiClient.identity); } catch {}
}
} catch {}
return newState;
2024-04-20 12:21:41 +02:00
}
);
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
2026-05-08 13:56:20 +00:00
const currentState = statePartArg.getState() || { identity: null };
try {
apiClient.identity = null;
dataState.setState({ ...emptyDataState });
appStoreStatePart.setState({ ...emptyAppStoreState });
} catch {}
2024-04-20 12:21:41 +02:00
return {
...currentState,
identity: null,
2024-04-20 12:21:41 +02:00
};
});
export const dataState = await appstate.getStatePart<IDataState>(
'data',
{ ...emptyDataState },
2024-04-20 12:21:41 +02:00
'soft'
);
export const appStoreStatePart = await appstate.getStatePart<IAppStoreState>(
'appstore',
{ ...emptyAppStoreState },
'soft',
);
// Shared API client instance (used by UI actions)
2026-05-08 13:56:20 +00:00
type TCloudlyApiClientWithNullableIdentity = Omit<plugins.servezoneApi.CloudlyApiClient, 'identity'> & {
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,
2026-05-08 13:56:20 +00:00
}) 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;
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<void> => {
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 });
} finally {
identityInvalidationRunning = false;
}
};
export const validateStoredIdentity = async (): Promise<boolean> => {
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<IReq_AdminValidateIdentity>(
'/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();
2024-04-20 12:21:41 +02:00
// Getting data
export const getAllDataAction = dataState.createAction(async (statePartArg) => {
2026-05-08 13:56:20 +00:00
let currentState = statePartArg.getState() || {};
// SecretsGroups
try {
2026-05-08 13:56:20 +00:00
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 {
2026-05-08 13:56:20 +00:00
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: [],
};
}
2024-04-20 12:21:41 +02:00
// images
try {
2026-05-08 13:56:20 +00:00
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: [],
};
}
2024-04-20 12:21:41 +02:00
// Clusters
try {
2026-05-08 13:56:20 +00:00
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: [],
}
2024-04-20 12:21:41 +02:00
}
// External Registries via shared API client
try {
2026-05-08 13:56:20 +00:00
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 {
2026-05-08 13:56:20 +00:00
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 {
2026-05-08 13:56:20 +00:00
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 {
2026-05-08 13:56:20 +00:00
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 {
2026-05-08 13:56:20 +00:00
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: [],
};
}
2024-04-20 12:21:41 +02:00
return currentState;
});
// Service Actions
export const createServiceAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
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);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const updateServiceAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
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);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const deleteServiceAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
async (statePartArg, payloadArg: { serviceId: string }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.services.deleteService(payloadArg.serviceId);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
2024-04-20 12:21:41 +02:00
// SecretGroup Actions
export const createSecretGroupAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
async (statePartArg, payloadArg: { data: plugins.interfaces.data.ISecretGroup['data'] }, context) => {
let currentState = statePartArg.getState() || {};
try {
2026-05-08 13:56:20 +00:00
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.secretgroup.createSecretGroup(payloadArg.data);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
} catch (err) {
console.error('Failed to create secret group:', err);
}
2024-04-20 12:21:41 +02:00
return currentState;
}
);
export const deleteSecretGroupAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
async (statePartArg, payloadArg: { secretGroupId: string }, context) => {
let currentState = statePartArg.getState() || {};
try {
2026-05-08 13:56:20 +00:00
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.secretgroup.deleteSecretGroupById(payloadArg.secretGroupId);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
} catch (err) {
console.error('Failed to delete secret group:', err);
}
2024-04-20 12:21:41 +02:00
return currentState;
}
);
// SecretBundle Actions
export const deleteSecretBundleAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
async (statePartArg, payloadArg: { configBundleId: string }, context) => {
let currentState = statePartArg.getState() || {};
try {
2026-05-08 13:56:20 +00:00
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.secretbundle.deleteSecretBundleById(payloadArg.configBundleId);
2026-05-08 13:56:20 +00:00
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(
2026-05-08 13:56:20 +00:00
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 });
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const deleteImageAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
async (statePartArg, payloadArg: { imageId: string }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.image.deleteImage(payloadArg.imageId);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
2024-04-20 12:21:41 +02:00
return currentState;
}
);
// Deployment Actions
export const createDeploymentAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
async (statePartArg, payloadArg: { deploymentData: Partial<plugins.interfaces.data.IDeployment> }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.deployments.createDeployment(payloadArg.deploymentData);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const updateDeploymentAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
async (statePartArg, payloadArg: { deploymentId: string; deploymentData: Partial<plugins.interfaces.data.IDeployment> }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.deployments.updateDeployment(payloadArg.deploymentId, payloadArg.deploymentData);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const deleteDeploymentAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
async (statePartArg, payloadArg: { deploymentId: string }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.deployments.deleteDeployment(payloadArg.deploymentId);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
// DNS Actions
export const createDnsEntryAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
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);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const updateDnsEntryAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
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);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const deleteDnsEntryAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
async (statePartArg, payloadArg: { dnsEntryId: string }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.dns.deleteDnsEntry(payloadArg.dnsEntryId);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
// Domain Actions
export const createDomainAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
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);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const updateDomainAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
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);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const deleteDomainAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
async (statePartArg, payloadArg: { domainId: string }, context) => {
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.domains.deleteDomain(payloadArg.domainId);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const verifyDomainAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
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);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
// External Registry Actions
export const createExternalRegistryAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
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);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
return currentState;
}
);
export const updateExternalRegistryAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
async (statePartArg, payloadArg: { registryId: string; registryData: plugins.interfaces.data.IExternalRegistry['data'] }, context) => {
let currentState = statePartArg.getState() || {};
try {
2026-05-08 13:56:20 +00:00
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.externalRegistry.updateRegistry(payloadArg.registryId, payloadArg.registryData);
2026-05-08 13:56:20 +00:00
currentState = await context.dispatch(getAllDataAction, null);
} catch (err) {
console.error('Failed to update external registry:', err);
}
return currentState;
}
);
export const deleteExternalRegistryAction = dataState.createAction(
2026-05-08 13:56:20 +00:00
async (statePartArg, payloadArg: { registryId: string }, context) => {
let currentState = statePartArg.getState() || {};
try {
2026-05-08 13:56:20 +00:00
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.externalRegistry.deleteRegistry(payloadArg.registryId);
2026-05-08 13:56:20 +00:00
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 }) => {
2026-05-08 13:56:20 +00:00
let currentState = statePartArg.getState() || {};
try {
2026-05-08 13:56:20 +00:00
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: {}) => {
2026-05-08 13:56:20 +00:00
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 }) => {
2026-05-08 13:56:20 +00:00
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 }) => {
2026-05-08 13:56:20 +00:00
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 }) => {
2026-05-08 13:56:20 +00:00
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 }) => {
2026-05-08 13:56:20 +00:00
const currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.tasks.cancelTask(payloadArg.executionId);
return currentState;
}
),
};
2024-04-20 12:21:41 +02:00
// cluster
export const addClusterAction = dataState.createAction(
async (
statePartArg,
payloadArg: {
clusterName: string;
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
2026-05-08 13:56:20 +00:00
},
context
2024-04-20 12:21:41 +02:00
) => {
2026-05-08 13:56:20 +00:00
let currentState = statePartArg.getState() || {};
apiClient.identity = loginStatePart.getState()?.identity ?? null;
await apiClient.cluster.createClusterAdvanced(payloadArg.clusterName, payloadArg.setupMode);
2026-05-08 13:56:20 +00:00
return await context.dispatch(getAllDataAction, null);
2024-04-20 12:21:41 +02:00
}
);
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;
};