1077 lines
36 KiB
TypeScript
1077 lines
36 KiB
TypeScript
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<unknown, ILoginState> = await appstate.getStatePart<ILoginState>(
|
|
'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<plugins.interfaces.appstore.IUpgradeableAppStoreService & { serviceId?: string }>;
|
|
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<IUiState>(
|
|
'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<IDataState>(
|
|
'data',
|
|
{ ...emptyDataState },
|
|
'soft'
|
|
);
|
|
|
|
export const appStoreStatePart = await appstate.getStatePart<IAppStoreState>(
|
|
'appstore',
|
|
{ ...emptyAppStoreState },
|
|
'soft',
|
|
);
|
|
|
|
export const hostedRuntimeStatePart = await appstate.getStatePart<IHostedRuntimeState>(
|
|
'hostedRuntime',
|
|
{ ...emptyHostedRuntimeState },
|
|
'soft',
|
|
);
|
|
|
|
// Shared API client instance (used by UI actions)
|
|
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,
|
|
}) 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 });
|
|
hostedRuntimeStatePart.setState({ ...emptyHostedRuntimeState });
|
|
clearHostedRuntimeUpgradePoll();
|
|
} 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();
|
|
|
|
// 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<plugins.interfaces.data.IDeployment> }, 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<plugins.interfaces.data.IDeployment> }, 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<plugins.interfaces.requests.hostedapp.IReq_Admin_GetHostedAppParentUpgradeStatus>(
|
|
'/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<plugins.interfaces.requests.hostedapp.IReq_Admin_StartHostedAppParentUpgrade>(
|
|
'/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<any>('/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<any>('/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<any>('/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<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');
|
|
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<any>('/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<any>('/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;
|
|
};
|